Skip to content

Commit 798dcb2

Browse files
authored
Follow draft-ietf-cose-hpke-23 (#638)
* Align with draft-ietf-cose-hpke-15. * feat(cose): follow draft-ietf-cose-hpke-23 for HPKE algorithms - Split HPKE into integrated (HPKE-0..7) and key encryption (HPKE-0-KE..7-KE) - Update algorithm IDs and COSE recipient algorithms - Add hpke_info parameter to encrypt - Relax key_ops validation for HPKE private keys - Update README and docs references to draft-23 * fix(utils): keep cty, content type, x5u as tstr in COSE headers per RFC 9052/9360 * refactor: format long lines and improve type hints in COSE/HPKE code
1 parent febb85e commit 798dcb2

18 files changed

Lines changed: 2627 additions & 159 deletions

README.md

Lines changed: 50 additions & 5 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-07: Use of HPKE with COSE](https://www.ietf.org/archive/id/draft-ietf-cose-hpke-07.html) - experimental
16+
- [draft-23: Use of HPKE with COSE](https://www.ietf.org/archive/id/draft-ietf-cose-hpke-23.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
- [draft-13: Fully-Specified Algorithms for JOSE and COSE](https://www.ietf.org/archive/id/draft-ietf-jose-fully-specified-algorithms-13.html)
1919
- and related various specifications. See [Referenced Specifications](#referenced-specifications).
@@ -542,7 +542,7 @@ rpk = COSEKey.from_jwk(
542542
)
543543
r = Recipient.new(
544544
protected={
545-
COSEHeaders.ALG: COSEAlgs.HPKE_BASE_P256_SHA256_AES128GCM,
545+
COSEHeaders.ALG: COSEAlgs.HPKE_0_KE,
546546
},
547547
unprotected={
548548
COSEHeaders.KID: b"01", # kid: "01"
@@ -690,7 +690,7 @@ encoded = sender.encode(
690690
b"This is the content.",
691691
rpk,
692692
protected={
693-
COSEHeaders.ALG: COSEAlgs.HPKE_BASE_P256_SHA256_AES128GCM,
693+
COSEHeaders.ALG: COSEAlgs.HPKE_0,
694694
},
695695
unprotected={
696696
COSEHeaders.KID: b"01", # kid: "01"
@@ -712,6 +712,51 @@ recipient = COSE.new()
712712
assert b"This is the content." == recipient.decode(encoded, rsk)
713713
```
714714

715+
COSE-HPKE (Encrypt0) with psk_id
716+
717+
```py
718+
from cwt import COSE, COSEAlgs, COSEHeaders, COSEKey
719+
720+
# The sender side:
721+
rpk = COSEKey.from_jwk(
722+
{
723+
"kty": "EC",
724+
"kid": "01",
725+
"crv": "P-256",
726+
"x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
727+
"y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4",
728+
}
729+
)
730+
731+
sender = COSE.new()
732+
encoded = sender.encode(
733+
b"This is the content.",
734+
rpk,
735+
protected={COSEHeaders.ALG: COSEAlgs.HPKE_0},
736+
unprotected={
737+
COSEHeaders.KID: b"01",
738+
COSEHeaders.PSK_ID: b"psk-01", # HPKE PSK identifier
739+
},
740+
hpke_psk=b"secret-psk",
741+
)
742+
743+
# The recipient side:
744+
rsk = COSEKey.from_jwk(
745+
{
746+
"kty": "EC",
747+
"kid": "01",
748+
"crv": "P-256",
749+
"x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
750+
"y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4",
751+
"d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM",
752+
}
753+
)
754+
recipient = COSE.new()
755+
assert b"This is the content." == recipient.decode(encoded, rsk, hpke_psk=b"secret-psk")
756+
```
757+
758+
Note: `psk_id` (label `-5`) is carried in unprotected headers and validated as a `bstr`. The actual PSK provisioning is deployment-specific as described in the HPKE draft. Pass the PSK via `hpke_psk` to `encode/encode_and_encrypt` and `decode/decode_with_headers` when using the PSK-authenticated variant.
759+
715760
### COSE Encrypt
716761

717762
#### Direct Key Distribution for encryption
@@ -996,7 +1041,7 @@ rpk = COSEKey.from_jwk(
9961041
)
9971042
r = Recipient.new(
9981043
protected={
999-
COSEHeaders.ALG: COSEAlgs.HPKE_BASE_P256_SHA256_AES128GCM,
1044+
COSEHeaders.ALG: COSEAlgs.HPKE_0_KE,
10001045
},
10011046
unprotected={
10021047
COSEHeaders.KID: b"01", # kid: "01"
@@ -1756,7 +1801,7 @@ Python CWT is (partially) compliant with following specifications:
17561801
- [RFC8230: Using RSA Algorithms with COSE Messages](https://tools.ietf.org/html/rfc8230)
17571802
- [RFC9459: CBOR Object Signing and Encryption (COSE): AES-CTR and AES-CBC](https://www.rfc-editor.org/rfc/rfc9459.html) - experimental
17581803
- [RFC8152: CBOR Object Signing and Encryption (COSE)](https://tools.ietf.org/html/rfc8152)
1759-
- [draft-07: Use of HPKE with COSE](https://www.ietf.org/archive/id/draft-ietf-cose-hpke-07.html) - experimental
1804+
- [draft-23: Use of HPKE with COSE](https://www.ietf.org/archive/id/draft-ietf-cose-hpke-23.html) - experimental
17601805
- [draft-06: CWT Claims in COSE Headers](https://www.ietf.org/archive/id/draft-ietf-cose-cwt-claims-in-headers-06.html) - experimental
17611806
- [draft-13: Fully-Specified Algorithms for JOSE and COSE](https://www.ietf.org/archive/id/draft-ietf-jose-fully-specified-algorithms-13.html)
17621807
- [Electronic Health Certificate Specification](https://github.com/ehn-dcc-development/hcert-spec/blob/main/hcert_spec.md)

cwt/algs/ec2.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -83,11 +83,13 @@ def __init__(self, params: Dict[int, Any]):
8383
self._key_ops = [2]
8484
if self._alg:
8585
# Validate alg for EC2 curve.
86-
if self._crv == 1 and self._alg not in ([-7, -9, 35, 36] + list(COSE_ALGORITHMS_CKDM_KEY_AGREEMENT.values())):
86+
if self._crv == 1 and self._alg not in (
87+
[-7, -9, 35, 45, 46, 53] + list(COSE_ALGORITHMS_CKDM_KEY_AGREEMENT.values())
88+
):
8789
raise ValueError(f"Unsupported or unknown alg used with P-256: {self._alg}.")
88-
elif self._crv == 2 and self._alg not in ([-35, -51, 37, 38] + list(COSE_ALGORITHMS_CKDM_KEY_AGREEMENT.values())):
90+
elif self._crv == 2 and self._alg not in ([-35, -51, 37, 47] + list(COSE_ALGORITHMS_CKDM_KEY_AGREEMENT.values())):
8991
raise ValueError(f"Unsupported or unknown alg used with P-384: {self._alg}.")
90-
elif self._crv == 3 and self._alg not in ([-36, -52, 39, 40] + list(COSE_ALGORITHMS_CKDM_KEY_AGREEMENT.values())):
92+
elif self._crv == 3 and self._alg not in ([-36, -52, 39, 48] + list(COSE_ALGORITHMS_CKDM_KEY_AGREEMENT.values())):
9193
raise ValueError(f"Unsupported or unknown alg used with P-521: {self._alg}.")
9294
elif self._crv == 8 and self._alg != -47:
9395
raise ValueError(f"Unsupported or unknown alg used with secp256k1: {self._alg}.")
@@ -129,7 +131,7 @@ def __init__(self, params: Dict[int, Any]):
129131
# private key for key derivation.
130132
self._key_ops = [7, 8]
131133
elif self._alg in COSE_ALGORITHMS_HPKE.values():
132-
if self._key_ops:
134+
if 4 in params:
133135
if -4 in params:
134136
# private key for key derivation.
135137
if len(self._key_ops) != 1 or self._key_ops[0] != 8:
@@ -139,8 +141,6 @@ def __init__(self, params: Dict[int, Any]):
139141
if len(self._key_ops) > 0:
140142
raise ValueError("Invalid key_ops for HPKE public key.")
141143
else:
142-
if -4 in params and isinstance(self._key_ops, list) and len(self._key_ops) == 0:
143-
raise ValueError("Invalid key_ops for HPKE private key.")
144144
self._key_ops = [8] if -4 in params else []
145145
else:
146146
raise ValueError(f"Unsupported or unknown alg(3) for EC2: {self._alg}.")

cwt/algs/okp.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ def __init__(self, params: Dict[int, Any]):
108108
else:
109109
self._key_ops = [1, 2] if -4 in params else [2]
110110
elif self._alg in COSE_ALGORITHMS_HPKE.values():
111-
if self._key_ops:
111+
if 4 in params:
112112
if -4 in params:
113113
# private key for key derivation.
114114
if len(self._key_ops) != 1 or self._key_ops[0] != 8:
@@ -118,8 +118,6 @@ def __init__(self, params: Dict[int, Any]):
118118
if len(self._key_ops) > 0:
119119
raise ValueError("Invalid key_ops for HPKE public key.")
120120
else:
121-
if -4 in params and isinstance(self._key_ops, list) and len(self._key_ops) == 0:
122-
raise ValueError("Invalid key_ops for HPKE private key.")
123121
self._key_ops = [8] if -4 in params else []
124122
else:
125123
# self._alg in COSE_ALGORITHMS_CKDM_KEY_AGREEMENT.values():

cwt/const.py

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@
6565
"cty": 3,
6666
"content type": 3,
6767
"kid": 4,
68+
"ek": -4,
69+
"psk_id": -5,
6870
"iv": 5,
6971
"IV": 5,
7072
"Partial IV": 6,
@@ -195,17 +197,33 @@
195197
# etc.
196198
}
197199

200+
COSE_ALGORITHMS_HPKE_INTEGRATED = {
201+
# Integrated Encryption per draft-ietf-cose-hpke-23
202+
"HPKE-0": 35, # DHKEM(P-256, HKDF-SHA256) + HKDF-SHA256 + AES-128-GCM
203+
"HPKE-1": 37, # DHKEM(P-384, HKDF-SHA384) + HKDF-SHA384 + AES-256-GCM
204+
"HPKE-2": 39, # DHKEM(P-521, HKDF-SHA512) + HKDF-SHA512 + AES-256-GCM
205+
"HPKE-3": 41, # DHKEM(X25519, HKDF-SHA256) + HKDF-SHA256 + AES-128-GCM
206+
"HPKE-4": 42, # DHKEM(X25519, HKDF-SHA256) + HKDF-SHA256 + ChaCha20Poly1305
207+
"HPKE-5": 43, # DHKEM(X448, HKDF-SHA512) + HKDF-SHA512 + AES-256-GCM
208+
"HPKE-6": 44, # DHKEM(X448, HKDF-SHA512) + HKDF-SHA512 + ChaCha20Poly1305
209+
"HPKE-7": 45, # DHKEM(P-256, HKDF-SHA256) + HKDF-SHA256 + AES-256-GCM
210+
}
211+
212+
COSE_ALGORITHMS_HPKE_KE = {
213+
# Key Encryption per draft-ietf-cose-hpke-23
214+
"HPKE-0-KE": 46, # DHKEM(P-256, HKDF-SHA256) + HKDF-SHA256 + AES-128-GCM
215+
"HPKE-1-KE": 47, # DHKEM(P-384, HKDF-SHA384) + HKDF-SHA384 + AES-256-GCM
216+
"HPKE-2-KE": 48, # DHKEM(P-521, HKDF-SHA512) + HKDF-SHA512 + AES-256-GCM
217+
"HPKE-3-KE": 49, # DHKEM(X25519, HKDF-SHA256) + HKDF-SHA256 + AES-128-GCM
218+
"HPKE-4-KE": 50, # DHKEM(X25519, HKDF-SHA256) + HKDF-SHA256 + ChaCha20Poly1305
219+
"HPKE-5-KE": 51, # DHKEM(X448, HKDF-SHA512) + HKDF-SHA512 + AES-256-GCM
220+
"HPKE-6-KE": 52, # DHKEM(X448, HKDF-SHA512) + HKDF-SHA512 + ChaCha20Poly1305
221+
"HPKE-7-KE": 53, # DHKEM(P-256, HKDF-SHA256) + HKDF-SHA256 + AES-256-GCM
222+
}
223+
198224
COSE_ALGORITHMS_HPKE = {
199-
"HPKE-Base-P256-SHA256-AES128GCM": 35,
200-
"HPKE-Base-P256-SHA256-ChaCha20Poly1305": 36,
201-
"HPKE-Base-P384-SHA384-AES256GCM": 37,
202-
"HPKE-Base-P384-SHA384-ChaCha20Poly1305": 38,
203-
"HPKE-Base-P521-SHA512-AES256GCM": 39,
204-
"HPKE-Base-P521-SHA512-ChaCha20Poly1305": 40,
205-
"HPKE-Base-X448-SHA512-AES256GCM": 43,
206-
"HPKE-Base-X448-SHA512-ChaCha20Poly1305": 44,
207-
"HPKE-Base-X25519-SHA256-AES128GCM": 41,
208-
"HPKE-Base-X25519-SHA256-ChaCha20Poly1305": 42,
225+
**COSE_ALGORITHMS_HPKE_INTEGRATED,
226+
**COSE_ALGORITHMS_HPKE_KE,
209227
}
210228

211229
COSE_ALGORITHMS_CKDM_KEY_AGREEMENT_WITH_KEY_WRAP_SS = {
@@ -400,7 +418,7 @@
400418
**COSE_ALGORITHMS_CKDM,
401419
**COSE_ALGORITHMS_KEY_WRAP,
402420
**COSE_ALGORITHMS_CKDM_KEY_AGREEMENT,
403-
**COSE_ALGORITHMS_HPKE,
421+
**COSE_ALGORITHMS_HPKE_KE,
404422
}
405423

406424
# COSE Algorithms for Symmetric Keys.
@@ -425,6 +443,7 @@
425443
**COSE_ALGORITHMS_SIGNATURE,
426444
**COSE_ALGORITHMS_SYMMETRIC,
427445
**COSE_ALGORITHMS_RECIPIENT,
446+
**COSE_ALGORITHMS_HPKE_INTEGRATED,
428447
}
429448

430449
# COSE Named Algorithms for converting from JWK-like key.

0 commit comments

Comments
 (0)