Skip to content

Commit 6d142ef

Browse files
committed
Follow draft-cose-hpke-06.
1 parent 416f9c5 commit 6d142ef

13 files changed

Lines changed: 354 additions & 484 deletions

README.md

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ implementation compliant with:
1313
- [RFC9053: CBOR Object Signing and Encryption (COSE): Initial Algorithms](https://www.rfc-editor.org/rfc/rfc9053.html)
1414
- [RFC9338: CBOR Object Signing and Encryption (COSE): Countersignatures](https://www.rfc-editor.org/rfc/rfc9338.html) - experimental
1515
- [RFC8392: CWT (CBOR Web Token)](https://tools.ietf.org/html/rfc8392)
16-
- [draft-05: Use of HPKE with COSE](https://www.ietf.org/archive/id/draft-ietf-cose-hpke-05.html) - experimental
16+
- [draft-06: Use of HPKE with COSE](https://www.ietf.org/archive/id/draft-ietf-cose-hpke-06.html) - experimental
1717
- [draft-06: CWT Claims in COSE Headers](https://www.ietf.org/archive/id/draft-ietf-cose-cwt-claims-in-headers-06.html) - experimental
1818
- and related various specifications. See [Referenced Specifications](#referenced-specifications).
1919

@@ -517,7 +517,7 @@ assert countersignature.unprotected[4] == b"01" # kid: b"01"
517517
Create a COSE-HPKE MAC message, verify and decode it as follows:
518518

519519
```py
520-
from cwt import COSE, COSEHeaders, COSEKey, Recipient
520+
from cwt import COSE, COSEAlgs, COSEHeaders, COSEKey, Recipient
521521

522522
# The sender side:
523523
mac_key = COSEKey.generate_symmetric_key(alg="HS256")
@@ -532,23 +532,18 @@ rpk = COSEKey.from_jwk(
532532
)
533533
r = Recipient.new(
534534
protected={
535-
COSEHeaders.ALG: -1, # alg: "HPKE"
535+
COSEHeaders.ALG: COSEAlgs.HPKE_BASE_P256_SHA256_AES128GCM,
536536
},
537537
unprotected={
538538
COSEHeaders.KID: b"01", # kid: "01"
539-
COSEHeaders.HPKE_SENDER_INFO: [ # HPKE sender information
540-
0x0010, # kem: DHKEM(P-256, HKDF-SHA256)
541-
0x0001, # kdf: HKDF-SHA256
542-
0x0001, # aead: AES-128-GCM
543-
],
544539
},
545540
recipient_key=rpk,
546541
)
547542
sender = COSE.new()
548543
encoded = sender.encode(
549544
b"This is the content.",
550545
mac_key,
551-
protected={COSEHeaders.ALG: 5}, # alg: HS256
546+
protected={COSEHeaders.ALG: COSEAlgs.HS256},
552547
recipients=[r],
553548
)
554549

@@ -667,7 +662,7 @@ assert countersignature.unprotected[4] == b"01" # kid: b"01"
667662
Create a COSE-HPKE Encrypt0 message and decrypt it as follows:
668663

669664
```py
670-
from cwt import COSE, COSEHeaders, COSEKey
665+
from cwt import COSE, COSEAlgs, COSEHeaders, COSEKey
671666

672667
# The sender side:
673668
rpk = COSEKey.from_jwk(
@@ -685,15 +680,10 @@ encoded = sender.encode(
685680
b"This is the content.",
686681
rpk,
687682
protected={
688-
COSEHeaders.ALG: -1, # alg: "HPKE"
683+
COSEHeaders.ALG: COSEAlgs.HPKE_BASE_P256_SHA256_AES128GCM,
689684
},
690685
unprotected={
691686
COSEHeaders.KID: b"01", # kid: "01"
692-
COSEHeaders.HPKE_SENDER_INFO: [ # HPKE sender information
693-
0x0010, # kem: DHKEM(P-256, HKDF-SHA256)
694-
0x0001, # kdf: HKDF-SHA256
695-
0x0001, # aead: AES-128-GCM
696-
],
697687
},
698688
)
699689

@@ -981,7 +971,7 @@ assert countersignature.unprotected[4] == b"01" # kid: b"01"
981971
Create a COSE-HPKE Encrypt message and decrypt it as follows:
982972

983973
```py
984-
from cwt import COSE, COSEHeaders, COSEKey, Recipient
974+
from cwt import COSE, COSEAlgs, COSEHeaders, COSEKey, Recipient
985975

986976
# The sender side:
987977
enc_key = COSEKey.generate_symmetric_key(alg="A128GCM")
@@ -996,15 +986,10 @@ rpk = COSEKey.from_jwk(
996986
)
997987
r = Recipient.new(
998988
protected={
999-
COSEHeaders.ALG: -1, # alg: "HPKE"
989+
COSEHeaders.ALG: COSEAlgs.HPKE_BASE_P256_SHA256_AES128GCM,
1000990
},
1001991
unprotected={
1002992
COSEHeaders.KID: b"01", # kid: "01"
1003-
COSEHeaders.HPKE_SENDER_INFO: [ # HPKE sender information
1004-
0x0010, # kem: DHKEM(P-256, HKDF-SHA256)
1005-
0x0001, # kdf: HKDF-SHA256
1006-
0x0001, # aead: AES-128-GCM
1007-
],
1008993
},
1009994
recipient_key=rpk,
1010995
)
@@ -1760,7 +1745,7 @@ Python CWT is (partially) compliant with following specifications:
17601745
- [RFC8392: CWT (CBOR Web Token)](https://tools.ietf.org/html/rfc8392)
17611746
- [RFC8230: Using RSA Algorithms with COSE Messages](https://tools.ietf.org/html/rfc8230)
17621747
- [RFC8152: CBOR Object Signing and Encryption (COSE)](https://tools.ietf.org/html/rfc8152)
1763-
- [draft-05: Use of HPKE with COSE](https://www.ietf.org/archive/id/draft-ietf-cose-hpke-05.html) - experimental
1748+
- [draft-05: Use of HPKE with COSE](https://www.ietf.org/archive/id/draft-ietf-cose-hpke-06.html) - experimental
17641749
- [draft-06: CWT Claims in COSE Headers](https://www.ietf.org/archive/id/draft-ietf-cose-cwt-claims-in-headers-06.html) - experimental
17651750
- [Electronic Health Certificate Specification](https://github.com/ehn-dcc-development/hcert-spec/blob/main/hcert_spec.md)
17661751
- [Technical Specifications for Digital Green Certificates Volume 1](https://ec.europa.eu/health/sites/default/files/ehealth/docs/digital-green-certificates_v1_en.pdf)

cwt/algs/okp.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -68,17 +68,21 @@ def __init__(self, params: Dict[int, Any]):
6868
if self._crv not in [4, 5, 6, 7]:
6969
raise ValueError(f"Unsupported or unknown crv(-1) for OKP: {self._crv}.")
7070
if self._crv in [4, 5]:
71-
if not self._alg:
72-
raise ValueError("X25519/X448 needs alg explicitly.")
71+
# if not self._alg:
72+
# raise ValueError("X25519/X448 needs alg explicitly.")
7373
if self._alg in [-25, -27]:
7474
self._hash_alg = hashes.SHA256
7575
elif self._alg in [-26, -28]:
7676
self._hash_alg = hashes.SHA512
77-
elif self._alg == -1:
77+
elif self._alg in COSE_ALGORITHMS_HPKE.values():
7878
self._hash_alg = hashes.SHA256 if self._crv == 4 else hashes.SHA512
79-
else:
79+
elif self._alg is not None:
8080
raise ValueError(f"Unsupported or unknown alg used with X25519/X448: {self._alg}.")
8181

82+
# Check the existence of the key.
83+
if -2 not in params and -4 not in params:
84+
raise ValueError("The body of the key not found.")
85+
8286
# Validate alg and key_ops.
8387
if self._key_ops:
8488
if set(self._key_ops) & set([3, 4, 5, 6, 9, 10]):
@@ -126,11 +130,13 @@ def __init__(self, params: Dict[int, Any]):
126130
self._alg = -8 # EdDSA
127131
else:
128132
# public key.
129-
if 2 in self._key_ops:
130-
if len(self._key_ops) > 1:
133+
if self._crv in [4, 5]: # X25519/X448
134+
if not set(self._key_ops) & set([7, 8]):
131135
raise ValueError("Invalid key_ops for public key.")
132-
else:
133-
raise ValueError("Invalid key_ops for public key.")
136+
else: # Ed25519/Ed448
137+
if len(self._key_ops) != 1 or self._key_ops[0] != 2:
138+
raise ValueError("Invalid key_ops for public key.")
139+
self._alg = -8 # EdDSA
134140

135141
if self._alg in COSE_ALGORITHMS_CKDM_KEY_AGREEMENT_ES.values():
136142
if -2 not in params:

cwt/const.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -173,7 +173,16 @@
173173
}
174174

175175
COSE_ALGORITHMS_HPKE = {
176-
"HPKE": -1, # HPKE
176+
"HPKE-Base-P256-SHA256-AES128GCM": 35,
177+
"HPKE-Base-P256-SHA256-ChaCha20Poly1305": 36,
178+
"HPKE-Base-P384-SHA384-AES256GCM": 37,
179+
"HPKE-Base-P384-SHA384-ChaCha20Poly1305": 38,
180+
"HPKE-Base-P521-SHA512-AES256GCM": 39,
181+
"HPKE-Base-P521-SHA512-ChaCha20Poly1305": 40,
182+
"HPKE-Base-X448-SHA512-AES256GCM": 43,
183+
"HPKE-Base-X448-SHA512-ChaCha20Poly1305": 44,
184+
"HPKE-Base-X25519-SHA256-AES128GCM": 41,
185+
"HPKE-Base-X25519-SHA256-ChaCha20Poly1305": 42,
177186
}
178187

179188
COSE_ALGORITHMS_CKDM_KEY_AGREEMENT_WITH_KEY_WRAP_SS = {

cwt/cose.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ def decode_with_headers(
395395
if k.kid != kid:
396396
continue
397397
try:
398-
if not isinstance(p, bytes) and alg == -1: # HPKE
398+
if not isinstance(p, bytes) and alg in COSE_ALGORITHMS_HPKE.values(): # HPKE
399399
hpke = HPKE(p, u, data.value[2])
400400
res = hpke.decode(k, aad)
401401
if not isinstance(res, bytes):
@@ -685,7 +685,7 @@ def _encode_and_encrypt(
685685
if len(recipients) == 0:
686686
enc_structure = ["Encrypt0", b_protected, external_aad]
687687
aad = self._dumps(enc_structure)
688-
if 1 in p and p[1] == -1: # HPKE
688+
if 1 in p and p[1] in COSE_ALGORITHMS_HPKE.values(): # HPKE
689689
hpke = HPKE(p, u, recipient_key=key)
690690
encoded, _ = hpke.encode(payload, aad)
691691
res = CBORTag(16, encoded)

cwt/enums.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ class COSETypes(enum.IntEnum):
1414

1515

1616
class COSEHeaders(enum.IntEnum):
17-
HPKE_SENDER_INFO = -4
1817
ALG = 1
1918
CRIT = 2
2019
CTY = 3
@@ -88,7 +87,6 @@ class COSEAlgs(enum.IntEnum):
8887
A256KW = -5
8988
A192KW = -4
9089
A128KW = -3
91-
HPKE_V1_BASE = -1
9290
A128GCM = 1
9391
A192GCM = 2
9492
A256GCM = 3
@@ -105,6 +103,16 @@ class COSEAlgs(enum.IntEnum):
105103
AES_CCM_16_128_256 = 31
106104
AES_CCM_64_128_128 = 32
107105
AES_CCM_64_128_256 = 33
106+
HPKE_BASE_P256_SHA256_AES128GCM = 35
107+
HPKE_BASE_P256_SHA256_CHACHA20POLY1305 = 36
108+
HPKE_BASE_P384_SHA384_AES256GCM = 37
109+
HPKE_BASE_P384_SHA384_CHACHA20POLY1305 = 38
110+
HPKE_BASE_P521_SHA512_AES256GCM = 39
111+
HPKE_BASE_P521_SHA512_CHACHA20POLY1305 = 40
112+
HPKE_BASE_X25519_SHA256_AES128GCM = 41
113+
HPKE_BASE_X25519_SHA256_CHACHA20POLY1305 = 42
114+
HPKE_BASE_X448_SHA512_AES256GCM = 43
115+
HPKE_BASE_X448_SHA512_CHACHA20POLY1305 = 44
108116

109117

110118
class CWTClaims(enum.IntEnum):

cwt/recipient_algs/hpke.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,35 @@
44

55
from ..cose_key import COSEKey
66
from ..cose_key_interface import COSEKeyInterface
7+
from ..enums import COSEAlgs
78
from ..exceptions import DecodeError, EncodeError
89
from ..recipient_interface import RecipientInterface
910

1011

12+
def to_hpke_ciphersuites(alg: int) -> Tuple[int, int, int]:
13+
if alg == COSEAlgs.HPKE_BASE_P256_SHA256_AES128GCM:
14+
return 16, 1, 1
15+
if alg == COSEAlgs.HPKE_BASE_P256_SHA256_CHACHA20POLY1305:
16+
return 16, 1, 3
17+
if alg == COSEAlgs.HPKE_BASE_P384_SHA384_AES256GCM:
18+
return 17, 2, 2
19+
if alg == COSEAlgs.HPKE_BASE_P384_SHA384_CHACHA20POLY1305:
20+
return 17, 2, 3
21+
if alg == COSEAlgs.HPKE_BASE_P521_SHA512_AES256GCM:
22+
return 18, 3, 2
23+
if alg == COSEAlgs.HPKE_BASE_P521_SHA512_CHACHA20POLY1305:
24+
return 18, 3, 3
25+
if alg == COSEAlgs.HPKE_BASE_X25519_SHA256_AES128GCM:
26+
return 32, 1, 1
27+
if alg == COSEAlgs.HPKE_BASE_X25519_SHA256_CHACHA20POLY1305:
28+
return 32, 1, 3
29+
if alg == COSEAlgs.HPKE_BASE_X448_SHA512_AES256GCM:
30+
return 33, 3, 2
31+
if alg == COSEAlgs.HPKE_BASE_X448_SHA512_CHACHA20POLY1305:
32+
return 33, 3, 3
33+
raise ValueError("alg should be one of the HPKE algorithms.")
34+
35+
1136
class HPKE(RecipientInterface):
1237
def __init__(
1338
self,
@@ -19,14 +44,8 @@ def __init__(
1944
):
2045
super().__init__(protected, unprotected, ciphertext, recipients)
2146
self._recipient_key = recipient_key
22-
23-
if self._alg != -1:
24-
raise ValueError("alg should be HPKE(-1).")
25-
if -4 not in unprotected:
26-
raise ValueError("HPKE sender information(-4) not found.")
27-
if not isinstance(unprotected[-4], list) or len(unprotected[-4]) not in [3, 4]:
28-
raise ValueError("HPKE sender information(-4) should be a list of length 3 or 4.")
29-
self._suite = CipherSuite.new(KEMId(unprotected[-4][0]), KDFId(unprotected[-4][1]), AEADId(unprotected[-4][2]))
47+
kem, kdf, aead = to_hpke_ciphersuites(self._alg)
48+
self._suite = CipherSuite.new(KEMId(kem), KDFId(kdf), AEADId(aead))
3049
return
3150

3251
def encode(self, plaintext: bytes = b"", aad: bytes = b"") -> Tuple[List[Any], Optional[COSEKeyInterface]]:
@@ -35,10 +54,7 @@ def encode(self, plaintext: bytes = b"", aad: bytes = b"") -> Tuple[List[Any], O
3554
self._kem_key = self._to_kem_key(self._recipient_key)
3655
try:
3756
enc, ctx = self._suite.create_sender_context(self._kem_key)
38-
if len(self._unprotected[-4]) == 3:
39-
self._unprotected[-4].append(enc)
40-
else:
41-
self._unprotected[-4][3] = enc
57+
self._unprotected[-4] = enc
4258
self._ciphertext = ctx.seal(plaintext, aad=aad)
4359
except Exception as err:
4460
raise EncodeError("Failed to seal.") from err
@@ -52,7 +68,7 @@ def decode(
5268
as_cose_key: bool = False,
5369
) -> Union[bytes, COSEKeyInterface]:
5470
try:
55-
ctx = self._suite.create_recipient_context(self._unprotected[-4][3], self._to_kem_key(key))
71+
ctx = self._suite.create_recipient_context(self._unprotected[-4], self._to_kem_key(key))
5672
raw = ctx.open(self._ciphertext, aad=aad)
5773
if not as_cose_key:
5874
return raw

tests/test_algs_ec2.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -830,7 +830,7 @@ def test_ec2_key_hpke_with_alg_hpke_and_invalid_key_ops(self, key_ops):
830830
"kty": "EC",
831831
"kid": "01",
832832
"crv": "P-256",
833-
"alg": "HPKE",
833+
"alg": "HPKE-Base-P256-SHA256-AES128GCM",
834834
"key_ops": key_ops,
835835
"x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
836836
"y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4",
@@ -852,7 +852,7 @@ def test_ec2_key_hpke_with_invalid_key_ops(self, key_ops):
852852
"kty": "EC",
853853
"kid": "01",
854854
"crv": "P-256",
855-
# "alg": "HPKE",
855+
# "alg": "HPKE-Base-P256-SHA256-AES128GCM",
856856
"key_ops": key_ops,
857857
"x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
858858
"y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4",

0 commit comments

Comments
 (0)