Skip to content

Commit a30ce79

Browse files
committed
Make X.509 certificate and key handling more alike for RSA and EC keys.
1 parent 0717022 commit a30ce79

File tree

9 files changed

+258
-133
lines changed

9 files changed

+258
-133
lines changed

src/cryptojwt/jwk/ec.py

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,15 @@
22
from cryptography.hazmat.primitives import serialization
33
from cryptography.hazmat.primitives.asymmetric import ec
44

5+
from .asym import AsymmetricKey
6+
from .x509 import import_public_key_from_pem_data
7+
from .x509 import import_public_key_from_pem_file
58
from ..exception import DeSerializationNotPossible
69
from ..exception import JWKESTException
710
from ..exception import UnsupportedECurve
811
from ..utils import as_unicode
912
from ..utils import deser
1013
from ..utils import long_to_base64
11-
from .asym import AsymmetricKey
1214

1315
# This is used to translate between the curve representation in
1416
# Cryptography and the one used by NIST (and in RFC 7518)
@@ -123,7 +125,7 @@ class ECKey(AsymmetricKey):
123125
required = ["kty", "crv", "x", "y"]
124126

125127
def __init__(
126-
self, kty="EC", alg="", use="", kid="", crv="", x="", y="", d="", **kwargs
128+
self, kty="EC", alg="", use="", kid="", crv="", x="", y="", d="", **kwargs
127129
):
128130
AsymmetricKey.__init__(self, kty, alg, use, kid, **kwargs)
129131
self.crv = crv
@@ -315,3 +317,32 @@ def new_ec_key(crv, kid="", **kwargs):
315317
_rk.add_kid()
316318

317319
return _rk
320+
321+
322+
def import_public_ec_key_from_file(filename):
323+
"""
324+
Read a public Elliptic Curve key from a PEM file.
325+
326+
:param filename: The name of the file
327+
:param passphrase: A pass phrase to use to unpack the PEM file.
328+
:return: A cryptography.hazmat.primitives.asymmetric.ec.EllipticCurvePublicKey instance
329+
"""
330+
public_key = import_public_key_from_pem_file(filename)
331+
if isinstance(public_key, ec.EllipticCurvePublicKey):
332+
return public_key
333+
else:
334+
return ValueError('Not a Elliptic Curve key')
335+
336+
337+
def import_ec_key(pem_data):
338+
"""
339+
Extract an Elliptic Curve key from a PEM-encoded X.509 certificate
340+
341+
:param pem_data: Elliptic Curve key encoded in standard form
342+
:return: ec.EllipticCurvePublicKey
343+
"""
344+
public_key = import_public_key_from_pem_data(pem_data)
345+
if isinstance(public_key, ec.EllipticCurvePublicKey):
346+
return public_key
347+
else:
348+
return ValueError('Not a Elliptic Curve key')

src/cryptojwt/jwk/jwk.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22
import json
33
import os
44

5+
from cryptography import x509
56
from cryptography.hazmat import backends
7+
from cryptography.hazmat.backends import default_backend
8+
from cryptography.hazmat.primitives import serialization
69
from cryptography.hazmat.primitives.asymmetric import ec
710
from cryptography.hazmat.primitives.asymmetric import rsa
811
from cryptography.hazmat.primitives.asymmetric.rsa import rsa_crt_dmp1

src/cryptojwt/jwk/rsa.py

Lines changed: 40 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
11
import base64
2-
import hashlib
32
import logging
43

5-
from cryptography import x509
64
from cryptography.hazmat.backends import default_backend
75
from cryptography.hazmat.primitives import serialization
86
from cryptography.hazmat.primitives.asymmetric import rsa
97

8+
from . import JWK
9+
from .asym import AsymmetricKey
10+
from .x509 import der_cert
11+
from .x509 import import_private_key_from_pem_file
12+
from .x509 import import_public_key_from_pem_data
13+
from .x509 import import_public_key_from_pem_file
14+
from .x509 import x5t_calculation
1015
from ..exception import DeSerializationNotPossible
1116
from ..exception import JWKESTException
1217
from ..exception import SerializationNotPossible
1318
from ..exception import UnsupportedKeyType
1419
from ..utils import as_unicode
15-
from ..utils import b64e
1620
from ..utils import deser
1721
from ..utils import long_to_base64
18-
from . import JWK
19-
from .asym import AsymmetricKey
2022

2123
logger = logging.getLogger(__name__)
2224

