Skip to content

Commit

Permalink
Merge pull request #1503 from reaperhulk/serialize-some-keys
Browse files Browse the repository at this point in the history
Support for traditional OpenSSL and PKCS8 RSA private key serialization
  • Loading branch information
public committed Mar 1, 2015
2 parents e8f12f8 + 223a8f0 commit 6bb9624
Show file tree
Hide file tree
Showing 11 changed files with 436 additions and 12 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ Changelog
support loading DER encoded public keys.
* Fixed building against LibreSSL, a compile-time substitute for OpenSSL.
* FreeBSD 9.2 was removed from the continuous integration system.
* Added
:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization`
and deprecated
:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithNumbers`.
* Added
:meth:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization.private_bytes`
to
:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization`.

0.7.2 - 2015-01-16
~~~~~~~~~~~~~~~~~~
Expand Down
76 changes: 76 additions & 0 deletions docs/hazmat/primitives/asymmetric/rsa.rst
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,39 @@ password. If the key is encrypted we can pass a ``bytes`` object as the
There is also support for :func:`loading public keys in the SSH format
<cryptography.hazmat.primitives.serialization.load_ssh_public_key>`.

Key serialization
~~~~~~~~~~~~~~~~~

If you have a 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`
to serialize the key.

.. doctest::

>>> from cryptography.hazmat.primitives import serialization
>>> pem = private_key.private_bytes(
... encoding=serialization.Encoding.PEM,
... format=serialization.Format.PKCS8,
... encryption_algorithm=serialization.BestAvailableEncryption(b'mypassword')
... )
>>> pem.splitlines()[0]
'-----BEGIN ENCRYPTED PRIVATE KEY-----'

It is also possible to serialize without encryption using
:class:`~cryptography.hazmat.primitives.serialization.NoEncryption`.

.. doctest::

>>> pem = private_key.private_bytes(
... encoding=serialization.Encoding.PEM,
... format=serialization.Format.TraditionalOpenSSL,
... encryption_algorithm=serialization.NoEncryption()
... )
>>> pem.splitlines()[0]
'-----BEGIN RSA PRIVATE KEY-----'

Signing
~~~~~~~

Expand Down Expand Up @@ -485,6 +518,49 @@ Key interfaces
instance.


.. class:: RSAPrivateKeyWithSerialization

.. versionadded:: 0.8

Extends :class:`RSAPrivateKey`.

.. method:: private_numbers()

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

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

.. method:: private_bytes(encoding, format, encryption_algorithm)

Allows serialization of the key to bytes. Encoding (
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.PEM` or
:attr:`~cryptography.hazmat.primitives.serialization.Encoding.DER`),
format (
:attr:`~cryptography.hazmat.primitives.serialization.Format.TraditionalOpenSSL`
or
:attr:`~cryptography.hazmat.primitives.serialization.Format.PKCS8`) and
encryption algorithm (such as
:class:`~cryptography.hazmat.primitives.serialization.BestAvailableEncryption`
or :class:`~cryptography.hazmat.primitives.serialization.NoEncryption`)
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.Format` enum.

:param encryption_algorithm: An instance of an object conforming to the
:class:`~cryptography.hazmat.primitives.serialization.KeySerializationEncryption`
interface.

:return bytes: Serialized key.


.. class:: RSAPublicKey

.. versionadded:: 0.2
Expand Down
66 changes: 65 additions & 1 deletion docs/hazmat/primitives/asymmetric/serialization.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Key Serialization
=================

.. currentmodule:: cryptography.hazmat.primitives.serialization
.. module:: cryptography.hazmat.primitives.serialization

.. testsetup::

Expand Down Expand Up @@ -282,3 +282,67 @@ DSA keys look almost identical but begin with ``ssh-dss`` rather than

:raises cryptography.exceptions.UnsupportedAlgorithm: If the serialized
key is of a type that is not supported.

Serialization Formats
~~~~~~~~~~~~~~~~~~~~~

.. class:: Format

.. versionadded:: 0.8

An enumeration for key formats. Used with
:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization.private_bytes`.

.. attribute:: TraditionalOpenSSL

Frequently known as PKCS#1 format. Still a widely used format, but
generally considered legacy.

.. attribute:: PKCS8

A more modern format for serializing keys which allows for better
encryption. Choose this unless you have explicit legacy compatibility
requirements.

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

.. class:: Encoding

.. versionadded:: 0.8

