Skip to content

Commit f51a2c9

Browse files
committed
A couple of new functions for JWK handling.
1 parent 3fb744a commit f51a2c9

File tree

8 files changed

+165
-45
lines changed

8 files changed

+165
-45
lines changed

src/cryptojwt/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
except ImportError:
2222
pass
2323

24-
__version__ = '0.7.10'
24+
__version__ = '0.7.11'
2525

2626
logger = logging.getLogger(__name__)
2727

src/cryptojwt/jwk/hmac.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import logging
22
import os
33

4+
from . import JWK
5+
from . import USE
46
from .utils import sha256_digest
57
from .utils import sha384_digest
68
from .utils import sha512_digest
@@ -12,8 +14,6 @@
1214
from ..utils import b64d
1315
from ..utils import b64e
1416

15-
from . import JWK, USE
16-
1717
logger = logging.getLogger(__name__)
1818

1919
ALG2KEYLEN = {
@@ -23,7 +23,7 @@
2323
"HS256": 32,
2424
"HS384": 48,
2525
"HS512": 64
26-
}
26+
}
2727

2828

2929
class SYMKey(JWK):

src/cryptojwt/jwk/jwk.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,23 @@
11
import copy
2+
import json
3+
import os
24

35
from cryptography.hazmat import backends
4-
from cryptography.hazmat.primitives.asymmetric import rsa
56
from cryptography.hazmat.primitives.asymmetric import ec
7+
from cryptography.hazmat.primitives.asymmetric import rsa
68
from cryptography.hazmat.primitives.asymmetric.rsa import rsa_crt_dmp1
79
from cryptography.hazmat.primitives.asymmetric.rsa import rsa_crt_dmq1
810
from cryptography.hazmat.primitives.asymmetric.rsa import rsa_crt_iqmp
911

10-
from ..exception import MissingValue
11-
from ..exception import WrongKeyType
12-
from ..exception import UnknownKeyType
13-
from ..exception import UnsupportedAlgorithm
14-
from ..utils import base64url_to_long, b64d, as_bytes
15-
1612
from .ec import ECKey
1713
from .ec import NIST2SEC
18-
from .rsa import RSAKey
1914
from .hmac import SYMKey
20-
15+
from .rsa import RSAKey
16+
from ..exception import MissingValue
17+
from ..exception import UnknownKeyType
18+
from ..exception import UnsupportedAlgorithm
19+
from ..exception import WrongKeyType
20+
from ..utils import base64url_to_long
2121

2222
EC_PUBLIC_REQUIRED = frozenset(['crv', 'x', 'y'])
2323
EC_PUBLIC = EC_PUBLIC_REQUIRED
@@ -173,3 +173,18 @@ def jwk_wrap(key, use="", kid=""):
173173

174174
kspec.serialize()
175175
return kspec
176+
177+
178+
def dump_jwk(filename, key):
179+
"""Writes a RSAKey, ECKey or SYMKey instance as a JWK to a file."""
180+
with open(filename, 'w') as fp:
181+
fp.write(json.dumps(key.to_dict()))
182+
183+
184+
def import_jwk(filename):
185+
"""Reads a JWK from a file and converts it into the appropriate key class instance."""
186+
if os.path.isfile(filename):
187+
with open(filename) as jwk_file:
188+
jwk_dict = json.loads(jwk_file.read())
189+
return key_from_jwk_dict(jwk_dict)
190+
return None

src/cryptojwt/jws/hmac.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1-
import sys
2-
1+
from cryptography.hazmat.backends import default_backend
32
from cryptography.hazmat.primitives import hashes
43
from cryptography.hazmat.primitives import hmac
5-
from cryptography.hazmat.backends import default_backend
64

75
from . import Signer
8-
96
from ..exception import Unsupported
107

118

@@ -50,5 +47,3 @@ def verify(self, msg, sig, key):
5047
return True
5148
except:
5249
return False
53-
54-

