Skip to content

Commit e128666

Browse files
authored
Updating code to support PKCS#8 private key format (#59)
1 parent ba9de87 commit e128666

8 files changed

Lines changed: 97 additions & 10 deletions

File tree

‎mauth_client/rsa_signer.py‎

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
import rsa
77
from .utils import make_bytes, hexdigest
8+
from pyasn1.codec.der import decoder
9+
10+
RSA_ALGORITHM_OID = "1.2.840.113549.1.1.1"
811

912

1013
class RSASigner:
@@ -16,7 +19,29 @@ def __init__(self, private_key_data):
1619
"""
1720
:param private_key_data:
1821
"""
19-
self.private_key = rsa.PrivateKey.load_pkcs1(private_key_data, "PEM")
22+
self.private_key = self.load_private_key(private_key_data)
23+
24+
@staticmethod
25+
def load_private_key(private_key_data):
26+
private_key_data = make_bytes(private_key_data)
27+
try:
28+
return rsa.PrivateKey.load_pkcs1(private_key_data, "PEM")
29+
except ValueError:
30+
try:
31+
return RSASigner.load_pkcs8_private_key(private_key_data)
32+
except ValueError as pkcs8_error:
33+
raise ValueError("Unable to load private key as PKCS#1 or PKCS#8 PEM") from pkcs8_error
34+
35+
@staticmethod
36+
def load_pkcs8_private_key(private_key_data):
37+
private_key_der = rsa.pem.load_pem(private_key_data, "PRIVATE KEY")
38+
pkcs8_structure, _ = decoder.decode(private_key_der)
39+
40+
algorithm = str(pkcs8_structure[1][0])
41+
if algorithm != RSA_ALGORITHM_OID:
42+
raise ValueError(f"Expected RSA algorithm OID {RSA_ALGORITHM_OID}, but got: {algorithm}")
43+
44+
return rsa.PrivateKey.load_pkcs1(bytes(pkcs8_structure[2]), "DER")
2045