@@ -67,11 +69,11 @@ def import_private_rsa_key_from_file(filename, passphrase=None):
6769
:return: A
6870
cryptography.hazmat.primitives.asymmetric.rsa.RSAPrivateKey instance
6971
"""
70-
with open(filename, "rb") as key_file:
71-
private_key = serialization.load_pem_private_key(
72-
key_file.read(), password=passphrase, backend=default_backend()
73-
)
74-
return private_key
72+
private_key = import_private_key_from_pem_file(filename, passphrase)
73+
if isinstance(private_key, rsa.RSAPrivateKey):
74+
return private_key
75+
else:
76+
return ValueError('Not a RSA key')
7577

7678

7779
def import_public_rsa_key_from_file(filename):
@@ -80,14 +82,13 @@ def import_public_rsa_key_from_file(filename):
8082
8183
:param filename: The name of the file
8284
:param passphrase: A pass phrase to use to unpack the PEM file.
83-
:return: A
84-
cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey instance
85+
:return: A cryptography.hazmat.primitives.asymmetric.rsa.RSAPublicKey instance
8586
"""
86-
with open(filename, "rb") as key_file:
87-
public_key = serialization.load_pem_public_key(
88-
key_file.read(), backend=default_backend()
89-
)
90-
return public_key
87+
public_key = import_public_key_from_pem_file(filename)
88+
if isinstance(public_key, rsa.RSAPublicKey):
89+
return public_key
90+
else:
91+
return ValueError('Not a RSA key')
9192

9293

9394
def import_rsa_key(pem_data):
@@ -97,12 +98,11 @@ def import_rsa_key(pem_data):
9798
:param pem_data: RSA key encoded in standard form
9899
:return: rsa.RSAPublicKey instance
99100
"""
100-
if not pem_data.startswith(PREFIX):
101-
pem_data = bytes("{}\n{}\n{}".format(PREFIX, pem_data, POSTFIX), "utf-8")
101+
public_key = import_public_key_from_pem_data(pem_data)
102+
if isinstance(public_key, rsa.RSAPublicKey):
103+
return public_key
102104
else:
103-
pem_data = bytes(pem_data, "utf-8")
104-
cert = x509.load_pem_x509_certificate(pem_data, default_backend())
105-
return cert.public_key()
105+
return ValueError('Not a RSA key')
106106

107107

108108
def import_rsa_key_from_cert_file(pem_file):
@@ -182,46 +182,6 @@ def rsa_construct_private(numbers):
182182
return rprivn.private_key(default_backend())
183183

184184

185-
def der_cert(der_data):
186-
"""
187-
Load a DER encoded certificate
188-
189-
:param der_data: DER-encoded certificate
190-
:return: A cryptography.x509.certificate instance
191-
"""
192-
if isinstance(der_data, str):
193-
der_data = bytes(der_data, "utf-8")
194-
return x509.load_der_x509_certificate(der_data, default_backend())
195-
196-
197-
def load_x509_cert(url, httpc, spec2key, **get_args):
198-
"""
199-
Get and transform a X509 cert into a key.
200-
201-
:param url: Where the X509 cert can be found
202-
:param httpc: HTTP client to use for fetching
203-
:param spec2key: A dictionary over keys already seen
204-
:param get_args: Extra key word arguments to the HTTP GET request
205-
:return: List of 2-tuples (keytype, key)
206-
"""
207-
try:
208-
r = httpc("GET", url, allow_redirects=True, **get_args)
209-
if r.status_code == 200:
210-
cert = str(r.text)
211-
try:
212-
public_key = spec2key[cert] # If I've already seen it
213-
except KeyError:
214-
public_key = import_rsa_key(cert)
215-
spec2key[cert] = public_key
216-
if isinstance(public_key, rsa.RSAPublicKey):
217-
return {"rsa": public_key}
218-
else:
219-
raise Exception("HTTP Get error: %s" % r.status_code)
220-
except Exception as err: # not a RSA key
221-
logger.warning("Can't load key: %s" % err)
222-
return []
223-
224-
225185
def cmp_public_numbers(pn1, pn2):
226186
"""
227187
Compare 2 sets of public numbers. These is a way to compare
@@ -255,22 +215,6 @@ def cmp_private_numbers(pn1, pn2):
255215
return True
256216

257217