src/cryptojwt/key_bundle.py

Lines changed: 68 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import requests
99

10+
from cryptojwt.jwk.ec import NIST2SEC
1011
from cryptojwt.jwk.hmac import new_sym_key
1112
from .exception import DeSerializationNotPossible
1213
from .exception import JWKException
@@ -15,6 +16,8 @@
1516
from .jwk.ec import ECKey
1617
from .jwk.ec import new_ec_key
1718
from .jwk.hmac import SYMKey
19+
from .jwk.jwk import dump_jwk
20+
from .jwk.jwk import import_jwk
1821
from .jwk.rsa import RSAKey
1922
from .jwk.rsa import import_private_rsa_key_from_file
2023
from .jwk.rsa import new_rsa_key
@@ -67,7 +70,7 @@ def rsa_init(spec):
6770
Example of specification::
6871
{'size':2048, 'use': ['enc', 'sig'] }
6972
70-
Using the spec above 2 RSA keys would be minted, one for
73+
Using the spec above 2 RSA keys would be minted, one for
7174
encryption and one for signing.
7275
:param spec:
7376
:return: KeyBundle
@@ -439,8 +442,8 @@ def get(self, typ="", only_active=True):
439442
def keys(self):
440443
"""
441444
Return all keys after having updated them
442-
443-
:return: List of all keys
445+
446+
:return: List of all keys
444447
"""
445448
self._uptodate()
446449

@@ -462,7 +465,7 @@ def active_keys(self):
462465
def remove_keys_by_type(self, typ):
463466
"""
464467
Remove keys that are of a specific type.
465-
468+
466469
:param typ: Type of key (rsa, ec, oct, ..)
467470
"""
468471
_typs = [typ.lower(), typ.upper()]
@@ -474,7 +477,7 @@ def __str__(self):
474477
def jwks(self, private=False):
475478
"""
476479
Create a JWKS as a JSON document
477-
480+
478481
:param private: Whether private key information should be included.
479482
:return: A JWKS JSON representation of the keys in this bundle
480483
"""
@@ -493,8 +496,8 @@ def jwks(self, private=False):
493496
def append(self, key):
494497
"""
495498
Add a key to list of keys in this bundle
496-
497-
:param key: Key to be added
499+
500+
:param key: Key to be added
498501
"""
499502
self._keys.append(key)
500503

@@ -505,8 +508,8 @@ def extend(self, keys):
505508
def remove(self, key):
506509
"""
507510
Remove a specific key from this bundle
508-
509-
:param key: The key that should be removed
511+
512+
:param key: The key that should be removed
510513
"""
511514
try:
512515
self._keys.remove(key)
@@ -700,15 +703,16 @@ def build_key_bundle(key_conf, kid_template=""):
700703
An example of such a specification::
701704
702705
keys = [
703-
{"type": "RSA", "key": "cp_keys/key.pem", "use": ["enc", "sig"]},
706+
{"type": "RSA", "key": "cp_keys/key.pem", "use": ["enc", "sig"], 'size': 2048},
704707
{"type": "EC", "crv": "P-256", "use": ["sig"], "kid": "ec.1"},
705-
{"type": "EC", "crv": "P-256", "use": ["enc"], "kid": "ec.2"}
708+
{"type": "EC", "crv": "P-256", "use": ["enc"], "kid": "ec.2"},
709+
{"type": "OCT", "bytes":}
706710
]
707711
708712
Keys in this specification are:
709713
710714
type
711-
The type of key. Presently only 'rsa' and 'ec' supported.
715+
The type of key. Presently only 'rsa', 'ec' and 'oct' supported.
712716
713717
key
714718
A name of a file where a key can be found. Only works with PEM encoded
@@ -801,6 +805,7 @@ def type_order(kd1, kd2):
801805

802806
return None
803807

808+
804809
def kid_order(kd1, kd2):
805810
"""Order key descriptions by kid."""
806811
try:
@@ -992,3 +997,54 @@ def unique_keys(keys):
992997
unique.append(k)
993998

