Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion mauth_client/rsa_signer.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@

import rsa
from .utils import make_bytes, hexdigest
from pyasn1.codec.der import decoder

RSA_ALGORITHM_OID = "1.2.840.113549.1.1.1"
Comment thread
johnduhart marked this conversation as resolved.


class RSASigner:
Expand All @@ -16,7 +19,29 @@ def __init__(self, private_key_data):
"""
:param private_key_data:
"""
self.private_key = rsa.PrivateKey.load_pkcs1(private_key_data, "PEM")
self.private_key = self.load_private_key(private_key_data)

@staticmethod
def load_private_key(private_key_data):
private_key_data = make_bytes(private_key_data)
try:
return rsa.PrivateKey.load_pkcs1(private_key_data, "PEM")
except ValueError:
try:
return RSASigner.load_pkcs8_private_key(private_key_data)
except ValueError as pkcs8_error:
raise ValueError("Unable to load private key as PKCS#1 or PKCS#8 PEM") from pkcs8_error

@staticmethod
def load_pkcs8_private_key(private_key_data):
private_key_der = rsa.pem.load_pem(private_key_data, "PRIVATE KEY")
pkcs8_structure, _ = decoder.decode(private_key_der)

algorithm = str(pkcs8_structure[1][0])
if algorithm != RSA_ALGORITHM_OID:
raise ValueError(f"Expected RSA algorithm OID {RSA_ALGORITHM_OID}, but got: {algorithm}")

return rsa.PrivateKey.load_pkcs1(bytes(pkcs8_structure[2]), "DER")
Comment thread
johnduhart marked this conversation as resolved.

def sign_v2(self, string_to_sign):
"""Signs the data using SHA512 for V2 protocol
Expand Down
24 changes: 20 additions & 4 deletions mauth_client/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@

HEADER = '-----BEGIN RSA PRIVATE KEY-----'
FOOTER = '-----END RSA PRIVATE KEY-----'
PKCS8_HEADER = '-----BEGIN PRIVATE KEY-----'
PKCS8_FOOTER = '-----END PRIVATE KEY-----'
SUPPORTED_PRIVATE_KEY_FORMATS = (
(HEADER, FOOTER),
(PKCS8_HEADER, PKCS8_FOOTER),
)


def make_bytes(val):
Expand Down Expand Up @@ -39,13 +45,23 @@ def decode(byte_string: bytes) -> str:


def to_rsa_format(key: str) -> str:
"""Convert a private key to RSA format with proper newlines."""
"""Normalize a private key PEM string with proper newlines.

