Skip to content

Commit d4e0a62

Browse files
committed
add: option for Deterministic encoded protected and unprotected headers
1 parent 5025acb commit d4e0a62

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+
import cbor2
5+
6+
import pytest
7+
8+
from cwt import COSE, COSEKey
9+
from cwt.const import COSE_ALGORITHMS_SIGNATURE, COSE_ALGORITHMS_MAC
10+
from cwt.utils import sort_keys_for_deterministic_encoding
11+
12+
class TestDeterministicEncoding:
13+
"""
14+
Tests for Core Deterministic Encoding
15+
"""
16+
17+
def test_deterministically_sorted_dict(self):
18+
expected = {}
19+
expected[0] = 0 # Reserved
20+
expected[1] = 0 # alg
21+
expected[4] = 0 # kid
22+
expected[5] = 0 # IV
23+
expected[7] = 0 # counter signature
24+
expected[11] = 0 # Countersignature version 2
25+
expected[12] = 0 # Countersignature0 version 2
26+
expected[15] = 0 # CWT Claims
27+
expected[33] = 0 # x5chain
28+
expected[-1] = 0 # (max of COSE Header Algorithm Parameters resistry)
29+
expected[-65536] = 0 # (min of COSE Header Algorithm Parameters resistry)
30+
expected[-65537] = 0 # (max of Reserved for Private Use)
31+
32+
d = {}
33+
d[4] = 0
34+
d[-1] = 0
35+
d[33] = 0
36+
d[7] = 0
37+
d[11] = 0
38+
d[1] = 0
39+
d[5] = 0
40+
d[-65537] = 0
41+
d[0] = 0
42+
d[15] = 0
43+
d[12] = 0
44+
d[-65536] = 0
45+
46+
assert expected == sort_keys_for_deterministic_encoding(d)
47+
48+
def test_deterministic_cose_sign_binary(self):
49+
sign_jwk = {
50+
"kty": "EC",
51+
"kid": "11",
52+
"alg": "ES256",
53+
"crv": "P-256",
54+
"x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
55+
"y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4",
56+
"d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM",
57+
}
58+
sign_key = COSEKey.from_jwk(sign_jwk)
59+
60+
# create unsorted protected header
61+
p = {}
62+
p["kid"] = sign_jwk["kid"] # 4
63+
p["alg"] = sign_jwk["alg"] # 1
64+
65+
ctx = COSE.new(deterministic_header=True)
66+
encoded = ctx.encode_and_sign(
67+
payload=b'a',
68+
key=sign_key,
69+
protected=p,
70+
)
71+
encoded_p = cbor2.loads(encoded).value[0]
72+
73+
sorted_p = {}
74+
sorted_p[1] = COSE_ALGORITHMS_SIGNATURE[sign_jwk["alg"]] # -7 for ES256
75+
sorted_p[4] = str.encode(sign_jwk["kid"]) # b'11'
76+
expected_p = cbor2.dumps(sorted_p)
77+
78+
assert expected_p == encoded_p
79+
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)