Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for serializing/deserializing public keys #382

Merged
merged 13 commits into from
Oct 28, 2015
6 changes: 5 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 Down Expand Up @@ -54,6 +54,10 @@ Changes:
This will default us to the setting that actually works.
To revert this you can call ``OpenSSL.crypto._lib.ASN1_STRING_set_default_mask_asc(b"default")``.
[`#234 <https://github.com/pyca/pyopenssl/pull/234>`_]
- Added :func:`OpenSSL.crypto.dump_publickey` to dump :class:`OpenSSL.crypto.PKey` objects that represent public keys.
[`#382 <https://github.com/pyca/pyopenssl/pull/382>`_]
- Added :func:`OpenSSL.crypto.load_publickey` to load :class:`OpenSSL.crypto.PKey` objects that represent public keys from serialized representations.
[`#382 <https://github.com/pyca/pyopenssl/pull/382>`_]



Expand Down
58 changes: 58 additions & 0 deletions src/OpenSSL/crypto.py
Original file line number Diff line number Diff line change
Expand Up @@ -1623,6 +1623,32 @@ 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: The PKey to dump.
:type pkey: :class:`PKey`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about :param PKey pkey: The public key to dump?

:class: is definitely not necessary if you write type markup (and it's only the one type).

: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 +2430,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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add an explanation what type buffer can be.

: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