258-
def x5t_calculation(cert):
259-
"""
260-
base64url-encoded SHA-1 thumbprint (a.k.a. digest) of the DER
261-
encoding of an X.509 certificate.
262-
263-
:param cert: DER encoded X.509 certificate
264-
:return: x5t value
265-
"""
266-
if isinstance(cert, str):
267-
der_cert = base64.b64decode(cert.encode("ascii"))
268-
else:
269-
der_cert = base64.b64decode(cert)
270-
271-
return b64e(hashlib.sha1(der_cert).digest())
272-
273-
274218
class RSAKey(AsymmetricKey):
275219
"""
276220
JSON Web key representation of a RSA key
@@ -303,24 +247,24 @@ class RSAKey(AsymmetricKey):
303247
required = ["kty", "n", "e"]
304248

305249
def __init__(
306-
self,
307-
kty="RSA",
308-
alg="",
309-
use="",
310-
kid="",
311-
x5c=None,
312-
x5t="",
313-
x5u="",
314-
n="",
315-
e="",
316-
d="",
317-
p="",
318-
q="",
319-
dp="",
320-
dq="",
321-
di="",
322-
qi="",
323-
**kwargs
250+
self,
251+
kty="RSA",
252+
alg="",
253+
use="",
254+
kid="",
255+
x5c=None,
256+
x5t="",
257+
x5u="",
258+
n="",
259+
e="",
260+
d="",
261+
p="",
262+
q="",
263+
dp="",
264+
dq="",
265+
di="",
266+
qi="",
267+
**kwargs
324268
):
325269
AsymmetricKey.__init__(self, kty, alg, use, kid, x5c, x5t, x5u, **kwargs)
326270
self.n = n

src/cryptojwt/jwk/x509.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import base64
2+
import hashlib
3+
import logging
4+
5+
from cryptography import x509
6+
from cryptography.hazmat.backends import default_backend
7+
from cryptography.hazmat.primitives import serialization
8+
from cryptography.hazmat.primitives.asymmetric import ec
9+
from cryptography.hazmat.primitives.asymmetric import rsa
10+
11+
from cryptojwt.utils import b64e
12+
13+
logger = logging.getLogger(__name__)
14+
15+
16+
def import_public_key_from_pem_file(filename):
17+
"""
18+
Read a public RSA key from a PEM file.
19+
20+
:param filename: The name of the file
21+
:param passphrase: A pass phrase to use to unpack the PEM file.
22+
:return: A public key instance
23+
"""
24+
with open(filename, "rb") as key_file:
25+
public_key = serialization.load_pem_public_key(
26+
key_file.read(), backend=default_backend()
27+
)
28+
return public_key
29+
30+
31+
def import_private_key_from_pem_file(filename, passphrase=None):
32+
"""
33+
Read a private RSA key from a PEM file.
34+
35+
:param filename: The name of the file
36+
:param passphrase: A pass phrase to use to unpack the PEM file.
37+
:return: A private key instance
38+
"""
39+
with open(filename, "rb") as key_file:
40+
private_key = serialization.load_pem_private_key(
41+
key_file.read(), password=passphrase, backend=default_backend()
42+
)
43+
return private_key
44+
45+
46+
PREFIX = "-----BEGIN CERTIFICATE-----"
47+
POSTFIX = "-----END CERTIFICATE-----"
48+
49+
50+
def import_public_key_from_pem_data(pem_data):
51+
"""
52+
Extract an RSA key from a PEM-encoded X.509 certificate
53+
54+
:param pem_data: RSA key encoded in standard form
55+
:return: rsa.RSAPublicKey instance
56+
"""
57+
if not pem_data.startswith(PREFIX):
58+
pem_data = bytes("{}\n{}\n{}".format(PREFIX, pem_data, POSTFIX), "utf-8")
59+
else:
60+
pem_data = bytes(pem_data, "utf-8")
61+
cert = x509.load_pem_x509_certificate(pem_data, default_backend())
62+
return cert.public_key()
63+
64+
65+
def der_cert(der_data):
66+
"""
67+
Load a DER encoded certificate
68+
69+
:param der_data: DER-encoded certificate
70+
:return: A cryptography.x509.certificate instance
71+
"""
72+
if isinstance(der_data, str):
73+
der_data = bytes(der_data, "utf-8")
74+
return x509.load_der_x509_certificate(der_data, default_backend())
75+
76+
77+
def load_x509_cert(url, httpc, spec2key, **get_args):
78+
"""
79+
Get and transform a X509 cert into a key.
80+
81+
:param url: Where the X509 cert can be found
82+
:param httpc: HTTP client to use for fetching
83+
:param spec2key: A dictionary over keys already seen
84+
:param get_args: Extra key word arguments to the HTTP GET request
85+
:return: List of 2-tuples (keytype, key)
86+
"""
87+
try:
88+
r = httpc("GET", url, allow_redirects=True, **get_args)
89+
if r.status_code == 200:
90+
cert = str(r.text)
91+
try:
92+
public_key = spec2key[cert] # If I've already seen it
93+
except KeyError:
94+
public_key = import_public_key_from_pem_data(cert)
95+
spec2key[cert] = public_key
96+
97+
if isinstance(public_key, rsa.RSAPublicKey):
98+
return {"rsa": public_key}
99+
elif isinstance(public_key, ec.EllipticCurvePublicKey):
100+
return {'ec': public_key}
101+
else:
102+
raise Exception("HTTP Get error: %s" % r.status_code)
103+
except Exception as err: # not a RSA key
104+
logger.warning("Can't load key: %s" % err)
105+
return []
106+
107+
108+
def x5t_calculation(cert):
109+
"""
110+
base64url-encoded SHA-1 thumbprint (a.k.a. digest) of the DER
111+
encoding of an X.509 certificate.
112+
113+
:param cert: DER encoded X.509 certificate
114+
:return: x5t value
115+
"""
116+
if isinstance(cert, str):
117+
der_cert = base64.b64decode(cert.encode("ascii"))
118+
else:
119+
der_cert = base64.b64decode(cert)
120+
121+
return b64e(hashlib.sha1(der_cert).digest())
122+

src/cryptojwt/jwx.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from .jwk.jwk import key_from_jwk_dict
1212
from .jwk.rsa import RSAKey
1313
from .jwk.rsa import import_rsa_key
14-
from .jwk.rsa import load_x509_cert
14+
from .jwk.x509 import load_x509_cert
1515
from .utils import as_bytes
1616
from .utils import as_unicode
1717
from .utils import b64d

0 commit comments

Comments
 (0)