Skip to content

Commit

Permalink
support RSA public key serialization
Browse files Browse the repository at this point in the history
  • Loading branch information
reaperhulk committed Mar 8, 2015
1 parent 7d5483b commit 3f157e0
Show file tree
Hide file tree
Showing 9 changed files with 197 additions and 7 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,14 @@ Changelog
:meth:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKeyWithSerialization.private_bytes`
to
:class:`~cryptography.hazmat.primitives.asymmetric.dsa.DSAPrivateKeyWithSerialization`.
* Added
:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKeyWithSerialization`
and deprecated
:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKeyWithNumbers`.
* Added
:meth:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKeyWithSerialization.public_bytes`
to
:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKeyWithSerialization`.

0.7.2 - 2015-01-16
~~~~~~~~~~~~~~~~~~
Expand Down
55 changes: 54 additions & 1 deletion docs/hazmat/primitives/asymmetric/rsa.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ There is also support for :func:`loading public keys in the SSH format
Key serialization
~~~~~~~~~~~~~~~~~

If you have a key that you've loaded or generated which implements the
If you have a private key that you've loaded or generated which implements the
:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization`
interface you can use
:meth:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization.private_bytes`
Expand Down Expand Up @@ -113,6 +113,23 @@ It is also possible to serialize without encryption using
>>> pem.splitlines()[0]
'-----BEGIN RSA PRIVATE KEY-----'

Similarly, if your public key implements
:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKeyWithSerialization`
interface you can use
:meth:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKeyWithSerialization.public_bytes`
to serialize the key.

.. doctest::

>>> from cryptography.hazmat.primitives import serialization
>>> public_key = private_key.public_key()
>>> pem = public_key.public_bytes(
... encoding=serialization.Encoding.PEM,
... format=serialization.PublicFormat.SubjectPublicKeyInfo
... )
>>> pem.splitlines()[0]
'-----BEGIN PUBLIC KEY-----'

Signing
~~~~~~~

Expand Down Expand Up @@ -626,6 +643,42 @@ Key interfaces
instance.


.. class:: RSAPublicKeyWithSerialization

.. versionadded:: 0.8

Extends :class:`RSAPublicKey`.

.. method:: public_numbers()

Create a
:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicNumbers`
object.

:returns: An
:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicNumbers`
instance.

.. method:: public_bytes(encoding, format)

Allows serialization of the key to bytes. Encoding (
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.PEM` or
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.DER`) and
format (
:attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.SubjectPublicKeyInfo`
or
:attr:`~cryptography.hazmat.primitives.serialization.PublicFormat.PKCS1`)
are chosen to define the exact serialization.

:param encoding: A value from the
:class:`~cryptography.hazmat.primitives.serialization.Encoding` enum.

:param format: A value from the
:class:`~cryptography.hazmat.primitives.serialization.PublicFormat` enum.

:return bytes: Serialized key.


.. _`RSA`: https://en.wikipedia.org/wiki/RSA_(cryptosystem)
.. _`public-key`: https://en.wikipedia.org/wiki/Public-key_cryptography
.. _`specific mathematical properties`: https://en.wikipedia.org/wiki/RSA_(cryptosystem)#Key_generation
Expand Down
19 changes: 19 additions & 0 deletions docs/hazmat/primitives/asymmetric/serialization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,25 @@ Serialization Formats
encryption. Choose this unless you have explicit legacy compatibility
requirements.

.. class:: PublicFormat

.. versionadded:: 0.8

An enumeration for public key formats. Used with the ``public_bytes``
method available on
:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKeyWithSerialization`.

.. attribute:: SubjectPublicKeyInfo

This is the typical public key format. It consists of an algorithm
identifier and the public key as a bit string. Choose this unless
you have specific needs.

.. attribute:: PKCS1

Just the public key elements (without the algorithm identifier). This
format is occasionally used by various systems.

Serialization Encodings
~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
26 changes: 26 additions & 0 deletions src/cryptography/hazmat/backends/openssl/backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -1184,6 +1184,32 @@ def _private_key_bytes(self, encoding, format, encryption_algorithm,
assert res == 1
return self._read_mem_bio(bio)

def _public_key_bytes(self, encoding, format, pkcs1_write_func, evp_pkey,
cdata):
if not isinstance(encoding, serialization.Encoding):
raise TypeError("encoding must be an item from the Encoding enum")

if not isinstance(format, serialization.PublicFormat):
raise TypeError(
"format must be an item from the PublicFormat enum"
)

# This is a temporary check until we land DER serialization.
if encoding is not serialization.Encoding.PEM:
raise ValueError("Only PEM encoding is supported by this backend")

if format is serialization.PublicFormat.SubjectPublicKeyInfo:
write_bio = self._lib.PEM_write_bio_PUBKEY
key = evp_pkey
elif format is serialization.PublicFormat.PKCS1:
write_bio = pkcs1_write_func
key = cdata

bio = self._create_mem_bio()
res = write_bio(bio, key)
assert res == 1
return self._read_mem_bio(bio)


class GetCipherByName(object):
def __init__(self, fmt):
Expand Down
13 changes: 11 additions & 2 deletions src/cryptography/hazmat/backends/openssl/rsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
)
from cryptography.hazmat.primitives.asymmetric.rsa import (
RSAPrivateKeyWithNumbers, RSAPrivateKeyWithSerialization,
RSAPublicKeyWithNumbers
RSAPublicKeyWithSerialization
)


Expand Down Expand Up @@ -572,7 +572,7 @@ def private_bytes(self, encoding, format, encryption_algorithm):
)