994999
return unique
1000+
1001+
1002+
DEFAULT_SYM_KEYSIZE = 32
1003+
DEFAULT_RSA_KEYSIZE = 2048
1004+
DEFAULT_RSA_EXP = 65537
1005+
DEFAULT_EC_CURVE = 'P-256'
1006+
1007+
1008+
def key_gen(type, kid='', **kwargs):
1009+
"""
1010+
Create a key and return it as a JWK.
1011+
1012+
:param type: Key type (RSA, EC, OCT)
1013+
:param kid:
1014+
:param kwargs: key specific keyword arguments
1015+
RSA: size, exp
1016+
EC: crv
1017+
SYM: bytes
1018+
"""
1019+
if type.upper() == 'RSA':
1020+
keysize = kwargs.get("size", DEFAULT_RSA_KEYSIZE)
1021+
public_exponent = kwargs.get("exp", DEFAULT_RSA_EXP)
1022+
_key = new_rsa_key(public_exponent=public_exponent, key_size=keysize, kid=kid)
1023+
elif type.upper() == 'EC':
1024+
crv = kwargs.get("crv", DEFAULT_EC_CURVE)
1025+
if crv not in NIST2SEC:
1026+
logging.error("Unknown curve: %s", crv)
1027+
raise ValueError("Unknown curve: {}".format(crv))
1028+
_key = new_ec_key(crv=crv, kid=kid)
1029+
elif type.upper() in ["SYM", "OCT"]:
1030+
keysize = kwargs.get("bytes", 24)
1031+
randomkey = os.urandom(keysize)
1032+
_key = SYMKey(key=randomkey, kid=kid)
1033+
else:
1034+
logging.error("Unknown key type: %s", type)
1035+
raise ValueError("Unknown key type: %s".format(type))
1036+
1037+
return _key
1038+
1039+
1040+
def init_key(filename, type, kid="", **kwargs):
1041+
if os.path.isfile(filename):
1042+
_old_key = import_jwk(filename)
1043+
1044+
if _old_key.kty == type:
1045+
if not kid or _old_key.kid == kid:
1046+
return _old_key
1047+
1048+
_new_key = key_gen(type, kid, **kwargs)
1049+
dump_jwk(filename, _new_key)
1050+
return _new_key

src/cryptojwt/key_jar.py

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ def __init__(self, ca_certs=None, verify_ssl=True, keybundle_cls=KeyBundle,
3939
remove_after=3600, httpc=None):
4040
"""
4141
KeyJar init function
42-
42+
4343
:param ca_certs: CA certificates, to be used for HTTPS
4444
:param verify_ssl: Attempting SSL certificate verification
4545
:return: Keyjar instance
@@ -58,11 +58,11 @@ def __repr__(self):
5858

5959
def add_url(self, issuer, url, **kwargs):
6060
"""
61-
Add a set of keys by url. This method will create a
61+
Add a set of keys by url. This method will create a
6262
:py:class:`oidcmsg.key_bundle.KeyBundle` instance with the
6363
url as source specification. If no file format is given it's assumed
6464
that what's on the other side is a JWKS.
65-
65+
6666
:param issuer: Who issued the keys
6767
:param url: Where can the key/-s be found
6868
:param kwargs: extra parameters for instantiating KeyBundle
@@ -86,13 +86,13 @@ def add_url(self, issuer, url, **kwargs):
8686

