Skip to content

Commit 6693d55

Browse files
authored
Add support for RSA signature recovery (#5573)
* Removed unused argument. * Added support for RSA signature recovery. * Syntatic corrections for passing pep8 tests. * Corrected typo. * Added test of invalid Prehashed parameter to RSA signature recover. * Renamed recover to a more descriptive name. * Extended RSA signature recovery with option to return full data (not only the digest part). * Added missing words to pass spell check.
1 parent 8686d52 commit 6693d55

File tree

8 files changed

+191
-16
lines changed

8 files changed

+191
-16
lines changed

CHANGELOG.rst

+5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ Changelog
2727
in any application outside of testing.
2828
* Python 2 support is deprecated in ``cryptography``. This is the last release
2929
that will support Python 2.
30+
* Added the
31+
:meth:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey.recover_data_from_signature`
32+
function to
33+
:class:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey`
34+
for recovering the signed data from an RSA signature.
3035

3136
.. _v3-2-1:
3237

docs/hazmat/primitives/asymmetric/rsa.rst

+49
Original file line numberDiff line numberDiff line change
@@ -714,6 +714,55 @@ Key interfaces
714714
:raises cryptography.exceptions.InvalidSignature: If the signature does
715715
not validate.
716716

717+
.. method:: recover_data_from_signature(signature, padding, algorithm)
718+
719+
.. versionadded:: 3.3
720+
721+
Recovers the signed data from the signature. The data contains the
722+
digest of the original message string. The ``padding`` and
723+
``algorithm`` parameters must match the ones used when the signature
724+
was created for the recovery to succeed.
725+
726+
The ``algorithm`` parameter can also be set to ``None`` to recover all
727+
the data present in the signature, without regard to its format or the
728+
hash algorithm used for its creation.
729+
730+
For
731+
:class:`~cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15`
732+
padding, this returns the data after removing the padding layer. For
733+
standard signatures the data contains the full ``DigestInfo`` structure.
734+
For non-standard signatures, any data can be returned, including zero-
735+
length data.
736+
737+
Normally you should use the
738+
:meth:`~cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey.verify`
739+
function to validate the signature. But for some non-standard signature
740+
formats you may need to explicitly recover and validate the signed
741+
data. Following are some examples:
742+
743+
- Some old Thawte and Verisign timestamp certificates without ``DigestInfo``.
744+
- Signed MD5/SHA1 hashes in TLS 1.1 or earlier (RFC 4346, section 4.7).
745+
- IKE version 1 signatures without ``DigestInfo`` (RFC 2409, section 5.1).
746+
747+
:param bytes signature: The signature.
748+
749+
:param padding: An instance of
750+
:class:`~cryptography.hazmat.primitives.asymmetric.padding.AsymmetricPadding`.
751+
Recovery is only supported with some of the padding types. (Currently
752+
only with
753+
:class:`~cryptography.hazmat.primitives.asymmetric.padding.PKCS1v15`).
754+
755+
:param algorithm: An instance of
756+
:class:`~cryptography.hazmat.primitives.hashes.HashAlgorithm`.
757+
Can be ``None`` to return the all the data present in the signature.
758+
759+
:return bytes: The signed data.
760+
761+
:raises cryptography.exceptions.InvalidSignature: If the signature is
762+
invalid.
763+
764+
:raises cryptography.exceptions.UnsupportedAlgorithm: If signature
765+
data recovery is not supported with the provided ``padding`` type.
717766

718767
.. class:: RSAPublicKeyWithSerialization
719768

docs/spelling_wordlist.txt

+1
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ Solaris
103103
syscall
104104
Tanja
105105
testability
106+
Thawte
106107
timestamp
107108
timestamps
108109
tunable

src/_cffi_src/openssl/evp.py

+3
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@
101101
int EVP_PKEY_verify_init(EVP_PKEY_CTX *);
102102
int EVP_PKEY_verify(EVP_PKEY_CTX *, const unsigned char *, size_t,
103103
const unsigned char *, size_t);
104+
int EVP_PKEY_verify_recover_init(EVP_PKEY_CTX *);
105+
int EVP_PKEY_verify_recover(EVP_PKEY_CTX *, unsigned char *,
106+
size_t *, const unsigned char *, size_t);
104107
int EVP_PKEY_encrypt_init(EVP_PKEY_CTX *);
105108
int EVP_PKEY_decrypt_init(EVP_PKEY_CTX *);
106109

src/cryptography/hazmat/backends/openssl/rsa.py

+62-11
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ def _rsa_sig_determine_padding(backend, key, padding, algorithm):
142142
backend.openssl_assert(pkey_size > 0)
143143

144144
if isinstance(padding, PKCS1v15):
145+
# Hash algorithm is ignored for PKCS1v15-padding, may be None.
145146
padding_enum = backend._lib.RSA_PKCS1_PADDING
146147
elif isinstance(padding, PSS):
147148
if not isinstance(padding._mgf, MGF1):
@@ -150,6 +151,10 @@ def _rsa_sig_determine_padding(backend, key, padding, algorithm):
150151
_Reasons.UNSUPPORTED_MGF,
151152
)
152153

