diff options
-rw-r--r-- | CHANGELOG.rst | 2 | ||||
-rw-r--r-- | cryptography/hazmat/backends/openssl/backend.py | 136 | ||||
-rw-r--r-- | docs/hazmat/primitives/asymmetric/rsa.rst | 67 | ||||
-rw-r--r-- | tests/hazmat/backends/test_openssl.py | 11 | ||||
-rw-r--r-- | tests/hazmat/primitives/test_rsa.py | 197 |
5 files changed, 399 insertions, 14 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index abbea9fa..ef2f7c1e 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -9,6 +9,8 @@ Changelog * Added :class:`~cryptography.hazmat.primitives.twofactor.hotp.HOTP`. * Added :class:`~cryptography.hazmat.primitives.twofactor.totp.TOTP`. * Added :class:`~cryptography.hazmat.primitives.ciphers.algorithms.IDEA` support. +* Added signature support to :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey` + and verification support to :class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`. 0.2.2 - 2014-03-03 ~~~~~~~~~~~~~~~~~~ diff --git a/cryptography/hazmat/backends/openssl/backend.py b/cryptography/hazmat/backends/openssl/backend.py index fa21260c..d2744cf3 100644 --- a/cryptography/hazmat/backends/openssl/backend.py +++ b/cryptography/hazmat/backends/openssl/backend.py @@ -15,6 +15,7 @@ from __future__ import absolute_import, division, print_function import collections import itertools +import math import six @@ -696,21 +697,57 @@ class _HMACContext(object): return self._backend._ffi.buffer(buf)[:outlen[0]] +def _get_rsa_pss_salt_length(mgf, key_size, digest_size): + if mgf._salt_length is MGF1.MAX_LENGTH: + # bit length - 1 per RFC 3447 + emlen = int(math.ceil((key_size - 1) / 8.0)) + salt_length = emlen - digest_size - 2 + assert salt_length >= 0 + return salt_length + else: + return mgf._salt_length + + @utils.register_interface(interfaces.AsymmetricSignatureContext) class _RSASignatureContext(object): def __init__(self, backend, private_key, padding, algorithm): self._backend = backend self._private_key = private_key + if not isinstance(padding, interfaces.AsymmetricPadding): raise TypeError( "Expected provider of interfaces.AsymmetricPadding") - if padding.name == "EMSA-PKCS1-v1_5": + if isinstance(padding, PKCS1v15): if self._backend._lib.Cryptography_HAS_PKEY_CTX: self._finalize_method = self._finalize_pkey_ctx self._padding_enum = self._backend._lib.RSA_PKCS1_PADDING else: self._finalize_method = self._finalize_pkcs1 + elif isinstance(padding, PSS): + if not isinstance(padding._mgf, MGF1): + raise UnsupportedAlgorithm( + "Only MGF1 is supported by this backend" + ) + + # Size of key in bytes - 2 is the maximum + # PSS signature length (salt length is checked later) + key_size_bytes = int(math.ceil(private_key.key_size / 8.0)) + if key_size_bytes - algorithm.digest_size - 2 < 0: + raise ValueError("Digest too large for key size. Use a larger " + "key.") + + if not self._backend.mgf1_hash_supported(padding._mgf._algorithm): + raise UnsupportedHash( + "When OpenSSL is older than 1.0.1 then only SHA1 is " + "supported with MGF1." + ) + + if self._backend._lib.Cryptography_HAS_PKEY_CTX: + self._finalize_method = self._finalize_pkey_ctx + self._padding_enum = self._backend._lib.RSA_PKCS1_PSS_PADDING + else: + self._finalize_method = self._finalize_pss else: raise UnsupportedPadding( "{0} is not supported by this backend".format(padding.name) @@ -755,6 +792,26 @@ class _RSASignatureContext(object): res = self._backend._lib.EVP_PKEY_CTX_set_rsa_padding( pkey_ctx, self._padding_enum) assert res > 0 + if isinstance(self._padding, PSS): + res = self._backend._lib.EVP_PKEY_CTX_set_rsa_pss_saltlen( + pkey_ctx, + _get_rsa_pss_salt_length( + self._padding._mgf, + self._private_key.key_size, + self._hash_ctx.algorithm.digest_size + ) + ) + assert res > 0 + + if self._backend._lib.Cryptography_HAS_MGF1_MD: + # MGF1 MD is configurable in OpenSSL 1.0.1+ + mgf1_md = self._backend._lib.EVP_get_digestbyname( + self._padding._mgf._algorithm.name.encode("ascii")) + assert mgf1_md != self._backend._ffi.NULL + res = self._backend._lib.EVP_PKEY_CTX_set_rsa_mgf1_md( + pkey_ctx, mgf1_md + ) + assert res > 0 data_to_sign = self._hash_ctx.finalize() self._hash_ctx = None buflen = self._backend._ffi.new("size_t *") @@ -769,7 +826,14 @@ class _RSASignatureContext(object): buf = self._backend._ffi.new("unsigned char[]", buflen[0]) res = self._backend._lib.EVP_PKEY_sign( pkey_ctx, buf, buflen, data_to_sign, len(data_to_sign)) - assert res == 1 + if res != 1: + errors = self._backend._consume_errors() + assert errors[0].lib == self._backend._lib.ERR_LIB_RSA + assert (errors[0].reason == + self._backend._lib.RSA_R_DATA_TOO_LARGE_FOR_KEY_SIZE) + raise ValueError("Salt length too long for key size. Try using " + "MAX_LENGTH instead.") + return self._backend._ffi.buffer(buf)[:] def _finalize_pkcs1(self, evp_pkey, pkey_size, evp_md): @@ -786,6 +850,44 @@ class _RSASignatureContext(object): assert res == 1 return self._backend._ffi.buffer(sig_buf)[:sig_len[0]] + def _finalize_pss(self, evp_pkey, pkey_size, evp_md): + data_to_sign = self._hash_ctx.finalize() + self._hash_ctx = None + padded = self._backend._ffi.new("unsigned char[]", pkey_size) + rsa_cdata = self._backend._lib.EVP_PKEY_get1_RSA(evp_pkey) + assert rsa_cdata != self._backend._ffi.NULL + rsa_cdata = self._backend._ffi.gc(rsa_cdata, + self._backend._lib.RSA_free) + res = self._backend._lib.RSA_padding_add_PKCS1_PSS( + rsa_cdata, + padded, + data_to_sign, + evp_md, + _get_rsa_pss_salt_length( + self._padding._mgf, + self._private_key.key_size, + len(data_to_sign) + ) + ) + if res != 1: + errors = self._backend._consume_errors() + assert errors[0].lib == self._backend._lib.ERR_LIB_RSA + assert (errors[0].reason == + self._backend._lib.RSA_R_DATA_TOO_LARGE_FOR_KEY_SIZE) + raise ValueError("Salt length too long for key size. Try using " + "MAX_LENGTH instead.") + + sig_buf = self._backend._ffi.new("char[]", pkey_size) + sig_len = self._backend._lib.RSA_private_encrypt( + pkey_size, + padded, + sig_buf, + rsa_cdata, + self._backend._lib.RSA_NO_PADDING + ) + assert sig_len != -1 + return self._backend._ffi.buffer(sig_buf)[:sig_len] + @utils.register_interface(interfaces.AsymmetricVerificationContext) class _RSAVerificationContext(object): @@ -793,6 +895,7 @@ class _RSAVerificationContext(object): self._backend = backend self._public_key = public_key self._signature = signature + if not isinstance(padding, interfaces.AsymmetricPadding): raise TypeError( "Expected provider of interfaces.AsymmetricPadding") @@ -809,6 +912,15 @@ class _RSAVerificationContext(object): "Only MGF1 is supported by this backend" ) + # Size of key in bytes - 2 is the maximum + # PSS signature length (salt length is checked later) + key_size_bytes = int(math.ceil(public_key.key_size / 8.0)) + if key_size_bytes - algorithm.digest_size - 2 < 0: + raise ValueError( + "Digest too large for key size. Check that you have the " + "correct key and digest algorithm." + ) + if not self._backend.mgf1_hash_supported(padding._mgf._algorithm): raise UnsupportedHash( "When OpenSSL is older than 1.0.1 then only SHA1 is " @@ -862,7 +974,13 @@ class _RSAVerificationContext(object): assert res > 0 if isinstance(self._padding, PSS): res = self._backend._lib.EVP_PKEY_CTX_set_rsa_pss_saltlen( - pkey_ctx, self._get_salt_length()) + pkey_ctx, + _get_rsa_pss_salt_length( + self._padding._mgf, + self._public_key.key_size, + self._hash_ctx.algorithm.digest_size + ) + ) assert res > 0 if self._backend._lib.Cryptography_HAS_MGF1_MD: # MGF1 MD is configurable in OpenSSL 1.0.1+ @@ -937,18 +1055,16 @@ class _RSAVerificationContext(object): data_to_verify, evp_md, buf, - self._get_salt_length() + _get_rsa_pss_salt_length( + self._padding._mgf, + self._public_key.key_size, + len(data_to_verify) + ) ) if res != 1: errors = self._backend._consume_errors() assert errors raise InvalidSignature - def _get_salt_length(self): - if self._padding._mgf._salt_length is MGF1.MAX_LENGTH: - return -2 - else: - return self._padding._mgf._salt_length - backend = Backend() diff --git a/docs/hazmat/primitives/asymmetric/rsa.rst b/docs/hazmat/primitives/asymmetric/rsa.rst index 03a7caed..57c8eec2 100644 --- a/docs/hazmat/primitives/asymmetric/rsa.rst +++ b/docs/hazmat/primitives/asymmetric/rsa.rst @@ -72,7 +72,12 @@ RSA ... backend=default_backend() ... ) >>> signer = private_key.signer( - ... padding.PKCS1v15(), + ... padding.PSS( + ... mgf=padding.MGF1( + ... algorithm=hashes.SHA256(), + ... salt_length=padding.MGF1.MAX_LENGTH + ... ) + ... ), ... hashes.SHA256(), ... default_backend() ... ) @@ -99,6 +104,25 @@ RSA the provided ``backend`` does not implement :class:`~cryptography.hazmat.backends.interfaces.RSABackend` + :raises TypeError: This is raised when the padding is not an + :class:`~cryptography.hazmat.primitives.interfaces.AsymmetricPadding` + provider. + + :raises ValueError: This is raised when the chosen hash algorithm is + too large for the key size. + + :raises UnsupportedHash: This is raised when the backend does not + support the chosen hash algorithm. If the padding is + :class:`~cryptography.hazmat.primitives.asymmetric.padding.PSS` + with the + :class:`~cryptography.hazmat.primitives.asymmetric.padding.MGF1` + mask generation function it may also refer to the `MGF1` hash + algorithm. + + :raises UnsupportedPadding: This is raised when the backend does not + support the chosen padding. + + .. class:: RSAPublicKey(public_exponent, modulus) .. versionadded:: 0.2 @@ -136,12 +160,31 @@ RSA ... key_size=2048, ... backend=default_backend() ... ) - >>> signer = private_key.signer(padding.PKCS1v15(), hashes.SHA256(), default_backend()) + >>> signer = private_key.signer( + ... padding.PSS( + ... mgf=padding.MGF1( + ... algorithm=hashes.SHA256(), + ... salt_length=padding.MGF1.MAX_LENGTH + ... ) + ... ), + ... hashes.SHA256(), + ... default_backend() + ... ) >>> data= b"this is some data I'd like to sign" >>> signer.update(data) >>> signature = signer.finalize() >>> public_key = private_key.public_key() - >>> verifier = public_key.verifier(signature, padding.PKCS1v15(), hashes.SHA256(), default_backend()) + >>> verifier = public_key.verifier( + ... signature, + ... padding.PSS( + ... mgf=padding.MGF1( + ... algorithm=hashes.SHA256(), + ... salt_length=padding.MGF1.MAX_LENGTH + ... ) + ... ), + ... hashes.SHA256(), + ... default_backend() + ... ) >>> verifier.update(data) >>> verifier.verify() @@ -166,6 +209,24 @@ RSA the provided ``backend`` does not implement :class:`~cryptography.hazmat.backends.interfaces.RSABackend` + :raises TypeError: This is raised when the padding is not an + :class:`~cryptography.hazmat.primitives.interfaces.AsymmetricPadding` + provider. + + :raises ValueError: This is raised when the chosen hash algorithm is + too large for the key size. + + :raises UnsupportedHash: This is raised when the backend does not + support the chosen hash algorithm. If the padding is + :class:`~cryptography.hazmat.primitives.asymmetric.padding.PSS` + with the + :class:`~cryptography.hazmat.primitives.asymmetric.padding.MGF1` + mask generation function it may also refer to the `MGF1` hash + algorithm. + + :raises UnsupportedPadding: This is raised when the backend does not + support the chosen padding. + .. _`RSA`: https://en.wikipedia.org/wiki/RSA_(cryptosystem) .. _`public-key`: https://en.wikipedia.org/wiki/Public-key_cryptography .. _`use 65537`: http://www.daemonology.net/blog/2009-06-11-cryptographic-right-answers.html diff --git a/tests/hazmat/backends/test_openssl.py b/tests/hazmat/backends/test_openssl.py index 5c6efbaf..3747f436 100644 --- a/tests/hazmat/backends/test_openssl.py +++ b/tests/hazmat/backends/test_openssl.py @@ -153,6 +153,17 @@ class TestOpenSSL(object): key_size=512, backend=backend ) + with pytest.raises(UnsupportedHash): + private_key.signer( + padding.PSS( + mgf=padding.MGF1( + algorithm=hashes.SHA256(), + salt_length=padding.MGF1.MAX_LENGTH + ) + ), + hashes.SHA1(), + backend + ) public_key = private_key.public_key() with pytest.raises(UnsupportedHash): public_key.verifier( diff --git a/tests/hazmat/primitives/test_rsa.py b/tests/hazmat/primitives/test_rsa.py index 67189c24..eb7e1e60 100644 --- a/tests/hazmat/primitives/test_rsa.py +++ b/tests/hazmat/primitives/test_rsa.py @@ -16,6 +16,7 @@ from __future__ import absolute_import, division, print_function import binascii import itertools +import math import os import pytest @@ -428,6 +429,162 @@ class TestRSASignature(object): signature = signer.finalize() assert binascii.hexlify(signature) == example["signature"] + @pytest.mark.parametrize( + "pkcs1_example", + _flatten_pkcs1_examples(load_vectors_from_file( + os.path.join( + "asymmetric", "RSA", "pkcs-1v2-1d2-vec", "pss-vect.txt"), + load_pkcs1_vectors + )) + ) + def test_pss_signing(self, pkcs1_example, backend): + private, public, example = pkcs1_example + private_key = rsa.RSAPrivateKey( + p=private["p"], + q=private["q"], + private_exponent=private["private_exponent"], + dmp1=private["dmp1"], + dmq1=private["dmq1"], + iqmp=private["iqmp"], + public_exponent=private["public_exponent"], + modulus=private["modulus"] + ) + public_key = rsa.RSAPublicKey( + public_exponent=public["public_exponent"], + modulus=public["modulus"] + ) + signer = private_key.signer( + padding.PSS( + mgf=padding.MGF1( + algorithm=hashes.SHA1(), + salt_length=padding.MGF1.MAX_LENGTH + ) + ), + hashes.SHA1(), + backend + ) + signer.update(binascii.unhexlify(example["message"])) + signature = signer.finalize() + assert len(signature) == math.ceil(private_key.key_size / 8.0) + # PSS signatures contain randomness so we can't do an exact + # signature check. Instead we'll verify that the signature created + # successfully verifies. + verifier = public_key.verifier( + signature, + padding.PSS( + mgf=padding.MGF1( + algorithm=hashes.SHA1(), + salt_length=padding.MGF1.MAX_LENGTH + ) + ), + hashes.SHA1(), + backend + ) + verifier.update(binascii.unhexlify(example["message"])) + verifier.verify() + + @pytest.mark.parametrize( + "hash_alg", + [hashes.SHA224(), hashes.SHA256(), hashes.SHA384(), hashes.SHA512()] + ) + def test_pss_signing_sha2(self, hash_alg, backend): + if not backend.mgf1_hash_supported(hash_alg): + pytest.skip( + "Does not support {0} with MGF1.".format(hash_alg.name) + ) + private_key = rsa.RSAPrivateKey.generate( + public_exponent=65537, + key_size=768, + backend=backend + ) + public_key = private_key.public_key() + pss = padding.PSS( + mgf=padding.MGF1( + algorithm=hash_alg, + salt_length=padding.MGF1.MAX_LENGTH + ) + ) + signer = private_key.signer( + pss, + hash_alg, + backend + ) + signer.update(b"testing signature") + signature = signer.finalize() + verifier = public_key.verifier( + signature, + pss, + hash_alg, + backend + ) + verifier.update(b"testing signature") + verifier.verify() + + @pytest.mark.supported( + only_if=lambda backend: backend.hash_supported(hashes.SHA512()), + skip_message="Does not support SHA512." + ) + def test_pss_minimum_key_size_for_digest(self, backend): + private_key = rsa.RSAPrivateKey.generate( + public_exponent=65537, + key_size=522, + backend=backend + ) + signer = private_key.signer( + padding.PSS( + mgf=padding.MGF1( + algorithm=hashes.SHA1(), + salt_length=padding.MGF1.MAX_LENGTH + ) + ), + hashes.SHA512(), + backend + ) + signer.update(b"no failure") + signer.finalize() + + @pytest.mark.supported( + only_if=lambda backend: backend.hash_supported(hashes.SHA512()), + skip_message="Does not support SHA512." + ) + def test_pss_signing_digest_too_large_for_key_size(self, backend): + private_key = rsa.RSAPrivateKey.generate( + public_exponent=65537, + key_size=512, + backend=backend + ) + with pytest.raises(ValueError): + private_key.signer( + padding.PSS( + mgf=padding.MGF1( + algorithm=hashes.SHA1(), + salt_length=padding.MGF1.MAX_LENGTH + ) + ), + hashes.SHA512(), + backend + ) + + def test_pss_signing_salt_length_too_long(self, backend): + private_key = rsa.RSAPrivateKey.generate( + public_exponent=65537, + key_size=512, + backend=backend + ) + signer = private_key.signer( + padding.PSS( + mgf=padding.MGF1( + algorithm=hashes.SHA1(), + salt_length=1000000 + ) + ), + hashes.SHA1(), + backend + ) + signer.update(b"failure coming") + with pytest.raises(ValueError): + signer.finalize() + def test_use_after_finalize(self, backend): private_key = rsa.RSAPrivateKey.generate( public_exponent=65537, @@ -468,6 +625,16 @@ class TestRSASignature(object): private_key.signer( padding.PKCS1v15(), hashes.SHA256, pretend_backend) + def test_unsupported_pss_mgf(self, backend): + private_key = rsa.RSAPrivateKey.generate( + public_exponent=65537, + key_size=512, + backend=backend + ) + with pytest.raises(UnsupportedAlgorithm): + private_key.signer(padding.PSS(mgf=DummyMGF()), hashes.SHA1(), + backend) + @pytest.mark.rsa class TestRSAVerification(object): @@ -558,7 +725,7 @@ class TestRSAVerification(object): padding.PSS( mgf=padding.MGF1( algorithm=hashes.SHA1(), - salt_length=padding.MGF1.MAX_LENGTH + salt_length=20 ) ), hashes.SHA1(), @@ -719,6 +886,34 @@ class TestRSAVerification(object): public_key.verifier(b"sig", padding.PSS(mgf=DummyMGF()), hashes.SHA1(), backend) + @pytest.mark.supported( + only_if=lambda backend: backend.hash_supported(hashes.SHA512()), + skip_message="Does not support SHA512." + ) + def test_pss_verify_digest_too_large_for_key_size(self, backend): + private_key = rsa.RSAPrivateKey.generate( + public_exponent=65537, + key_size=512, + backend=backend + ) + signature = binascii.unhexlify( + b"8b9a3ae9fb3b64158f3476dd8d8a1f1425444e98940e0926378baa9944d219d8" + b"534c050ef6b19b1bdc6eb4da422e89161106a6f5b5cc16135b11eb6439b646bd" + ) + public_key = private_key.public_key() + with pytest.raises(ValueError): + public_key.verifier( + signature, + padding.PSS( + mgf=padding.MGF1( + algorithm=hashes.SHA1(), + salt_length=padding.MGF1.MAX_LENGTH + ) + ), + hashes.SHA512(), + backend + ) + def test_pss_verify_salt_length_too_long(self, backend): signature = binascii.unhexlify( b"8b9a3ae9fb3b64158f3476dd8d8a1f1425444e98940e0926378baa9944d219d8" |