Skip to content

Commit 1e79713

Browse files
authored
Merge pull request #151 from IdentityPython/sdjwt2
Changes necessary to allow the SD JWT package to build on this.
2 parents e88715c + a24f6d9 commit 1e79713

File tree

11 files changed

+79
-53
lines changed

11 files changed

+79
-53
lines changed

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ exclude_lines = [
2222

2323
[tool.poetry]
2424
name = "cryptojwt"
25-
version = "1.8.3"
25+
version = "1.8.4"
2626
description = "Python implementation of JWT, JWE, JWS and JWK"
2727
authors = ["Roland Hedberg <roland@catalogix.se>"]
2828
license = "Apache-2.0"

src/cryptojwt/jwe/jwe_ec.py

-1
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,6 @@ def encrypt(self, key=None, iv="", cek="", **kwargs):
213213
return jwe.pack(parts=[iv, ctxt, tag])
214214

215215
def decrypt(self, token=None, **kwargs):
216-
217216
if isinstance(token, JWEnc):
218217
jwe = token
219218
else:

src/cryptojwt/jwk/__init__.py

-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ class JWK(object):
3131
def __init__(
3232
self, kty="", alg="", use="", kid="", x5c=None, x5t="", x5u="", key_ops=None, **kwargs
3333
):
34-
3534
self.extra_args = kwargs
3635

3736
# want kty, alg, use and kid to be strings

src/cryptojwt/jwk/ec.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ def ec_construct_public(num):
5555

5656
def ec_construct_private(num):
5757
"""
58-
Given a set of values on public and private attributes build a elliptic
58+
Given a set of values on public and private attributes build an elliptic
5959
curve private key instance.
6060
6161
:param num: A dictionary with public and private attributes and their values

src/cryptojwt/jwk/okp.py

-1
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,6 @@ def cmp_keys(a, b, key_type):
321321

322322

323323
def new_okp_key(crv, kid="", **kwargs):
324-
325324
_key = OKP_CRV2PRIVATE[crv].generate()
326325

327326
_rk = OKPKey(priv_key=_key, kid=kid, **kwargs)

src/cryptojwt/jws/jws.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,9 @@ def sign_compact(self, keys=None, protected=None, **kwargs):
120120

121121
key, xargs, _alg = self.alg_keys(keys, "sig", protected)
122122

123-
if "typ" in self:
124-
xargs["typ"] = self["typ"]
123+
for param in ["typ"]:
124+
if param in self:
125+
xargs[param] = self[param]
125126

126127
_headers.update(xargs)
127128
jwt = JWSig(**_headers)

src/cryptojwt/jwt.py

+50-28
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
import time
55
import uuid
66
from json import JSONDecodeError
7+
from typing import Dict
8+
from typing import List
9+
from typing import MutableMapping
10+
from typing import Optional
711

812
from .exception import HeaderError
913
from .exception import VerificationError
@@ -79,25 +83,26 @@ class JWT:
7983
def __init__(
8084
self,
8185
key_jar=None,
82-
iss="",
83-
lifetime=0,
84-
sign=True,
85-
sign_alg="RS256",
86-
encrypt=False,
87-
enc_enc="A128GCM",
88-
enc_alg="RSA-OAEP-256",
89-
msg_cls=None,
90-
iss2msg_cls=None,
91-
skew=15,
92-
allowed_sign_algs=None,
93-
allowed_enc_algs=None,
94-
allowed_enc_encs=None,
95-
allowed_max_lifetime=None,
96-
zip="",
86+
iss: str = "",
87+
lifetime: int = 0,
88+
sign: bool = True,
89+
sign_alg: str = "RS256",
90+
encrypt: bool = False,
91+
enc_enc: str = "A128GCM",
92+
enc_alg: str = "RSA-OAEP-256",
93+
msg_cls: Optional[MutableMapping] = None,
94+
iss2msg_cls: Optional[Dict[str, str]] = None,
95+
skew: Optional[int] = 15,
96+
allowed_sign_algs: Optional[List[str]] = None,
97+
allowed_enc_algs: Optional[List[str]] = None,
98+
allowed_enc_encs: Optional[List[str]] = None,
99+
allowed_max_lifetime: Optional[int] = None,
100+
zip: Optional[str] = "",
101+
typ2msg_cls: Optional[Dict] = None,
97102
):
98103
self.key_jar = key_jar # KeyJar instance
99104
self.iss = iss # My identifier
100-
self.lifetime = lifetime # default life time of the signature
105+
self.lifetime = lifetime # default lifetime of the signature
101106
self.sign = sign # default signing or not
102107
self.alg = sign_alg # default signing algorithm
103108
self.encrypt = encrypt # default encrypting or not
@@ -107,6 +112,7 @@ def __init__(
107112
self.with_jti = False # If a jti should be added
108113
# A map between issuers and the message classes they use
109114
self.iss2msg_cls = iss2msg_cls or {}
115+
self.typ2msg_cls = typ2msg_cls or {}
110116
# Allowed time skew
111117
self.skew = skew
112118
# When verifying/decrypting
@@ -206,16 +212,30 @@ def pack_key(self, issuer_id="", kid=""):
206212

207213
return keys[0] # Might be more then one if kid == ''
208214

209-
def pack(self, payload=None, kid="", issuer_id="", recv="", aud=None, iat=None, **kwargs):
215+
def message(self, signing_key, **kwargs):
216+
return json.dumps(kwargs)
217+
218+
def pack(
219+
self,
220+
payload: Optional[dict] = None,
221+
kid: Optional[str] = "",
222+
issuer_id: Optional[str] = "",
223+
recv: Optional[str] = "",
224+
aud: Optional[str] = None,
225+
iat: Optional[int] = None,
226+
jws_headers: Optional[Dict[str, str]] = None,
227+
**kwargs
228+
) -> str:
210229
"""
211230
212231
:param payload: Information to be carried as payload in the JWT
213232
:param kid: Key ID
214-
:param issuer_id: The owner of the the keys that are to be used for signing
233+
:param issuer_id: The owner of the keys that are to be used for signing
215234
:param recv: The intended immediate receiver
216235
:param aud: Intended audience for this JWS/JWE, not expected to
217236
contain the recipient.
218237
:param iat: Override issued at (default current timestamp)
238+
:param jws_headers: JWS headers
219239
:param kwargs: Extra keyword arguments
220240
:return: A signed or signed and encrypted Json Web Token
221241
"""
@@ -249,10 +269,12 @@ def pack(self, payload=None, kid="", issuer_id="", recv="", aud=None, iat=None,
249269
else:
250270
_key = None
251271

252-
_jws = JWS(json.dumps(_args), alg=self.alg)
253-
_sjwt = _jws.sign_compact([_key])
272+
jws_headers = jws_headers or {}
273+
274+
_jws = JWS(self.message(signing_key=_key, **_args), alg=self.alg)
275+
_sjwt = _jws.sign_compact([_key], protected=jws_headers)
254276
else:
255-
_sjwt = json.dumps(_args)
277+
_sjwt = self.message(signing_key=None, **_args)
256278

257279
if _encrypt:
258280
if not self.sign:
@@ -300,8 +322,7 @@ def verify_profile(msg_cls, info, **kwargs):
300322
:return: The verified message as a msg_cls instance.
301323
"""
302324
_msg = msg_cls(**info)
303-
if not _msg.verify(**kwargs):
304-
raise VerificationError()
325+
_msg.verify(**kwargs)
305326
return _msg
306327

307328
def unpack(self, token, timestamp=None):
@@ -373,11 +394,12 @@ def unpack(self, token, timestamp=None):
373394
if self.msg_cls:
374395
_msg_cls = self.msg_cls
375396
else:
376-
try:
377-
# try to find a issuer specific message class
378-
_msg_cls = self.iss2msg_cls[_info["iss"]]
379-
except KeyError:
380-
_msg_cls = None
397+
_msg_cls = None
398+
# try to find an issuer specific message class
399+
if "iss" in _info:
400+
_msg_cls = self.iss2msg_cls.get(_info["iss"])
401+
if not _msg_cls and _jws_header and "typ" in _jws_header:
402+
_msg_cls = self.typ2msg_cls.get(_jws_header["typ"])
381403

382404
timestamp = timestamp or utc_time_sans_frac()
383405

src/cryptojwt/jwx.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ class JWx:
5050
:return: A class instance
5151
"""
5252

53-
args = ["alg", "jku", "jwk", "x5u", "x5t", "x5c", "kid", "typ", "cty", "crit"]
53+
args = ["alg", "jku", "jwk", "x5u", "x5t", "x5c", "kid", "typ", "cty", "crit", "trust_chain"]
5454

5555
def __init__(self, msg=None, with_digest=False, httpc=None, **kwargs):
5656
self.msg = msg

src/cryptojwt/key_bundle.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -566,7 +566,6 @@ def update(self):
566566
:return: True if update was ok or False if we encountered an error during update.
567567
"""
568568
if self.source:
569-
570569
try:
571570
if self.local:
572571
if self.fileformat in ["jwks", "jwk"]:
@@ -681,7 +680,7 @@ def append(self, key):
681680

682681
@keys_writer
683682
def extend(self, keys):
684-
"""Add a key to the list of keys."""
683+
"""Add a list of keys to the list of keys."""
685684
self._keys.extend(keys)
686685

687686
@keys_writer

src/cryptojwt/key_jar.py

+7-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55

66
from requests import request
77

8+
from cryptojwt.jwk import JWK
9+
810
from .exception import IssuerNotFound
911
from .jwe.jwe import alg2keytype as jwe_alg2keytype
1012
from .jws.utils import alg2keytype as jws_alg2keytype
@@ -161,6 +163,11 @@ def add_kb(self, issuer_id, kb):
161163
issuer.add_kb(kb)
162164
self._issuers[issuer_id] = issuer
163165

166+
def add_keys(self, issuer_id: str, keys: List[JWK], **kwargs):
167+
_kb = KeyBundle(**kwargs)
168+
_kb.extend(keys)
169+
self.add_kb(issuer_id, _kb)
170+
164171
@deprecated_alias(issuer="issuer_id", owner="issuer_id")
165172
def get(self, key_use, key_type="", issuer_id="", kid=None, **kwargs):
166173
"""
@@ -475,7 +482,6 @@ def _add_key(
475482
no_kid_issuer=None,
476483
allow_missing_kid=False,
477484
):
478-
479485
_issuer = self._get_issuer(issuer_id)
480486
if _issuer is None:
481487
logger.error('Issuer "{}" not in keyjar'.format(issuer_id))

tests/test_09_jwt.py

+15-14
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22

33
import pytest
44

5-
from cryptojwt.exception import IssuerNotFound
65
from cryptojwt.jws.exception import NoSuitableSigningKeys
76
from cryptojwt.jwt import JWT
87
from cryptojwt.jwt import VerificationError
@@ -136,19 +135,6 @@ def test_jwt_pack_and_unpack_max_lifetime_exceeded():
136135
_ = bob.unpack(_jwt)
137136

138137

139-
def test_jwt_pack_and_unpack_max_lifetime_exceeded():
140-
lifetime = 3600
141-
alice = JWT(key_jar=ALICE_KEY_JAR, iss=ALICE, sign_alg="RS256", lifetime=lifetime)
142-
payload = {"sub": "sub"}
143-
_jwt = alice.pack(payload=payload)
144-
145-
bob = JWT(
146-
key_jar=BOB_KEY_JAR, iss=BOB, allowed_sign_algs=["RS256"], allowed_max_lifetime=lifetime - 1
147-
)
148-
with pytest.raises(VerificationError):
149-
_ = bob.unpack(_jwt)
150-
151-
152138
def test_jwt_pack_and_unpack_timestamp():
153139
lifetime = 3600
154140
alice = JWT(key_jar=ALICE_KEY_JAR, iss=ALICE, sign_alg="RS256", lifetime=lifetime)
@@ -258,6 +244,7 @@ class DummyMsg(object):
258244
def __init__(self, **kwargs):
259245
for key, val in kwargs.items():
260246
setattr(self, key, val)
247+
self.jws_headers = {}
261248

262249
def verify(self, **kwargs):
263250
return True
@@ -331,3 +318,17 @@ def test_eddsa_jwt():
331318
kj.add_kb(ISSUER, KeyBundle(JWKS_DICT))
332319
jwt = JWT(key_jar=kj)
333320
_ = jwt.unpack(JWT_TEST, timestamp=1655278809)
321+
322+
323+
def test_extra_headers():
324+
_kj = KeyJar()
325+
_kj.add_symmetric(ALICE, "hemligt ordsprak", usage=["sig"])
326+
327+
alice = JWT(key_jar=_kj, iss=ALICE, sign_alg="HS256")
328+
payload = {"sub": "sub2"}
329+
_jwt = alice.pack(payload=payload, jws_headers={"xtra": "header", "typ": "dummy"})
330+
331+
bob = JWT(key_jar=_kj, iss=BOB, sign_alg="HS256", typ2msg_cls={"dummy": DummyMsg})
332+
info = bob.unpack(_jwt)
333+
assert isinstance(info, DummyMsg)
334+
assert set(info.jws_header.keys()) == {"xtra", "typ", "alg", "kid"}

0 commit comments

Comments
 (0)