if "\n" in key and HEADER in key and FOOTER in key:
Supports both PKCS#1 (``-----BEGIN RSA PRIVATE KEY-----``) and
PKCS#8 (``-----BEGIN PRIVATE KEY-----``) PEM formats, preserving
the original header and footer markers.
"""

header, footer = next(
((hdr, ftr) for hdr, ftr in SUPPORTED_PRIVATE_KEY_FORMATS if hdr in key and ftr in key),
(HEADER, FOOTER),
)
Comment thread
johnduhart marked this conversation as resolved.

if "\n" in key and header in key and footer in key:
return key

body = key.strip()
body = body.replace(HEADER, "").replace(FOOTER, "").strip()
body = body.replace(header, "").replace(footer, "").strip()

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

return f"{HEADER}\n{body}\n{FOOTER}"
return f"{header}\n{body}\n{footer}"
8 changes: 4 additions & 4 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ python = "^3.9"
requests = "^2.31.0"
cachetools = "^5.3.3"
rsa = "^4.9"
pyasn1 = ">=0.1.3"
asgiref = "^3.8.1"
charset-normalizer = "^3.3.2"
importlib = "^1.0.4"
Expand Down
2 changes: 1 addition & 1 deletion tests/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ def load_key(keytype="pub"):
:return: key content
:rtype: str
"""
assert keytype in ("pub", "rsapub", "priv")
assert keytype in ("pub", "rsapub", "priv", "pkcs8")
content = ""
with open(
os.path.join(os.path.dirname(__file__), "keys", "fake_mauth.{}.key".format(keytype)), "r"
Expand Down
28 changes: 28 additions & 0 deletions tests/keys/fake_mauth.pkcs8.key
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDnKhtl4TR+RYem
ogVBPqKlFq6t7K+AJYdwniTKdTaobGPOvrd+DBbSzIQatwYCE0pwnpASR3KWpPK3
KCBUV4FHYo7VpUlRi9AH60DKo/D+QclTvqD7PHl8pohRZtoBjy8JPWHLg8i+doj6
GGJFRX6U198vtqfcC/sxjmC/BJOWuTe0IpVSsdqIZPd8s4YfZkkEafvbgSJ10a07
h5JWJhS2qC1yzxQyiKmtFFnnffTRIhiCy9xTX6VLfdC+BOlYqsvdiJj5UKfPGCPZ
+4y35RasW7bfGF7p6gHIhFLV/aQ56auh3KYv830aCkTBQkIbIMIc/6+HKBtLWQgs
EG+WtZSxAgMBAAECggEAfzhpQ7Shkyzo7wgUJEg4cur8z7OHEtuUoqImVVYU8rRU
aaAElFZK6VCtE5bOs62yNjDN0YGIyueUvMBUqpZOC99uBJ6mrz5nCzSpv9xh8Ux7
ZJ9xMVOi+LseIs6cBGes10T04XBxtoC9+gYZb05Dz97Ocg/waeYeJLPbAsWeBEB3
ZFivx22bNdL3dh9ow0L6Zq3ewCyIbY6FsESeSddZpTZBxDKwvH5hVrltxyNBwItz
nf9lAQQq/3aKxKEs9ohIiHXESjyCXCE5BfnFKRYvkNJh8tUMpkRVMDy6ry69G4Kn
7IJCSwa+MU40z0AARumAys4OZLh8q8Lyd6Te6NzhUQKBgQD1ygrmZ+dGZTnE1+HE
jA+80IYXsE3mQCYAUzZQtbocs887uZKCBjzvS6gSoDIhLNzxz0AQbvvtQD9wS8wz
96FXx92F6mFPPQ4EW2CqFkqeaQ6zavLcThIOWB4u5XTFTO/GfjmFaz3eWzapQhjn
E5Ui8iQLVLZIu1IExuOE2X0s8wKBgQDwxIfpSbEmcHYj9UIxE2Hrq1QU5roAi7PF
rEEj/2CduW3yTWewpGUufbzDSRsaDWSM3p85DUrZNP7hCLi9YyhK4mDMeo8RiDgi
6Do4fkLvroTlNXd6aJ6a+B8zC0wOPLpGeAcSeouX0VWmllKcXU0p/w8iQs5AzWk6
y7MLob1QywKBgDhZftA+B/7SnEDtwXvfJIiEQgnSSLm+AThX2377etSzwJIPxG/8
4CF4der3QSbr8yeY/TYHS8ijzyd+qS1M23eVgcr+5hpVhA4pqOq1u5u/uDgYD9/E
Ik2ZlCnqPzAwqFQYgjV9VazRuqFpXu4zRiDB5NOYM0ZPc8lWk7jUZ+dTAoGBAKZ4
LESnmB2synKIRDDsZtrsheFBuj9gWHcHZ669Bw9mw1cyH8xpji/77gTUSjHUKr6f
ulVXFHZkBwqO3jTMF3LXXPhkkQEdPzsbeA3RdvSDpQ1Zz3dKWMdrYR8I45hAcscA
fFtRca0RpvfcndfRRuYPnWYh7Luvit+FMhGLzfrLAoGBALkSY5Y/AhgWOf3U83Rg
5MlYBMN0id1ae9ZnTjmFsmnNRvTmQSSdMNuqM8Gqws7iXQPWAknKrWKc2bIQ15Yz
+FLKAMd0zpRwgwh2oVLmCydUKDDQJM6WNIBYI+dNPX4NR28WckeHT3eYOZjVHw3L
f0/HiT3RZ22EI8lwkeP7E+GP
-----END PRIVATE KEY-----
7 changes: 7 additions & 0 deletions tests/signer_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,10 @@ class SignerTest(unittest.TestCase):
def setUp(self):
with open(os.path.join(os.path.dirname(__file__), "keys", "fake_mauth.priv.key"), "r") as key_file:
self.private_key = key_file.read()
with open(os.path.join(os.path.dirname(__file__), "keys", "fake_mauth.pkcs8.key"), "r") as key_file:
self.private_key_pkcs8 = key_file.read()
self.signer = Signer(APP_UUID, self.private_key, "v1,v2")
self.signer_pkcs8 = Signer(APP_UUID, self.private_key_pkcs8, "v1,v2")
self.signer_v1_only = Signer(APP_UUID, self.private_key, "v1")
self.signer_v2_only = Signer(APP_UUID, self.private_key, "v2")
self.signable = RequestSignable(**REQUEST_ATTRIBUTES)
Expand Down Expand Up @@ -137,3 +140,7 @@ def test_sign_versions_bad_version(self):
self.assertEqual(
str(exc.exception), "SIGN_VERSIONS must be comma-separated MAuth protocol versions (e.g. 'v1,v2')"
)

def test_pkcs8_and_pkcs1_signatures_match(self):
self.assertEqual(self.signer_pkcs8.signature_v1("Hello world"), self.signer.signature_v1("Hello world"))
self.assertEqual(self.signer_pkcs8.signature_v2("Hello world"), self.signer.signature_v2("Hello world"))
10 changes: 10 additions & 0 deletions tests/utils_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from mauth_client.utils import to_rsa_format

PRIVATE_KEY = load_key("priv").strip()
PRIVATE_KEY_PKCS8 = load_key("pkcs8").strip()


class TestToRsaFormat(unittest.TestCase):
Expand All @@ -20,3 +21,12 @@ def test_newlines_removed(self):
key_no_newlines = PRIVATE_KEY.replace("\n", "")
key = to_rsa_format(key_no_newlines)
self.assertEqual(key, PRIVATE_KEY)

def test_proper_format_pkcs8(self):
key = to_rsa_format(PRIVATE_KEY_PKCS8)
self.assertEqual(key, PRIVATE_KEY_PKCS8)

def test_newlines_replaced_with_spaces_pkcs8(self):
key_no_newlines = PRIVATE_KEY_PKCS8.replace("\n", " ")
key = to_rsa_format(key_no_newlines)
self.assertEqual(key, PRIVATE_KEY_PKCS8)
Loading