Skip to content

Commit c28f15a

Browse files
authored
Merge pull request #120 from mattsb42-aws/fix-rsa
Fix invalid RSA private key PKCS8 encoding
2 parents ff8a03b + 428c29c commit c28f15a

File tree

6 files changed

+474
-41
lines changed

6 files changed

+474
-41
lines changed

jose/backends/rsa_backend.py

Lines changed: 87 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1+
import binascii
2+
13
import six
2-
from pyasn1.codec.der import encoder
3-
from pyasn1.type import univ
4+
from pyasn1.codec.der import decoder, encoder
5+
from pyasn1.error import PyAsn1Error
6+
from pyasn1.type import namedtype, univ
47

58
import rsa as pyrsa
69
import rsa.pem as pyrsa_pem
@@ -12,7 +15,18 @@
1215
from jose.utils import base64_to_long, long_to_base64
1316

1417

15-
PKCS8_RSA_HEADER = b'0\x82\x04\xbd\x02\x01\x000\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00'
18+
LEGACY_INVALID_PKCS8_RSA_HEADER = binascii.unhexlify(
19+
"30" # sequence
20+
"8204BD" # DER-encoded sequence contents length of 1213 bytes -- INCORRECT STATIC LENGTH
21+
"020100" # integer: 0 -- Version
22+
"30" # sequence
23+
"0D" # DER-encoded sequence contents length of 13 bytes -- PrivateKeyAlgorithmIdentifier
24+
"06092A864886F70D010101" # OID -- rsaEncryption
25+
"0500" # NULL -- parameters
26+
)
27+
ASN1_SEQUENCE_ID = binascii.unhexlify("30")
28+
RSA_ENCRYPTION_ASN1_OID = "1.2.840.113549.1.1.1"
29+
1630
# Functions gcd and rsa_recover_prime_factors were copied from cryptography 1.9
1731
# to enable pure python rsa module to be in compliance with section 6.3.1 of RFC7518
1832
# which requires only private exponent (d) for private key.
@@ -83,6 +97,65 @@ def pem_to_spki(pem, fmt='PKCS8'):
8397
return key.to_pem(fmt)
8498

8599

