Skip to content

Commit ae4512a

Browse files
authored
Merge pull request #437 from dajiaji/v2_6_0
Bump version to v2.6.0.
2 parents 3cc26ae + bf3845d commit ae4512a

17 files changed

Lines changed: 474 additions & 489 deletions

CHANGES.rst

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,19 @@ Changes
44
Unreleased
55
----------
66

7+
Version 2.6.0
8+
-------------
9+
10+
Released 2023-10-09
11+
12+
- Add enum COSEKeyTypes. `#437 <https://github.com/dajiaji/python-cwt/pull/437>`__
13+
- Add enum COSEKeyCrvs. `#437 <https://github.com/dajiaji/python-cwt/pull/437>`__
14+
- Add enum COSEKeyOps. `#437 <https://github.com/dajiaji/python-cwt/pull/437>`__
15+
- Follow draft-cose-hpke-06. `#437 <https://github.com/dajiaji/python-cwt/pull/437>`__
16+
- Fix typo of private attribute. `#435 <https://github.com/dajiaji/python-cwt/pull/435>`__
17+
- Update dev dependencies.
18+
- Bump urllib3 to 2.0.6. `#436 <https://github.com/dajiaji/python-cwt/pull/436>`__
19+
720
Version 2.5.1
821
-------------
922

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)

SECURITY.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@
44

55
| Version | Supported |
66
| ------- | ------------------ |
7-
| 2.5.x | :white_check_mark: |
8-
| < 2.5 | :x: |
7+
| 2.6.x | :white_check_mark: |
8+
| < 2.6 | :x: |
99

1010
## Reporting a Vulnerability
1111

cwt/__init__.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,22 @@
1212
set_private_claim_names,
1313
)
1414
from .encrypted_cose_key import EncryptedCOSEKey
15-
from .enums import COSEAlgs, COSEHeaders, COSEKeyParams, COSETypes, CWTClaims
15+
from .enums import (
16+
COSEAlgs,
17+
COSEHeaders,
18+
COSEKeyCrvs,
19+
COSEKeyOps,
20+
COSEKeyParams,
21+
COSEKeyTypes,
22+
COSETypes,
23+
CWTClaims,
24+
)
1625
from .exceptions import CWTError, DecodeError, EncodeError, VerifyError
1726
from .helpers.hcert import load_pem_hcert_dsc
1827
from .recipient import Recipient
1928
from .signer import Signer
2029

21-
__version__ = "2.5.1"
30+
__version__ = "2.6.0"
2231
__title__ = "cwt"
2332
__description__ = "A Python implementation of CWT/COSE"
2433
__url__ = "https://python-cwt.readthedocs.io"
@@ -38,7 +47,10 @@
3847
"COSE",
3948
"COSEAlgs",
4049
"COSEHeaders",
50+
"COSEKeyCrvs",
51+
"COSEKeyOps",
4152
"COSEKeyParams",
53+
"COSEKeyTypes",
4254
"COSETypes",
4355
"COSEKey",
4456
"COSEMessage",

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: 43 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):
@@ -129,3 +137,36 @@ class CWTClaims(enum.IntEnum):
129137
LOCATION = 17
130138
EAT_PROFILE = 18
131139
SUBMODS = 20
140+
141+
142+
class COSEKeyTypes(enum.IntEnum):
143+
OKP = 1
144+
EC2 = 2
145+
RSA = 3
146+
ASYMMETRIC = 4
147+
# HSS_LMS = 5
148+
# WALNUT_DSA = 6
149+
150+
151+
class COSEKeyCrvs(enum.IntEnum):
152+
P256 = 1
153+
P384 = 2
154+
P521 = 3
155+
X25519 = 4
156+
X448 = 5
157+
ED25519 = 6
158+
ED448 = 7
159+
SECP256K1 = 8
160+
161+
162+
class COSEKeyOps(enum.IntEnum):
163+
SIGN = 1
164+
VERIFY = 2
165+
ENCRYPT = 3
166+
DECRYPT = 4
167+
WRAP_KEY = 5
168+
UNWRAP_KEY = 6
169+
DERIVE_KEY = 7
170+
DERIVE_BITS = 8
171+
MAC_CREATE = 9
172+
MAC_VERIFY = 10

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

0 commit comments

Comments
 (0)