@utils.register_interface(RSAPublicKeyWithNumbers)
@utils.register_interface(RSAPublicKeyWithSerialization)
class _RSAPublicKey(object):
def __init__(self, backend, rsa_cdata):
self._backend = backend
Expand Down Expand Up @@ -604,3 +604,12 @@ def public_numbers(self):
e=self._backend._bn_to_int(self._rsa_cdata.e),
n=self._backend._bn_to_int(self._rsa_cdata.n),
)

def public_bytes(self, encoding, format):
return self._backend._public_key_bytes(
encoding,
format,
self._backend._lib.PEM_write_bio_RSAPublicKey,
self._evp_pkey,
self._rsa_cdata
)
19 changes: 18 additions & 1 deletion src/cryptography/hazmat/primitives/asymmetric/rsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,13 +89,30 @@ def key_size(self):


@six.add_metaclass(abc.ABCMeta)
class RSAPublicKeyWithNumbers(RSAPublicKey):
class RSAPublicKeyWithSerialization(RSAPublicKey):
@abc.abstractmethod
def public_numbers(self):
"""
Returns an RSAPublicNumbers
"""

@abc.abstractmethod
def public_bytes(self, encoding, format):
"""
Returns the key serialized as bytes.
"""


RSAPublicKeyWithNumbers = utils.deprecated(
RSAPublicKeyWithSerialization,
__name__,
(
"The RSAPublicKeyWithNumbers interface has been renamed to "
"RSAPublicKeyWithSerialization"
),
utils.DeprecatedIn08
)


def generate_private_key(public_exponent, key_size, backend):
if not isinstance(backend, RSABackend):
Expand Down
5 changes: 5 additions & 0 deletions src/cryptography/hazmat/primitives/serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,11 @@ class PrivateFormat(Enum):
TraditionalOpenSSL = "TraditionalOpenSSL"


class PublicFormat(Enum):
SubjectPublicKeyInfo = "X.509 subjectPublicKeyInfo with PKCS#1"
PKCS1 = "Raw PKCS#1"


@six.add_metaclass(abc.ABCMeta)
class KeySerializationEncryption(object):
pass
Expand Down
10 changes: 9 additions & 1 deletion tests/hazmat/backends/test_openssl.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,11 +508,19 @@ def test_password_length_limit(self):
serialization.BestAvailableEncryption(password)
)

def test_unsupported_key_encoding(self):
def test_unsupported_private_key_encoding(self):
key = RSA_KEY_2048.private_key(backend)
with pytest.raises(ValueError):
key.private_bytes(
serialization.Encoding.DER,
serialization.PrivateFormat.PKCS8,
serialization.NoEncryption()
)

def test_unsupported_public_key_encoding(self):
key = RSA_KEY_2048.private_key(backend).public_key()
with pytest.raises(ValueError):
key.public_bytes(
serialization.Encoding.DER,
serialization.PublicFormat.SubjectPublicKeyInfo
)
49 changes: 47 additions & 2 deletions tests/hazmat/primitives/test_rsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,10 @@ def test_modular_inverse():


def _skip_if_no_serialization(key, backend):
if not isinstance(key, rsa.RSAPrivateKeyWithSerialization):
if not isinstance(
key,
(rsa.RSAPrivateKeyWithSerialization, rsa.RSAPublicKeyWithSerialization)
):
pytest.skip(
"{0} does not support RSA key serialization".format(backend)
)
Expand Down Expand Up @@ -1748,7 +1751,7 @@ def test_invalid_recover_prime_factors(self):

@pytest.mark.requires_backend_interface(interface=RSABackend)
@pytest.mark.requires_backend_interface(interface=PEMSerializationBackend)
class TestRSAPEMWriter(object):
class TestRSAPEMPrivateKeySerialization(object):
@pytest.mark.parametrize(
("fmt", "password"),
itertools.product(
Expand Down Expand Up @@ -1857,3 +1860,45 @@ def test_private_bytes_unsupported_encryption_type(self, backend):
serialization.PrivateFormat.TraditionalOpenSSL,
DummyKeyEncryption()
)


@pytest.mark.requires_backend_interface(interface=RSABackend)
@pytest.mark.requires_backend_interface(interface=PEMSerializationBackend)
class TestRSAPEMPublicKeySerialization(object):
def test_public_bytes_unencrypted_pem(self, backend):
key_bytes = load_vectors_from_file(
os.path.join("asymmetric", "PKCS8", "unenc-rsa-pkcs8.pub.pem"),
lambda pemfile: pemfile.read().encode()
)
key = serialization.load_pem_public_key(key_bytes, backend)
_skip_if_no_serialization(key, backend)
serialized = key.public_bytes(
serialization.Encoding.PEM,
serialization.PublicFormat.SubjectPublicKeyInfo,
)
assert serialized == key_bytes

def test_public_bytes_pkcs1_unencrypted_pem(self, backend):
key_bytes = load_vectors_from_file(
os.path.join("asymmetric", "public", "PKCS1", "rsa.pub.pem"),
lambda pemfile: pemfile.read().encode()
)
key = serialization.load_pem_public_key(key_bytes, backend)
_skip_if_no_serialization(key, backend)
serialized = key.public_bytes(
serialization.Encoding.PEM,
serialization.PublicFormat.PKCS1,
)
assert serialized == key_bytes

def test_public_bytes_invalid_encoding(self, backend):
key = RSA_KEY_2048.private_key(backend).public_key()
_skip_if_no_serialization(key, backend)
with pytest.raises(TypeError):
key.public_bytes("notencoding", serialization.PublicFormat.PKCS1)

def test_public_bytes_invalid_format(self, backend):
key = RSA_KEY_2048.private_key(backend).public_key()
_skip_if_no_serialization(key, backend)
with pytest.raises(TypeError):
key.public_bytes(serialization.Encoding.PEM, "invalidformat")

0 comments on commit 3f157e0

Please sign in to comment.