100+
def _legacy_private_key_pkcs8_to_pkcs1(pkcs8_key):
101+
"""Legacy RSA private key PKCS8-to-PKCS1 conversion.
102+
103+
.. warning::
104+
105+
This is incorrect parsing and only works because the legacy PKCS1-to-PKCS8
106+
encoding was also incorrect.
107+
"""
108+
# Only allow this processing if the prefix matches
109+
# AND the following byte indicates an ASN1 sequence,
110+
# as we would expect with the legacy encoding.
111+
if not pkcs8_key.startswith(LEGACY_INVALID_PKCS8_RSA_HEADER + ASN1_SEQUENCE_ID):
112+
raise ValueError("Invalid private key encoding")
113+
114+
return pkcs8_key[len(LEGACY_INVALID_PKCS8_RSA_HEADER):]
115+
116+
117+
class PKCS8RsaPrivateKeyAlgorithm(univ.Sequence):
118+
"""ASN1 structure for recording RSA PrivateKeyAlgorithm identifiers."""
119+
componentType = namedtype.NamedTypes(
120+
namedtype.NamedType("rsaEncryption", univ.ObjectIdentifier()),
121+
namedtype.NamedType("parameters", univ.Null())
122+
)
123+
124+
125+
class PKCS8PrivateKey(univ.Sequence):
126+
"""ASN1 structure for recording PKCS8 private keys."""
127+
componentType = namedtype.NamedTypes(
128+
namedtype.NamedType("version", univ.Integer()),
129+
namedtype.NamedType("privateKeyAlgorithm", PKCS8RsaPrivateKeyAlgorithm()),
130+
namedtype.NamedType("privateKey", univ.OctetString())
131+
)
132+
133+
134+
def _private_key_pkcs8_to_pkcs1(pkcs8_key):
135+
"""Convert a PKCS8-encoded RSA private key to PKCS1."""
136+
decoded_values = decoder.decode(pkcs8_key, asn1Spec=PKCS8PrivateKey())
137+
138+
try:
139+
decoded_key = decoded_values[0]
140+
except IndexError:
141+
raise ValueError("Invalid private key encoding")
142+
143+
return decoded_key["privateKey"]
144+
145+
146+
def _private_key_pkcs1_to_pkcs8(pkcs1_key):
147+
"""Convert a PKCS1-encoded RSA private key to PKCS8."""
148+
algorithm = PKCS8RsaPrivateKeyAlgorithm()
149+
algorithm["rsaEncryption"] = RSA_ENCRYPTION_ASN1_OID
150+
151+
pkcs8_key = PKCS8PrivateKey()
152+
pkcs8_key["version"] = 0
153+
pkcs8_key["privateKeyAlgorithm"] = algorithm
154+
pkcs8_key["privateKey"] = pkcs1_key
155+
156+
return encoder.encode(pkcs8_key)
157+
158+
86159
class RSAKey(Key):
87160
SHA256 = 'SHA-256'
88161
SHA384 = 'SHA-384'
@@ -121,12 +194,15 @@ def __init__(self, key, algorithm):
121194
self._prepared_key = pyrsa.PrivateKey.load_pkcs1(key)
122195
except ValueError:
123196
try:
124-
# python-rsa does not support PKCS8 yet so we have to manually remove OID
125197
der = pyrsa_pem.load_pem(key, b'PRIVATE KEY')
126-
header, der = der[:22], der[22:]
127-
if header != PKCS8_RSA_HEADER:
128-
raise ValueError("Invalid PKCS8 header")
129-
self._prepared_key = pyrsa.PrivateKey._load_pkcs1_der(der)
198+
try:
199+
pkcs1_key = _private_key_pkcs8_to_pkcs1(der)
200+
except PyAsn1Error:
201+
# If the key was encoded using the old, invalid,
202+
# encoding then pyasn1 will throw an error attempting
203+
# to parse the key.
204+
pkcs1_key = _legacy_private_key_pkcs8_to_pkcs1(der)
205+
self._prepared_key = pyrsa.PrivateKey.load_pkcs1(pkcs1_key, format="DER")
130206
except ValueError as e:
131207
raise JWKError(e)
132208
return
@@ -183,7 +259,8 @@ def to_pem(self, pem_format='PKCS8'):
183259
if isinstance(self._prepared_key, pyrsa.PrivateKey):
184260
der = self._prepared_key.save_pkcs1(format='DER')
185261
if pem_format == 'PKCS8':
186-
pem = pyrsa_pem.save_pem(PKCS8_RSA_HEADER + der, pem_marker='PRIVATE KEY')
262+
pkcs8_der = _private_key_pkcs1_to_pkcs8(der)
263+
pem = pyrsa_pem.save_pem(pkcs8_der, pem_marker='PRIVATE KEY')
187264
elif pem_format == 'PKCS1':
188265
pem = pyrsa_pem.save_pem(der, pem_marker='RSA PRIVATE KEY')
189266
else:
@@ -196,7 +273,7 @@ def to_pem(self, pem_format='PKCS8'):
196273
der = encoder.encode(asn_key)
197274

198275
header = PubKeyHeader()
199-
header['oid'] = univ.ObjectIdentifier('1.2.840.113549.1.1.1')
276+
header['oid'] = univ.ObjectIdentifier(RSA_ENCRYPTION_ASN1_OID)
200277
pub_key = OpenSSLPubKey()
201278
pub_key['header'] = header
202279
pub_key['key'] = univ.BitString.fromOctetString(der)

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ six
33
future
44
rsa
55
ecdsa
6+
pyasn1