2146
def sign_v2(self, string_to_sign):
2247
"""Signs the data using SHA512 for V2 protocol

‎mauth_client/utils.py‎

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@
55

66
HEADER = '-----BEGIN RSA PRIVATE KEY-----'
77
FOOTER = '-----END RSA PRIVATE KEY-----'
8+
PKCS8_HEADER = '-----BEGIN PRIVATE KEY-----'
9+
PKCS8_FOOTER = '-----END PRIVATE KEY-----'
10+
SUPPORTED_PRIVATE_KEY_FORMATS = (
11+
(HEADER, FOOTER),
12+
(PKCS8_HEADER, PKCS8_FOOTER),
13+
)
814

915

1016
def make_bytes(val):
@@ -39,13 +45,23 @@ def decode(byte_string: bytes) -> str:
3945

4046

4147
def to_rsa_format(key: str) -> str:
42-
"""Convert a private key to RSA format with proper newlines."""
48+
"""Normalize a private key PEM string with proper newlines.
4349
44-
if "\n" in key and HEADER in key and FOOTER in key:
50+
Supports both PKCS#1 (``-----BEGIN RSA PRIVATE KEY-----``) and
51+
PKCS#8 (``-----BEGIN PRIVATE KEY-----``) PEM formats, preserving
52+
the original header and footer markers.
53+
"""
54+
55+
header, footer = next(
56+
((hdr, ftr) for hdr, ftr in SUPPORTED_PRIVATE_KEY_FORMATS if hdr in key and ftr in key),
57+
(HEADER, FOOTER),
58+
)
59+
60+
if "\n" in key and header in key and footer in key:
4561
return key
4662

4763
body = key.strip()
48-
body = body.replace(HEADER, "").replace(FOOTER, "").strip()
64+
body = body.replace(header, "").replace(footer, "").strip()
4965

5066
# Replace whitespace with newlines or chunk into 64-char lines
5167
if " " in body or "\t" in body:
@@ -54,4 +70,4 @@ def to_rsa_format(key: str) -> str:
5470
# PEM-encoded keys are typically split into lines of 64 characters as per RFC 7468 (section 2)
5571
body = '\n'.join(body[i:i + 64] for i in range(0, len(body), 64))
5672

57-
return f"{HEADER}\n{body}\n{FOOTER}"
73+
return f"{header}\n{body}\n{footer}"

‎poetry.lock‎

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pyproject.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ python = "^3.9"
2727
requests = "^2.31.0"
2828
cachetools = "^5.3.3"
2929
rsa = "^4.9"
30+
pyasn1 = ">=0.1.3"
3031
asgiref = "^3.8.1"
3132
charset-normalizer = "^3.3.2"
3233
importlib = "^1.0.4"

‎tests/common.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def load_key(keytype="pub"):
88
:return: key content
99
:rtype: str
1010
"""
11-
assert keytype in ("pub", "rsapub", "priv")
11+
assert keytype in ("pub", "rsapub", "priv", "pkcs8")
1212
content = ""
1313
with open(
1414
os.path.join(os.path.dirname(__file__), "keys", "fake_mauth.{}.key".format(keytype)), "r"

‎tests/keys/fake_mauth.pkcs8.key‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
-----BEGIN PRIVATE KEY-----
2+
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDnKhtl4TR+RYem
3+
ogVBPqKlFq6t7K+AJYdwniTKdTaobGPOvrd+DBbSzIQatwYCE0pwnpASR3KWpPK3
4+
KCBUV4FHYo7VpUlRi9AH60DKo/D+QclTvqD7PHl8pohRZtoBjy8JPWHLg8i+doj6
5+
GGJFRX6U198vtqfcC/sxjmC/BJOWuTe0IpVSsdqIZPd8s4YfZkkEafvbgSJ10a07
6+
h5JWJhS2qC1yzxQyiKmtFFnnffTRIhiCy9xTX6VLfdC+BOlYqsvdiJj5UKfPGCPZ
7+
+4y35RasW7bfGF7p6gHIhFLV/aQ56auh3KYv830aCkTBQkIbIMIc/6+HKBtLWQgs
8+
EG+WtZSxAgMBAAECggEAfzhpQ7Shkyzo7wgUJEg4cur8z7OHEtuUoqImVVYU8rRU
9+
aaAElFZK6VCtE5bOs62yNjDN0YGIyueUvMBUqpZOC99uBJ6mrz5nCzSpv9xh8Ux7
10+
ZJ9xMVOi+LseIs6cBGes10T04XBxtoC9+gYZb05Dz97Ocg/waeYeJLPbAsWeBEB3
11+
ZFivx22bNdL3dh9ow0L6Zq3ewCyIbY6FsESeSddZpTZBxDKwvH5hVrltxyNBwItz
12+
nf9lAQQq/3aKxKEs9ohIiHXESjyCXCE5BfnFKRYvkNJh8tUMpkRVMDy6ry69G4Kn
13+
7IJCSwa+MU40z0AARumAys4OZLh8q8Lyd6Te6NzhUQKBgQD1ygrmZ+dGZTnE1+HE
14+
jA+80IYXsE3mQCYAUzZQtbocs887uZKCBjzvS6gSoDIhLNzxz0AQbvvtQD9wS8wz
15+
96FXx92F6mFPPQ4EW2CqFkqeaQ6zavLcThIOWB4u5XTFTO/GfjmFaz3eWzapQhjn
16+
E5Ui8iQLVLZIu1IExuOE2X0s8wKBgQDwxIfpSbEmcHYj9UIxE2Hrq1QU5roAi7PF
17+
rEEj/2CduW3yTWewpGUufbzDSRsaDWSM3p85DUrZNP7hCLi9YyhK4mDMeo8RiDgi
18+
6Do4fkLvroTlNXd6aJ6a+B8zC0wOPLpGeAcSeouX0VWmllKcXU0p/w8iQs5AzWk6
19+
y7MLob1QywKBgDhZftA+B/7SnEDtwXvfJIiEQgnSSLm+AThX2377etSzwJIPxG/8
20+
4CF4der3QSbr8yeY/TYHS8ijzyd+qS1M23eVgcr+5hpVhA4pqOq1u5u/uDgYD9/E
21+
Ik2ZlCnqPzAwqFQYgjV9VazRuqFpXu4zRiDB5NOYM0ZPc8lWk7jUZ+dTAoGBAKZ4
22+
LESnmB2synKIRDDsZtrsheFBuj9gWHcHZ669Bw9mw1cyH8xpji/77gTUSjHUKr6f
23+
ulVXFHZkBwqO3jTMF3LXXPhkkQEdPzsbeA3RdvSDpQ1Zz3dKWMdrYR8I45hAcscA
24+
fFtRca0RpvfcndfRRuYPnWYh7Luvit+FMhGLzfrLAoGBALkSY5Y/AhgWOf3U83Rg
25+
5MlYBMN0id1ae9ZnTjmFsmnNRvTmQSSdMNuqM8Gqws7iXQPWAknKrWKc2bIQ15Yz
26+
+FLKAMd0zpRwgwh2oVLmCydUKDDQJM6WNIBYI+dNPX4NR28WckeHT3eYOZjVHw3L
27+
f0/HiT3RZ22EI8lwkeP7E+GP
28+
-----END PRIVATE KEY-----

‎tests/signer_test.py‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,10 @@ class SignerTest(unittest.TestCase):
2525
def setUp(self):
2626
with open(os.path.join(os.path.dirname(__file__), "keys", "fake_mauth.priv.key"), "r") as key_file:
2727
self.private_key = key_file.read()
28+
with open(os.path.join(os.path.dirname(__file__), "keys", "fake_mauth.pkcs8.key"), "r") as key_file:
29+
self.private_key_pkcs8 = key_file.read()
2830
self.signer = Signer(APP_UUID, self.private_key, "v1,v2")
31+
self.signer_pkcs8 = Signer(APP_UUID, self.private_key_pkcs8, "v1,v2")
2932
self.signer_v1_only = Signer(APP_UUID, self.private_key, "v1")
3033
self.signer_v2_only = Signer(APP_UUID, self.private_key, "v2")
3134
self.signable = RequestSignable(**REQUEST_ATTRIBUTES)
@@ -137,3 +140,7 @@ def test_sign_versions_bad_version(self):
137140
self.assertEqual(
138141
str(exc.exception), "SIGN_VERSIONS must be comma-separated MAuth protocol versions (e.g. 'v1,v2')"
139142
)
143+
144+
def test_pkcs8_and_pkcs1_signatures_match(self):
145+
self.assertEqual(self.signer_pkcs8.signature_v1("Hello world"), self.signer.signature_v1("Hello world"))
146+
self.assertEqual(self.signer_pkcs8.signature_v2("Hello world"), self.signer.signature_v2("Hello world"))

‎tests/utils_test.py‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from mauth_client.utils import to_rsa_format
55

66
PRIVATE_KEY = load_key("priv").strip()
7+
PRIVATE_KEY_PKCS8 = load_key("pkcs8").strip()
78

89

910
class TestToRsaFormat(unittest.TestCase):
@@ -20,3 +21,12 @@ def test_newlines_removed(self):
2021
key_no_newlines = PRIVATE_KEY.replace("\n", "")
2122
key = to_rsa_format(key_no_newlines)
2223
self.assertEqual(key, PRIVATE_KEY)
24+
25+
def test_proper_format_pkcs8(self):
26+
key = to_rsa_format(PRIVATE_KEY_PKCS8)
27+
self.assertEqual(key, PRIVATE_KEY_PKCS8)
28+
29+
def test_newlines_replaced_with_spaces_pkcs8(self):
30+
key_no_newlines = PRIVATE_KEY_PKCS8.replace("\n", " ")
31+
key = to_rsa_format(key_no_newlines)
32+
self.assertEqual(key, PRIVATE_KEY_PKCS8)

0 commit comments

Comments
 (0)