Skip to content

Commit 6ced08f

Browse files
authored
Merge pull request #574 from dajiaji/add-deterministic-encoding
Add option for Deterministic encoded protected and unprotected headers
2 parents 938c2d1 + aef8293 commit 6ced08f

3 files changed

Lines changed: 130 additions & 2 deletions

File tree

cwt/cose.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
from .recipient_interface import RecipientInterface
2222
from .recipients import Recipients
2323
from .signer import Signer
24-
from .utils import to_cose_header
24+
from .utils import sort_keys_for_deterministic_encoding, to_cose_header
2525

2626

2727
class COSE(CBORProcessor):
@@ -36,6 +36,7 @@ def __init__(
3636
kid_auto_inclusion: bool = False,
3737
verify_kid: bool = False,
3838
ca_certs: str = "",
39+
deterministic_header: bool = False,
3940
):
4041
if not isinstance(alg_auto_inclusion, bool):
4142
raise ValueError("alg_auto_inclusion should be bool.")
@@ -58,13 +59,18 @@ def __init__(
5859
for _, _, der_bytes in pem.unarmor(f.read(), multiple=True):
5960
self._ca_certs.append(der_bytes)
6061

62+
if not isinstance(deterministic_header, bool):
63+
raise ValueError("deterministic_header should be bool.")
64+
self._deterministic_header = deterministic_header
65+
6166
@classmethod
6267
def new(
6368
cls,
6469
alg_auto_inclusion: bool = False,
6570
kid_auto_inclusion: bool = False,
6671
verify_kid: bool = False,
6772
ca_certs: str = "",
73+
deterministic_header: bool = False,
6874
):
6975
"""
7076
Constructor.
@@ -80,8 +86,10 @@ def new(
8086
of trusted root certificates. You should specify private CA
8187
certificates in your target system. There should be no need to
8288
use the public CA certificates for the Web PKI.
89+
deterministic_header(bool): The indicator whether the protected and unprotected
90+
headers will be deterministically encoded defined in section 4.2.1 of RFC 8949.
8391
"""
84-
return cls(alg_auto_inclusion, kid_auto_inclusion, verify_kid, ca_certs)
92+
return cls(alg_auto_inclusion, kid_auto_inclusion, verify_kid, ca_certs, deterministic_header)
8593

8694
@property
8795
def alg_auto_inclusion(self) -> bool:
@@ -561,6 +569,11 @@ def _encode_headers(
561569
if self._kid_auto_inclusion and key.kid:
562570
u[4] = key.kid
563571

572+
# sort the key for deterministic encoding
573+
if self._deterministic_header:
574+
p = sort_keys_for_deterministic_encoding(p)
575+
u = sort_keys_for_deterministic_encoding(u)
576+
564577
# Check the protected header is empty if the algorithm is non AEAD (AES-CBC or AES-CTR)
565578
# because section 4 of RFC9459 says "The 'protected' header MUST be a zero-length byte string."
566579
alg = p[1] if 1 in p else u.get(1, 0)

cwt/utils.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,3 +344,7 @@ def to_recipient_context(alg: int, u: Dict[int, Any], context: Union[List[Any],
344344
else:
345345
ctx[i].append(v)
346346
return ctx
347+
348+
349+
def sort_keys_for_deterministic_encoding(d: Dict[int, Any]) -> Dict[int, Any]:
350+
return {k: v for k, v in sorted(d.items(), key=lambda kv: cbor2.dumps(kv[0]))}
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
"""
2+
Tests for Core Deterministic Encoding.
3+
"""
4+
5+
import cbor2
6+
import pytest
7+
8+
from cwt import COSE, COSEKey
9+
from cwt.const import COSE_ALGORITHMS_MAC, COSE_ALGORITHMS_SIGNATURE
10+
from cwt.utils import sort_keys_for_deterministic_encoding
11+
12+
13+
class TestDeterministicEncoding:
14+
"""
15+
Tests for Core Deterministic Encoding
16+
"""
17+
18+
def test_deterministically_sorted_dict(self):
19+
expected = {}
20+
expected[0] = 0 # Reserved
21+
expected[1] = 0 # alg
22+
expected[4] = 0 # kid
23+
expected[5] = 0 # IV
24+
expected[7] = 0 # counter signature
25+
expected[11] = 0 # Countersignature version 2
26+
expected[12] = 0 # Countersignature0 version 2
27+
expected[15] = 0 # CWT Claims
28+
expected[33] = 0 # x5chain
29+
expected[-1] = 0 # (max of COSE Header Algorithm Parameters resistry)
30+
expected[-65536] = 0 # (min of COSE Header Algorithm Parameters resistry)
31+
expected[-65537] = 0 # (max of Reserved for Private Use)
32+
33+
d = {}
34+
d[4] = 0
35+
d[-1] = 0
36+
d[33] = 0
37+
d[7] = 0
38+
d[11] = 0
39+
d[1] = 0
40+
d[5] = 0
41+
d[-65537] = 0
42+
d[0] = 0
43+
d[15] = 0
44+
d[12] = 0
45+
d[-65536] = 0
46+
47+
assert expected == sort_keys_for_deterministic_encoding(d)
48+
49+
def test_deterministic_cose_sign_binary(self):
50+
sign_jwk = {
51+
"kty": "EC",
52+
"kid": "11",
53+
"alg": "ES256",
54+
"crv": "P-256",
55+
"x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
56+
"y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4",
57+
"d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM",
58+
}
59+
sign_key = COSEKey.from_jwk(sign_jwk)
60+
61+
# create unsorted protected header
62+
p = {}
63+
p["kid"] = sign_jwk["kid"] # 4
64+
p["alg"] = sign_jwk["alg"] # 1
65+
66+
ctx = COSE.new(deterministic_header=True)
67+
encoded = ctx.encode_and_sign(
68+
payload=b"a",
69+
key=sign_key,
70+
protected=p,
71+
)
72+
encoded_p = cbor2.loads(encoded).value[0]
73+
74+
sorted_p = {}
75+
sorted_p[1] = COSE_ALGORITHMS_SIGNATURE[sign_jwk["alg"]] # -7 for ES256
76+
sorted_p[4] = str.encode(sign_jwk["kid"]) # b'11'
77+
expected_p = cbor2.dumps(sorted_p)
78+
79+
assert expected_p == encoded_p
80+
81+
@pytest.mark.parametrize(
82+
"alg",
83+
[
84+
"HMAC 256/64",
85+
"HMAC 256/256",
86+
"HMAC 384/384",
87+
"HMAC 512/512",
88+
],
89+
)
90+
def test_deterministic_cose_mac_binary(self, alg):
91+
mac_key = COSEKey.generate_symmetric_key(alg=alg)
92+
93+
# create unsorted protected header
94+
p = {}
95+
p["kid"] = "01" # 4
96+
p["alg"] = alg # 1
97+
98+
ctx = COSE.new(deterministic_header=True)
99+
encoded = ctx.encode_and_mac(
100+
payload=b"a",
101+
key=mac_key,
102+
protected=p,
103+
)
104+
encoded_p = cbor2.loads(encoded).value[0]
105+
106+
sorted_p = {}
107+
sorted_p[1] = COSE_ALGORITHMS_MAC[alg]
108+
sorted_p[4] = str.encode("01")
109+
expected_p = cbor2.dumps(sorted_p)
110+
111+
assert expected_p == encoded_p

0 commit comments

Comments
 (0)