Skip to content

Commit e3ffa7d

Browse files
committed
feat: add RFC 9278 JWK thumbprint URI
1 parent 727ac55 commit e3ffa7d

9 files changed

Lines changed: 62 additions & 10 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ It follows RFCs with extensible API. The module has implementations of:
5353
- RFC7797: [JSON Web Signature (JWS) Unencoded Payload Option](https://jose.authlib.org/en/dev/guide/jws/#rfc7797)
5454
- RFC8037: ``OKP`` Key and ``EdDSA`` algorithm
5555
- RFC8812: ``ES256K`` algorithm
56+
- RFC9278: [JWK Thumbprint URI](https://jose.authlib.org/en/api/jwk/#joserfc.jwk.thumbprint_uri)
5657

5758
And draft RFCs implementation of:
5859

README.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ This package contains implementation of:
1616
- RFC7797: JSON Web Signature (JWS) Unencoded Payload Option
1717
- RFC8037: OKP Key and EdDSA algorithm
1818
- RFC8812: ES256K algorithm
19+
- RFC9278: JWK Thumbprint URI
1920

2021
And draft RFCs implementation of:
2122

docs/api/jwk.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ This part of the documentation covers all the interfaces of ``joserfc.jwk``.
77

88
.. automodule:: joserfc.jwk
99
:members:
10+
:inherited-members:

src/joserfc/_rfc7517/models.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
from collections.abc import KeysView
44
from abc import ABCMeta, abstractmethod
55
from .types import DictKey, AnyKey, KeyParameters
6-
from .._rfc7638 import thumbprint
6+
from .._rfc7638 import calculate_thumbprint
7+
from .._rfc9278 import concat_thumbprint_uri
78
from ..registry import (
89
KeyParameterRegistryDict,
910
JWK_PARAMETER_REGISTRY,
@@ -167,7 +168,14 @@ def thumbprint(self) -> str:
167168
defined in RFC7638."""
168169
fields = [k for k in self.value_registry if self.value_registry[k].required]
169170
fields.append("kty")
170-
return thumbprint(self.dict_value, fields, self.thumbprint_digest_method)
171+
data = {key: self.dict_value[key] for key in fields}
172+
return calculate_thumbprint(data, self.thumbprint_digest_method)
173+
174+
def thumbprint_uri(self) -> str:
175+
"""Call this method will generate the thumbprint URI
176+
defined in RFC9278."""
177+
value = self.thumbprint()
178+
return concat_thumbprint_uri(value, self.thumbprint_digest_method)
171179

172180
def as_dict(self, private: t.Optional[bool] = None, **params: t.Any) -> DictKey:
173181
"""Output this key to a JWK format (in dict). By default, it will return

src/joserfc/_rfc7638/__init__.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,14 @@
55
from ..util import to_bytes, urlsafe_b64encode
66

77

8-
def thumbprint(
9-
dict_value: t.Dict[str, t.Any],
10-
fields: t.List[str],
8+
def calculate_thumbprint(
9+
value: t.Dict[str, t.Any],
1110
digest_method: t.Literal["sha256", "sha384", "sha512"] = "sha256",
1211
) -> str:
13-
sorted_fields = sorted(fields)
14-
12+
sorted_fields = sorted(value.keys())
1513
data = OrderedDict()
1614
for k in sorted_fields:
17-
data[k] = dict_value[k]
18-
15+
data[k] = value[k]
1916
json_data = json.dumps(data, ensure_ascii=True, separators=(",", ":"))
2017
hash_value = hashlib.new(digest_method, to_bytes(json_data))
2118
digest_data = hash_value.digest()

src/joserfc/_rfc9278/__init__.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import typing as t
2+
from .._rfc7638 import calculate_thumbprint
3+
4+
JWK_THUMBPRINT_URN = "urn:ietf:params:oauth:jwk-thumbprint"
5+
6+
7+
def calculate_thumbprint_uri(
8+
value: t.Dict[str, t.Any],
9+
digest_method: t.Literal["sha256", "sha384", "sha512"] = "sha256",
10+
) -> str:
11+
"""Calculate JWK thumbprint URI, defined by RFC9278."""
12+
value = calculate_thumbprint(value, digest_method=digest_method)
13+
return concat_thumbprint_uri(value, digest_method=digest_method)
14+
15+
16+
def concat_thumbprint_uri(value: str, digest_method: t.Literal["sha256", "sha384", "sha512"]) -> str:
17+
method = digest_method.replace("sha", "sha-")
18+
return f"{JWK_THUMBPRINT_URN}:{method}:{value}"

src/joserfc/jwk.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from ._rfc7518.ec_key import ECKey as ECKey
1313
from ._rfc8037.okp_key import OKPKey as OKPKey
1414
from ._rfc8812 import register_secp256k1
15+
from ._rfc9278 import calculate_thumbprint_uri as thumbprint_uri
1516
from .registry import Header
1617

1718

@@ -36,6 +37,7 @@
3637
"guess_key",
3738
"import_key",
3839
"generate_key",
40+
"thumbprint_uri",
3941
]
4042

4143
register_secp256k1()

tests/jwk/test_key_methods.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
# trigger register_key_set
44
import joserfc.jws # noqa: F401
5-
from joserfc.jwk import guess_key, import_key, generate_key
5+
from joserfc.jwk import guess_key, import_key, generate_key, thumbprint_uri
66
from joserfc.jwk import KeySet, OctKey, RSAKey, ECKey, OKPKey
77
from joserfc.errors import (
88
UnsupportedKeyAlgorithmError,
@@ -126,3 +126,15 @@ def test_check_ops(self):
126126

127127
def test_import_without_kty(self):
128128
self.assertRaises(MissingKeyTypeError, import_key, {})
129+
130+
def test_thumbprint_uri(self):
131+
value = thumbprint_uri(
132+
{
133+
"kty": "EC",
134+
"crv": "P-256",
135+
"x": "jJ6Flys3zK9jUhnOHf6G49Dyp5hah6CNP84-gY-n9eo",
136+
"y": "nhI6iD5eFXgBTLt_1p3aip-5VbZeMhxeFSpjfEAf7Ww",
137+
}
138+
)
139+
expected = "urn:ietf:params:oauth:jwk-thumbprint:sha-256:w9eYdC6_s_tLQ8lH6PUpc0mddazaqtPgeC2IgWDiqY8"
140+
self.assertEqual(value, expected)

tests/jwk/test_oct_key.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,18 @@ def test_import_key_from_dict(self):
3737
key = OctKey.import_key(data)
3838
self.assertEqual(key.as_dict(), data)
3939

40+
def test_thumbprint_uri(self):
41+
data = {
42+
"kty": "oct",
43+
"alg": "A128KW",
44+
"k": "GawgguFyGrWKav7AX4VKUg",
45+
"use": "sig",
46+
"key_ops": ["sign", "verify"],
47+
}
48+
key = OctKey.import_key(data)
49+
thumbprint = "k1JnWRfC-5zzmL72vXIuBgTLfVROXBakS4OmGcrMCoc"
50+
self.assertEqual(key.thumbprint_uri(), f"urn:ietf:params:oauth:jwk-thumbprint:sha-256:{thumbprint}")
51+
4052
def test_import_missing_k(self):
4153
data = {
4254
"kty": "oct",

0 commit comments

Comments
 (0)