Skip to content

Commit

Permalink
Merge pull request #382 from Lukasa/dump_pubkey
Browse files Browse the repository at this point in the history
Support for serializing/deserializing public keys
  • Loading branch information
hynek committed Oct 28, 2015
2 parents 3fffb74 + 666b8c1 commit 3ca2eee
Show file tree
Hide file tree
Showing 4 changed files with 130 additions and 1 deletion.
4 changes: 3 additions & 1 deletion CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ We have made *many* changes to make local development more pleasing.
The test suite now passes both on Linux and OS X with OpenSSL 0.9.8, 1.0.1, and 1.0.2.
It has been moved to `py.test <http://pytest.org/latest/>`_, all CI test runs are part of `tox <https://testrun.org/tox/>`_ and the source code has been made fully `flake8 <https://flake8.readthedocs.org/en/>`_ compliant.

We hope to have lowered the barrier for contributions significantly but are open to hear about any remaining frustrations.
We hope to have lowered the barrier for contributions significantly but are open to hear about any remaining frustrations.


Backward-incompatible changes:
Expand All @@ -40,6 +40,8 @@ In accordance with our backward compatibility policy :func:`OpenSSL.rand.egd` wi
Changes:
^^^^^^^^

- Added :func:`OpenSSL.crypto.dump_publickey` to dump :class:`OpenSSL.crypto.PKey` objects that represent public keys, and :func:`OpenSSL.crypto.load_publickey` to load such objects from serialized representations.
[`#382 <https://github.com/pyca/pyopenssl/pull/382>`_]
- Added :func:`OpenSSL.crypto.dump_crl` to dump a certificate revocation list out to a string buffer.
[`#368 <https://github.com/pyca/pyopenssl/pull/368>`_]
- Added :meth:`OpenSSL.SSL.Connection.state_string` using the OpenSSL binding ``state_string_long``.
Expand Down
7 changes: 7 additions & 0 deletions doc/api/crypto.rst
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,13 @@ Private keys
*passphrase* must be either a string or a callback for providing the pass
phrase.

Public keys
~~~~~~~~~~~

.. autofunction:: dump_publickey

.. autofunction:: load_publickey

Certificate revocation lists
~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
57 changes: 57 additions & 0 deletions src/OpenSSL/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -1623,6 +1623,31 @@ def dump_certificate(type, cert):
return _bio_to_string(bio)


def dump_publickey(type, pkey):
"""
Dump a public key to a buffer.
:param type: The file type (one of :data:`FILETYPE_PEM` or
:data:`FILETYPE_ASN1`).
:param PKey pkey: The public key to dump
:return: The buffer with the dumped key in it.
:rtype: bytes
"""
bio = _new_mem_buf()
if type == FILETYPE_PEM:
write_bio = _lib.PEM_write_bio_PUBKEY
elif type == FILETYPE_ASN1:
write_bio = _lib.i2d_PUBKEY_bio
else:
raise ValueError("type argument must be FILETYPE_PEM or FILETYPE_ASN1")

result_code = write_bio(bio, pkey._pkey)
if result_code != 1: # pragma: no cover
_raise_current_error()

return _bio_to_string(bio)


def dump_privatekey(type, pkey, cipher=None, passphrase=None):
"""
Dump a private key to a buffer
Expand Down Expand Up @@ -2404,6 +2429,38 @@ def _read_passphrase(self, buf, size, rwflag, userdata):
return 0


def load_publickey(type, buffer):
"""
Load a public key from a buffer.
:param type: The file type (one of :data:`FILETYPE_PEM`,
:data:`FILETYPE_ASN1`).
:param buffer: The buffer the key is stored in.
:type buffer: A Python string object, either unicode or bytestring.
:return: The PKey object.
:rtype: :class:`PKey`
"""
if isinstance(buffer, _text_type):
buffer = buffer.encode("ascii")

bio = _new_mem_buf(buffer)

if type == FILETYPE_PEM:
evp_pkey = _lib.PEM_read_bio_PUBKEY(
bio, _ffi.NULL, _ffi.NULL, _ffi.NULL)
elif type == FILETYPE_ASN1:
evp_pkey = _lib.d2i_PUBKEY_bio(bio, _ffi.NULL)
else:
raise ValueError("type argument must be FILETYPE_PEM or FILETYPE_ASN1")

if evp_pkey == _ffi.NULL:
_raise_current_error()

pkey = PKey.__new__(PKey)
pkey._pkey = _ffi.gc(evp_pkey, _lib.EVP_PKEY_free)
return pkey


def load_privatekey(type, buffer, passphrase=None):
"""
Load a private key from a buffer
Expand Down
63 changes: 63 additions & 0 deletions tests/test_crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from OpenSSL.crypto import X509Req, X509ReqType
from OpenSSL.crypto import X509Extension, X509ExtensionType
from OpenSSL.crypto import load_certificate, load_privatekey
from OpenSSL.crypto import load_publickey, dump_publickey
from OpenSSL.crypto import FILETYPE_PEM, FILETYPE_ASN1, FILETYPE_TEXT
from OpenSSL.crypto import dump_certificate, load_certificate_request
from OpenSSL.crypto import dump_certificate_request, dump_privatekey
Expand Down Expand Up @@ -300,6 +301,18 @@ def normalize_privatekey_pem(pem):