8787
def add_symmetric(self, issuer, key, usage=None):
8888
"""
89-
Add a symmetric key. This is done by wrapping it in a key bundle
89+
Add a symmetric key. This is done by wrapping it in a key bundle
9090
cloak since KeyJar does not handle keys directly but only through
9191
key bundles.
92-
92+
9393
:param issuer: Owner of the key
94-
:param key: The key
95-
:param usage: What the key can be used for signing/signature
94+
:param key: The key
95+
:param usage: What the key can be used for signing/signature
9696
verification (sig) and/or encryption/decryption (enc)
9797
"""
9898
if issuer not in self.issuer_keys:
@@ -111,7 +111,7 @@ def add_symmetric(self, issuer, key, usage=None):
111111
def add_kb(self, issuer, kb):
112112
"""
113113
Add a key bundle and bind it to an identifier
114-
114+
115115
:param issuer: Owner of the keys in the key bundle
116116
:param kb: A :py:class:`oidcmsg.key_bundle.KeyBundle` instance
117117
"""
@@ -124,7 +124,7 @@ def __setitem__(self, issuer, val):
124124
"""
125125
Bind one or a list of key bundles to a special identifier.
126126
Will overwrite whatever was there before !!
127-
127+
128128
:param issuer: The owner of the keys in the key bundle/-s
129129
:param val: A single or a list of KeyBundle instance
130130
"""
@@ -140,7 +140,7 @@ def __setitem__(self, issuer, val):
140140
def items(self):
141141
"""
142142
Get all owner ID's and their key bundles
143-
143+
144144
:return: list of 2-tuples (Owner ID., list of KeyBundles)
145145
"""
146146
return self.issuer_keys.items()
@@ -375,9 +375,9 @@ def find(self, source, issuer):
375375

376376
def export_jwks(self, private=False, issuer="", usage=None):
377377
"""
378-
Produces a dictionary that later can be easily mapped into a
378+
Produces a dictionary that later can be easily mapped into a
379379
JSON string representing a JWKS.
380-
380+
381381
:param private: Whether it should be the private keys or the public
382382
:param issuer: The entity ID.
383383
:return: A dictionary with one key: 'keys'
@@ -650,17 +650,18 @@ def build_keyjar(key_conf, kid_template="", keyjar=None, owner=''):
650650
an existing KeyJar based on a key specification.
651651
652652
An example of such a specification::
653-
653+
654654
keys = [
655655
{"type": "RSA", "key": "cp_keys/key.pem", "use": ["enc", "sig"]},
656656
{"type": "EC", "crv": "P-256", "use": ["sig"], "kid": "ec.1"},
657657
{"type": "EC", "crv": "P-256", "use": ["enc"], "kid": "ec.2"}
658+
{"type": "OCT", "bytes": 32, "use":["sig"]}
658659
]
659660
660661
Keys in this specification are:
661662
662663
type
663-
The type of key. Presently only 'rsa' and 'ec' supported.
664+
The type of key. Presently only 'rsa', 'oct' and 'ec' supported.
664665
665666
key
666667
A name of a file where a key can be found. Only works with PEM encoded

tests/test_02_jwk.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from cryptojwt.jwk.hmac import SYMKey
2323
from cryptojwt.jwk.hmac import new_sym_key
2424
from cryptojwt.jwk.hmac import sha256_digest
25+
from cryptojwt.jwk.jwk import dump_jwk
26+
from cryptojwt.jwk.jwk import import_jwk
2527
from cryptojwt.jwk.jwk import jwk_wrap
2628
from cryptojwt.jwk.jwk import key_from_jwk_dict
2729
from cryptojwt.jwk.rsa import RSAKey
@@ -605,3 +607,15 @@ def test_mint_new_sym_key():
605607
assert key.use == 'sig'
606608
assert key.kid == 'one'
607609
assert len(key.key) == 24
610+
611+
612+
def test_dump_load():
613+
_ckey = import_rsa_key_from_cert_file(CERT)
614+
_key = jwk_wrap(_ckey, "sig", "kid1")
615+
_filename = full_path("tmp_jwk.json")
616+
617+
dump_jwk(_filename, _key)
618+
key = import_jwk(_filename)
619+
assert isinstance(key, RSAKey)
620+
assert key.kid == "kid1"
621+
assert key.use == "sig"

0 commit comments

Comments
 (0)