154+
# PSS padding requires a hash algorithm
155+
if not isinstance(algorithm, hashes.HashAlgorithm):
156+
raise TypeError("Expected instance of hashes.HashAlgorithm.")
157+
153158
# Size of key in bytes - 2 is the maximum
154159
# PSS signature length (salt length is checked later)
155160
if pkey_size - algorithm.digest_size - 2 < 0:
@@ -168,25 +173,37 @@ def _rsa_sig_determine_padding(backend, key, padding, algorithm):
168173
return padding_enum
169174

170175

171-
def _rsa_sig_setup(backend, padding, algorithm, key, data, init_func):
176+
# Hash algorithm can be absent (None) to initialize the context without setting
177+
# any message digest algorithm. This is currently only valid for the PKCS1v15
178+
# padding type, where it means that the signature data is encoded/decoded
179+
# as provided, without being wrapped in a DigestInfo structure.
180+
def _rsa_sig_setup(backend, padding, algorithm, key, init_func):
172181
padding_enum = _rsa_sig_determine_padding(backend, key, padding, algorithm)
173-
evp_md = backend._evp_md_non_null_from_algorithm(algorithm)
174182
pkey_ctx = backend._lib.EVP_PKEY_CTX_new(key._evp_pkey, backend._ffi.NULL)
175183
backend.openssl_assert(pkey_ctx != backend._ffi.NULL)
176184
pkey_ctx = backend._ffi.gc(pkey_ctx, backend._lib.EVP_PKEY_CTX_free)
177185
res = init_func(pkey_ctx)
178186
backend.openssl_assert(res == 1)
179-
res = backend._lib.EVP_PKEY_CTX_set_signature_md(pkey_ctx, evp_md)
180-
if res == 0:
187+
if algorithm is not None:
188+
evp_md = backend._evp_md_non_null_from_algorithm(algorithm)
189+
res = backend._lib.EVP_PKEY_CTX_set_signature_md(pkey_ctx, evp_md)
190+
if res == 0:
191+
backend._consume_errors()
192+
raise UnsupportedAlgorithm(
193+
"{} is not supported by this backend for RSA signing.".format(
194+
algorithm.name
195+
),
196+
_Reasons.UNSUPPORTED_HASH,
197+
)
198+
res = backend._lib.EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, padding_enum)
199+
if res <= 0:
181200
backend._consume_errors()
182201
raise UnsupportedAlgorithm(
183-
"{} is not supported by this backend for RSA signing.".format(
184-
algorithm.name
202+
"{} is not supported for the RSA signature operation.".format(
203+
padding.name
185204
),
186-
_Reasons.UNSUPPORTED_HASH,
205+
_Reasons.UNSUPPORTED_PADDING,
187206
)
188-
res = backend._lib.EVP_PKEY_CTX_set_rsa_padding(pkey_ctx, padding_enum)
189-
backend.openssl_assert(res > 0)
190207
if isinstance(padding, PSS):
191208
res = backend._lib.EVP_PKEY_CTX_set_rsa_pss_saltlen(
192209
pkey_ctx, _get_rsa_pss_salt_length(padding, key, algorithm)
@@ -208,7 +225,6 @@ def _rsa_sig_sign(backend, padding, algorithm, private_key, data):
208225
padding,
209226
algorithm,
210227
private_key,
211-
data,
212228
backend._lib.EVP_PKEY_sign_init,
213229
)
214230
buflen = backend._ffi.new("size_t *")
@@ -235,7 +251,6 @@ def _rsa_sig_verify(backend, padding, algorithm, public_key, signature, data):
235251
padding,
236252
algorithm,
237253
public_key,
238-
data,
239254
backend._lib.EVP_PKEY_verify_init,
240255
)
241256
res = backend._lib.EVP_PKEY_verify(
@@ -250,6 +265,36 @@ def _rsa_sig_verify(backend, padding, algorithm, public_key, signature, data):
250265
raise InvalidSignature
251266

252267

268+
def _rsa_sig_recover(backend, padding, algorithm, public_key, signature):
269+
pkey_ctx = _rsa_sig_setup(
270+
backend,
271+
padding,
272+
algorithm,
273+
public_key,
274+
backend._lib.EVP_PKEY_verify_recover_init,
275+
)
276+
277+
# Attempt to keep the rest of the code in this function as constant/time
278+
# as possible. See the comment in _enc_dec_rsa_pkey_ctx. Note that the
279+
# outlen parameter is used even though its value may be undefined in the
280+
# error case. Due to the tolerant nature of Python slicing this does not
281+
# trigger any exceptions.
282+
maxlen = backend._lib.EVP_PKEY_size(public_key._evp_pkey)
283+
backend.openssl_assert(maxlen > 0)
284+
buf = backend._ffi.new("unsigned char[]", maxlen)
285+
buflen = backend._ffi.new("size_t *", maxlen)
286+
res = backend._lib.EVP_PKEY_verify_recover(
287+
pkey_ctx, buf, buflen, signature, len(signature)
288+
)
289+
resbuf = backend._ffi.buffer(buf)[: buflen[0]]
290+
backend._lib.ERR_clear_error()
291+
# Assume that all parameter errors are handled during the setup phase and
292+
# any error here is due to invalid signature.
293+
if res != 1:
294+
raise InvalidSignature
295+
return resbuf
296+
297+
253298
@utils.register_interface(AsymmetricSignatureContext)
254299
class _RSASignatureContext(object):
255300
def __init__(self, backend, private_key, padding, algorithm):
@@ -463,3 +508,9 @@ def verify(self, signature, data, padding, algorithm):
463508
return _rsa_sig_verify(
464509
self._backend, padding, algorithm, self, signature, data
465510
)
511+
512+
def recover_data_from_signature(self, signature, padding, algorithm):
513+
_check_not_prehashed(algorithm)
514+
return _rsa_sig_recover(
515+
self._backend, padding, algorithm, self, signature
516+
)

src/cryptography/hazmat/backends/openssl/utils.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,8 @@ def _check_not_prehashed(signature_algorithm):
5252
if isinstance(signature_algorithm, Prehashed):
5353
raise TypeError(
5454
"Prehashed is only supported in the sign and verify methods. "
55-
"It cannot be used with signer or verifier."
55+
"It cannot be used with signer, verifier or "
56+
"recover_data_from_signature."
5657
)
5758

5859

src/cryptography/hazmat/primitives/asymmetric/rsa.py

+6
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ def verify(self, signature, data, padding, algorithm):
106106
Verifies the signature of the data.
107107
"""
108108

109+
@abc.abstractmethod
110+
def recover_data_from_signature(self, signature, padding, algorithm):
111+
"""
112+
Recovers the original data from the signature.
113+
"""
114+
109115

110116
RSAPublicKeyWithSerialization = RSAPublicKey
111117

tests/hazmat/primitives/test_rsa.py

+63-4
Original file line numberDiff line numberDiff line change
@@ -730,6 +730,18 @@ def test_prehashed_unsupported_in_verifier_ctx(self, backend):
730730
asym_utils.Prehashed(hashes.SHA1()),
731731
)
732732

733+
def test_prehashed_unsupported_in_signature_recover(self, backend):
734+
private_key = RSA_KEY_512.private_key(backend)
735+
public_key = private_key.public_key()
736+
signature = private_key.sign(
737+
b"sign me", padding.PKCS1v15(), hashes.SHA1()
738+
)
739+
prehashed_alg = asym_utils.Prehashed(hashes.SHA1())
740+
with pytest.raises(TypeError):
741+
public_key.recover_data_from_signature(
742+
signature, padding.PKCS1v15(), prehashed_alg
743+
)
744+
733745
def test_corrupted_private_key(self, backend):
734746
with pytest.raises(ValueError):
735747
serialization.load_pem_private_key(
@@ -759,13 +771,28 @@ def test_pkcs1v15_verification(self, pkcs1_example, backend):
759771
public_key = rsa.RSAPublicNumbers(
760772
e=public["public_exponent"], n=public["modulus"]
761773
).public_key(backend)
774+
signature = binascii.unhexlify(example["signature"])
775+
message = binascii.unhexlify(example["message"])
762776
public_key.verify(
763-
binascii.unhexlify(example["signature"]),
764-
binascii.unhexlify(example["message"]),
765-
padding.PKCS1v15(),
766-
hashes.SHA1(),
777+
signature, message, padding.PKCS1v15(), hashes.SHA1()
767778
)
768779

780+
# Test digest recovery by providing hash
781+
digest = hashes.Hash(hashes.SHA1())
782+
digest.update(message)
783+
msg_digest = digest.finalize()
784+
rec_msg_digest = public_key.recover_data_from_signature(
785+
signature, padding.PKCS1v15(), hashes.SHA1()
786+
)
787+
assert msg_digest == rec_msg_digest
788+
789+
# Test recovery of all data (full DigestInfo) with hash alg. as None
790+
rec_sig_data = public_key.recover_data_from_signature(
791+
signature, padding.PKCS1v15(), None
792+
)
793+
assert len(rec_sig_data) > len(msg_digest)
794+
assert msg_digest == rec_sig_data[-len(msg_digest) :]
795+
769796
@pytest.mark.supported(
770797
only_if=lambda backend: backend.rsa_padding_supported(
771798
padding.PKCS1v15()
@@ -783,6 +810,17 @@ def test_invalid_pkcs1v15_signature_wrong_data(self, backend):
783810
signature, b"incorrect data", padding.PKCS1v15(), hashes.SHA1()
784811
)
785812

813+
def test_invalid_pkcs1v15_signature_recover_wrong_hash_alg(self, backend):
814+
private_key = RSA_KEY_512.private_key(backend)
815+
public_key = private_key.public_key()
816+
signature = private_key.sign(
817+
b"sign me", padding.PKCS1v15(), hashes.SHA1()
818+
)
819+
with pytest.raises(InvalidSignature):
820+
public_key.recover_data_from_signature(
821+
signature, padding.PKCS1v15(), hashes.SHA256()
822+
)
823+
786824
def test_invalid_signature_sequence_removed(self, backend):
787825
"""
788826
This test comes from wycheproof
@@ -970,6 +1008,27 @@ def test_invalid_pss_signature_data_too_large_for_modulus(self, backend):
9701008
hashes.SHA1(),
9711009
)
9721010

1011+
def test_invalid_pss_signature_recover(self, backend):
1012+
private_key = RSA_KEY_1024.private_key(backend)
1013+
public_key = private_key.public_key()
1014+
pss_padding = padding.PSS(
1015+
mgf=padding.MGF1(algorithm=hashes.SHA1()),
1016+
salt_length=padding.PSS.MAX_LENGTH,
1017+
)
1018+
signature = private_key.sign(b"sign me", pss_padding, hashes.SHA1())
1019+
1020+
# Hash algorithm can not be absent for PSS padding
1021+
with pytest.raises(TypeError):
1022+
public_key.recover_data_from_signature(
1023+
signature, pss_padding, None
1024+
)
1025+
1026+
# Signature data recovery not supported with PSS
1027+
with raises_unsupported_algorithm(_Reasons.UNSUPPORTED_PADDING):
1028+
public_key.recover_data_from_signature(
1029+
signature, pss_padding, hashes.SHA1()
1030+
)
1031+
9731032
@pytest.mark.supported(
9741033
only_if=lambda backend: backend.rsa_padding_supported(
9751034
padding.PKCS1v15()

0 commit comments

Comments
 (0)