setup.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,11 @@ def get_packages(package):
2626
'pycrypto': ['pycrypto >=2.6.0, <2.7.0'],
2727
'pycryptodome': ['pycryptodome >=3.3.1, <4.0.0'],
2828
}
29+
legacy_backend_requires = ['ecdsa <1.0', 'rsa', 'pyasn1']
30+
install_requires = ['six <2.0', 'future <1.0']
31+
32+
# TODO: work this into the extras selection instead.
33+
install_requires += legacy_backend_requires
2934

3035

3136
setup(
@@ -64,5 +69,5 @@ def get_packages(package):
6469
'pytest-cov',
6570
'pytest-runner',
6671
],
67-
install_requires=['six <2.0', 'ecdsa <1.0', 'rsa', 'future <1.0']
72+
install_requires=install_requires
6873
)

tests/algorithms/test_EC_compat.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
None in (ECDSAECKey, CryptographyECKey),
1616
reason="Multiple crypto backends not available for backend compatibility tests"
1717
)
18-
class TestBackendRsaCompatibility(object):
18+
class TestBackendEcdsaCompatibility(object):
1919

2020
@pytest.mark.parametrize("BackendSign", [ECDSAECKey, CryptographyECKey])
2121
@pytest.mark.parametrize("BackendVerify", [ECDSAECKey, CryptographyECKey])
@@ -31,3 +31,42 @@ def test_signing_parity(self, BackendSign, BackendVerify):
3131

3232
# invalid signature
3333
assert not key_verify.verify(msg, b'n' * 64)
34+
35+
@pytest.mark.parametrize("BackendFrom", [ECDSAECKey, CryptographyECKey])
36+
@pytest.mark.parametrize("BackendTo", [ECDSAECKey, CryptographyECKey])
37+
def test_public_key_to_pem(self, BackendFrom, BackendTo):
38+
key = BackendFrom(private_key, ALGORITHMS.ES256)
39+
key2 = BackendTo(private_key, ALGORITHMS.ES256)
40+
41+
assert key.public_key().to_pem().strip() == key2.public_key().to_pem().strip()
42+
43+
@pytest.mark.parametrize("BackendFrom", [ECDSAECKey, CryptographyECKey])
44+
@pytest.mark.parametrize("BackendTo", [ECDSAECKey, CryptographyECKey])
45+
def test_private_key_to_pem(self, BackendFrom, BackendTo):
46+
key = BackendFrom(private_key, ALGORITHMS.ES256)
47+
key2 = BackendTo(private_key, ALGORITHMS.ES256)
48+
49+
assert key.to_pem().strip() == key2.to_pem().strip()
50+
51+
@pytest.mark.parametrize("BackendFrom", [ECDSAECKey, CryptographyECKey])
52+
@pytest.mark.parametrize("BackendTo", [ECDSAECKey, CryptographyECKey])
53+
def test_public_key_load_cycle(self, BackendFrom, BackendTo):
54+
key = BackendFrom(private_key, ALGORITHMS.ES256)
55+
pubkey = key.public_key()
56+
57+
pub_pem_source = pubkey.to_pem().strip()
58+
59+
pub_target = BackendTo(pub_pem_source, ALGORITHMS.ES256)
60+
61+
assert pub_pem_source == pub_target.to_pem().strip()
62+
63+
@pytest.mark.parametrize("BackendFrom", [ECDSAECKey, CryptographyECKey])
64+
@pytest.mark.parametrize("BackendTo", [ECDSAECKey, CryptographyECKey])
65+
def test_private_key_load_cycle(self, BackendFrom, BackendTo):
66+
key = BackendFrom(private_key, ALGORITHMS.ES256)
67+
68+
pem_source = key.to_pem().strip()
69+
70+
target = BackendTo(pem_source, ALGORITHMS.ES256)
71+
72+
assert pem_source == target.to_pem().strip()

0 commit comments

Comments
 (0)