encryptedPrivateKeyPEMPassphrase = b("foobar")


cleartextPublicKeyPEM = b("""-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxszlc+b71LvlLS0ypt/l
gT/JzSVJtnEqw9WUNGeiChywX2mmQLHEt7KP0JikqUFZOtPclNY823Q4pErMTSWC
90qlUxI47vNJbXGRfmO2q6Zfw6SE+E9iUb74xezbOJLjBuUIkQzEKEFV+8taiRV+
ceg1v01yCT2+OjhQW3cxG42zxyRFmqesbQAUWgS3uhPrUQqYQUEiTmVhh4FBUKZ5
XIneGUpX1S7mXRxTLH6YzRoGFqRoc9A0BBNcoXHTWnxV215k4TeHMFYE5RG0KYAS
8Xk5iKICEXwnZreIt3jyygqoOKsKZMK/Zl2VhMGhJR6HXRpQCyASzEG7bgtROLhL
ywIDAQAB
-----END PUBLIC KEY-----
""")

# Some PKCS#7 stuff. Generated with the openssl command line:
#
# openssl crl2pkcs7 -inform pem -outform pem -certfile s.pem -nocrl
Expand Down Expand Up @@ -2562,6 +2575,56 @@ def test_dump_privatekey_text(self):
good_text = _runopenssl(dumped_pem, b"rsa", b"-noout", b"-text")
self.assertEqual(dumped_text, good_text)

def test_dump_publickey_pem(self):
"""
dump_publickey writes a PEM.
"""
key = load_publickey(FILETYPE_PEM, cleartextPublicKeyPEM)
dumped_pem = dump_publickey(FILETYPE_PEM, key)
assert dumped_pem == cleartextPublicKeyPEM

def test_dump_publickey_asn1(self):
"""
dump_publickey writes a DER.
"""
key = load_publickey(FILETYPE_PEM, cleartextPublicKeyPEM)
dumped_der = dump_publickey(FILETYPE_ASN1, key)
key2 = load_publickey(FILETYPE_ASN1, dumped_der)
dumped_pem2 = dump_publickey(FILETYPE_PEM, key2)
assert dumped_pem2 == cleartextPublicKeyPEM

def test_dump_publickey_invalid_type(self):
"""
dump_publickey doesn't support FILETYPE_TEXT.
"""
key = load_publickey(FILETYPE_PEM, cleartextPublicKeyPEM)

with pytest.raises(ValueError):
dump_publickey(FILETYPE_TEXT, key)

def test_load_publickey_invalid_type(self):
"""
load_publickey doesn't support FILETYPE_TEXT.
"""
with pytest.raises(ValueError):
load_publickey(FILETYPE_TEXT, cleartextPublicKeyPEM)

def test_load_publickey_invalid_key_format(self):
"""
load_publickey explodes on incorrect keys.
"""
with pytest.raises(Error):
load_publickey(FILETYPE_ASN1, cleartextPublicKeyPEM)

def test_load_publickey_tolerates_unicode_strings(self):
"""
load_publickey works with text strings, not just bytes.
"""
serialized = cleartextPublicKeyPEM.decode('ascii')
key = load_publickey(FILETYPE_PEM, serialized)
dumped_pem = dump_publickey(FILETYPE_PEM, key)
assert dumped_pem == cleartextPublicKeyPEM

def test_dump_certificate_request(self):
"""
:py:obj:`dump_certificate_request` writes a PEM, DER, and text.
Expand Down

0 comments on commit 3ca2eee

Please sign in to comment.