An enumeration for encoding types. Used with
:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization.private_bytes`.

.. attribute:: PEM

For PEM format. This is a base64 format with delimiters.

.. attribute:: DER

For DER format. This is a binary format.


Serialization Encryption Types
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. class:: KeySerializationEncryption

Objects with this interface are usable as encryption types with methods
like
:meth:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization.private_bytes`.
All other classes in this section represent the available choices for
encryption and have this interface. They are used with
:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKeyWithSerialization.private_bytes`.

.. class:: BestAvailableEncryption(password)

Encrypt using the best available encryption for a given key's backend.
This is a curated encryption choice and the algorithm may change over
time.

:param bytes password: The password to use for encryption.

.. class:: NoEncryption

Do not encrypt.
2 changes: 2 additions & 0 deletions docs/spelling_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ pseudorandom
pyOpenSSL
Schneier
scrypt
Serializers
serializer
Solaris
Tanja
testability
Expand Down
66 changes: 64 additions & 2 deletions src/cryptography/hazmat/backends/openssl/rsa.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,13 @@
from cryptography.hazmat.primitives.asymmetric.padding import (
AsymmetricPadding, MGF1, OAEP, PKCS1v15, PSS
)
from cryptography.hazmat.primitives.interfaces import (
RSAPrivateKeyWithNumbers, RSAPublicKeyWithNumbers
from cryptography.hazmat.primitives.asymmetric.rsa import (
RSAPrivateKeyWithNumbers, RSAPrivateKeyWithSerialization,
RSAPublicKeyWithNumbers
)
from cryptography.hazmat.primitives.serialization import (
BestAvailableEncryption, Encoding, Format, KeySerializationEncryption,
NoEncryption
)


Expand Down Expand Up @@ -507,6 +512,7 @@ def _verify_pss(self, evp_md):


@utils.register_interface(RSAPrivateKeyWithNumbers)
@utils.register_interface(RSAPrivateKeyWithSerialization)
class _RSAPrivateKey(object):
def __init__(self, backend, rsa_cdata):
self._backend = backend
Expand Down Expand Up @@ -559,6 +565,62 @@ def private_numbers(self):
)
)

def private_bytes(self, encoding, format, encryption_algorithm):
if not isinstance(encoding, Encoding):
raise TypeError("encoding must be an item from the Encoding enum")

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

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

if format is Format.PKCS8:
write_bio = self._backend._lib.PEM_write_bio_PKCS8PrivateKey
key = self._evp_pkey
elif format is Format.TraditionalOpenSSL:
write_bio = self._backend._lib.PEM_write_bio_RSAPrivateKey
key = self._rsa_cdata

if not isinstance(encryption_algorithm, KeySerializationEncryption):
raise TypeError(
"Encryption algorithm must be a KeySerializationEncryption "
"instance"
)

if isinstance(encryption_algorithm, NoEncryption):
password = b""
passlen = 0
evp_cipher = self._backend._ffi.NULL
elif isinstance(encryption_algorithm, BestAvailableEncryption):
# This is a curated value that we will update over time.
evp_cipher = self._backend._lib.EVP_get_cipherbyname(
b"aes-256-cbc"
)
password = encryption_algorithm.password
passlen = len(password)
if passlen > 1023:
raise ValueError(
"Passwords longer than 1023 bytes are not supported by "
"this backend"
)
else:
raise ValueError("Unsupported encryption type")

bio = self._backend._create_mem_bio()
res = write_bio(
bio,
key,
evp_cipher,
password,
passlen,
self._backend._ffi.NULL,
self._backend._ffi.NULL
)
assert res == 1
return self._backend._read_mem_bio(bio)


@utils.register_interface(RSAPublicKeyWithNumbers)
class _RSAPublicKey(object):
Expand Down
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 @@ -42,13 +42,30 @@ def public_key(self):


@six.add_metaclass(abc.ABCMeta)
class RSAPrivateKeyWithNumbers(RSAPrivateKey):
class RSAPrivateKeyWithSerialization(RSAPrivateKey):
@abc.abstractmethod
def private_numbers(self):
"""
Returns an RSAPrivateNumbers.
"""

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


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


@six.add_metaclass(abc.ABCMeta)
class RSAPublicKey(object):
Expand Down
5 changes: 3 additions & 2 deletions src/cryptography/hazmat/primitives/interfaces/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -289,11 +289,12 @@
)

RSAPrivateKeyWithNumbers = utils.deprecated(
rsa.RSAPrivateKeyWithNumbers,
rsa.RSAPrivateKeyWithSerialization,
__name__,
(
"The RSAPrivateKeyWithNumbers interface has moved to the "
"cryptography.hazmat.primitives.asymmetric.rsa module"
"cryptography.hazmat.primitives.asymmetric.rsa module and has been "
"renamed RSAPrivateKeyWithSerialization"
),
utils.DeprecatedIn08
)
Expand Down
32 changes: 32 additions & 0 deletions src/cryptography/hazmat/primitives/serialization.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@

from __future__ import absolute_import, division, print_function

import abc
import base64
import struct
from enum import Enum

import six

from cryptography import utils
from cryptography.exceptions import UnsupportedAlgorithm
from cryptography.hazmat.primitives.asymmetric import dsa, ec, rsa

Expand Down Expand Up @@ -164,3 +167,32 @@ def _int_from_bytes(data, byteorder, signed=False):
data = data[4:]

return result


class Encoding(Enum):
PEM = "PEM"
DER = "DER"


class Format(Enum):
PKCS8 = "PKCS8"
TraditionalOpenSSL = "TraditionalOpenSSL"


@six.add_metaclass(abc.ABCMeta)
class KeySerializationEncryption(object):
pass


@utils.register_interface(KeySerializationEncryption)
class BestAvailableEncryption(object):
def __init__(self, password):
if not isinstance(password, bytes) or len(password) == 0:
raise ValueError("Password must be 1 or more bytes.")

self.password = password


@utils.register_interface(KeySerializationEncryption)
class NoEncryption(object):
pass

0 comments on commit 6bb9624

Please sign in to comment.