Skip to content

Commit 233937c

Browse files
authored
Merge branch 'main' into follow-core-hpke-15
2 parents 50b5eb2 + febb85e commit 233937c

25 files changed

Lines changed: 617 additions & 469 deletions

CHANGES.rst

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

7+
Version 3.2.0
8+
-------------
9+
10+
Released 2025-09-17
11+
12+
- Fix a DeprecationWarning in a test util. `#637 <https://github.com/dajiaji/python-cwt/pull/637>`__ by @achamayou
13+
- Add a cwt.utils.ResolvedHeaders to allow tstr header labels and values. `#636 <https://github.com/dajiaji/python-cwt/pull/636>`__ by @achamayou
14+
- Typo fix. `#635 <https://github.com/dajiaji/python-cwt/pull/635>`__ by @achamayou
15+
- Prevent mutation of supp_pub.protected in to_cis. `#630 <https://github.com/dajiaji/python-cwt/pull/630>`__
16+
- Use enums on tests. `#625 <https://github.com/dajiaji/python-cwt/pull/625>`__
17+
- Update dependencies.
18+
- Bump cryptography to 46.0.2. `#646 <https://github.com/dajiaji/python-cwt/pull/646>`__
19+
- Bump cbor2 to 5.7.0. `#632 <https://github.com/dajiaji/python-cwt/pull/632>`__
20+
- Update dev dependencies.
21+
- Bump pytest-cov to 7.0.0. `#644 <https://github.com/dajiaji/python-cwt/pull/644>`__
22+
- Bump tox to 4.30.2. `#642 <https://github.com/dajiaji/python-cwt/pull/642>`__
23+
- Bump pytest to 8.4.2. `#641 <https://github.com/dajiaji/python-cwt/pull/641>`__
24+
25+
26+
- Bump pre-commit/black to 25.1.0. `#642 <https://github.com/dajiaji/python-cwt/pull/642>`__
27+
- Bump pre-commit/blacken-docs to 1.19.1. `#642 <https://github.com/dajiaji/python-cwt/pull/642>`__
28+
- Bump pre-commit/flake8 to 7.2.0. `#642 <https://github.com/dajiaji/python-cwt/pull/642>`__
29+
- Bump pre-commit/isort to 6.0.1. `#642 <https://github.com/dajiaji/python-cwt/pull/642>`__
30+
- Bump pre-commit/mirrors-mypy to 1.15.0. `#642 <https://github.com/dajiaji/python-cwt/pull/642>`__
31+
- Bump pre-commit to 4.2.0. `#642 <https://github.com/dajiaji/python-cwt/pull/642>`__
32+
33+
- Bump pre-commit/black to 25.1.0. `#646 <https://github.com/dajiaji/python-cwt/pull/646>`__
34+
- Bump pre-commit/blacken-docs to 1.19.1. `#646 <https://github.com/dajiaji/python-cwt/pull/646>`__
35+
- Bump pre-commit/flake8 to 7.2.0. `#646 <https://github.com/dajiaji/python-cwt/pull/646>`__
36+
- Bump pre-commit/isort to 6.0.1. `#646 <https://github.com/dajiaji/python-cwt/pull/646>`__
37+
- Bump pre-commit/mirrors-mypy to 1.15.0. `#646 <https://github.com/dajiaji/python-cwt/pull/646>`__
38+
- Bump pre-commit to 4.2.0. `#646 <https://github.com/dajiaji/python-cwt/pull/646>`__
39+
- Bump requests to 2.32.4. `#646 <https://github.com/dajiaji/python-cwt/pull/646>`__
40+
741
Version 3.1.0
842
-------------
943

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,15 @@ assert b"Hello world!" == recipient.decode(encoded, mac_key)
5454
## You can get decoded protected/unprotected headers with the payload as follows:
5555
# protected, unprotected, payload = recipient.decode_with_headers(encoded, mac_key)
5656
# assert b"Hello world!" == payload
57+
58+
## Note that to pass header parameters with tstr labels, or tstr values, and avoid
59+
# clashes with short-string names such as "alg" or value encoding to bstr, you can
60+
# resolve the headers yourself, and pass a cwt.utils.ResolvedHeader({...}).
61+
#
62+
# For example:
63+
# protected=cwt.utils.ResolvedHeader({
64+
# "string label": "value"
65+
# })
5766
```
5867

5968
**CWT API**

cwt/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@
2727
from .recipient import Recipient
2828
from .signer import Signer
2929

30-
__version__ = "3.1.0"
30+
__version__ = "3.2.0"
3131
__title__ = "cwt"
3232
__description__ = "A Python implementation of CWT/COSE"
3333
__url__ = "https://python-cwt.readthedocs.io"

cwt/cbor_processor.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any, Dict
1+
from typing import Any, Dict, Union
22

33
from cbor2 import dumps, loads
44

@@ -12,7 +12,7 @@ def _dumps(self, obj: Any) -> bytes:
1212
except Exception as err:
1313
raise EncodeError("Failed to encode.") from err
1414

15-
def _loads(self, s: bytes) -> Dict[int, Any]:
15+
def _loads(self, s: bytes) -> Dict[Union[str, int], Any]:
1616
try:
1717
return loads(s)
1818
except Exception as err:

cwt/cose.py

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from .recipient_interface import RecipientInterface
2424
from .recipients import Recipients
2525
from .signer import Signer
26-
from .utils import sort_keys_for_deterministic_encoding, to_cose_header
26+
from .utils import ResolvedHeader, sort_keys_for_deterministic_encoding, to_cose_header
2727

2828

2929
class COSE(CBORProcessor):
@@ -134,8 +134,8 @@ def encode(
134134
self,
135135
payload: bytes,
136136
key: Optional[COSEKeyInterface] = None,
137-
protected: Optional[dict] = None,
138-
unprotected: Optional[dict] = None,
137+
protected: Optional[Union[dict, ResolvedHeader]] = None,
138+
unprotected: Optional[Union[dict, ResolvedHeader]] = None,
139139
recipients: List[RecipientInterface] = [],
140140
signers: List[Signer] = [],
141141
external_aad: bytes = b"",
@@ -150,8 +150,8 @@ def encode(
150150
Args:
151151
payload (bytes): A content to be MACed, signed or encrypted.
152152
key (Optional[COSEKeyInterface]): A content encryption key as COSEKey.
153-
protected (Optional[dict]): Parameters that are to be cryptographically protected.
154-
unprotected (Optional[dict]): Parameters that are not cryptographically protected.
153+
protected (Optional[Union[dict, ResolvedHeader]]): Parameters that are to be cryptographically protected.
154+
unprotected (Optional[Union[dict, ResolvedHeader]]): Parameters that are not cryptographically protected.
155155
recipients (List[RecipientInterface]): A list of recipient information structures.
156156
signers (List[Signer]): A list of signer information objects for
157157
multiple signer cases.
@@ -385,7 +385,7 @@ def decode_with_headers(
385385
Since non-AEAD ciphers DO NOT provide neither authentication nor integrity
386386
of decrypted message, make sure to validate them outside of this library.
387387
Returns:
388-
Tuple[Dict[int, Any], Dict[int, Any], bytes]: A dictionary data of decoded protected headers, and a dictionary data of unprotected headers, and a byte string of decoded payload.
388+
Tuple[Dict[Union[str, int], Any], Dict[Union[str, int], Any], bytes]: A dictionary data of decoded protected headers, and a dictionary data of unprotected headers, and a byte string of decoded payload.
389389
Raises:
390390
ValueError: Invalid arguments.
391391
DecodeError: Failed to decode data.
@@ -605,10 +605,10 @@ def decode_with_headers(
605605
def _encode_headers(
606606
self,
607607
key: Optional[COSEKeyInterface],
608-
protected: Optional[dict],
609-
unprotected: Optional[dict],
608+
protected: Optional[Union[dict, ResolvedHeader]],
609+
unprotected: Optional[Union[dict, ResolvedHeader]],
610610
enable_non_aead: bool,
611-
) -> Tuple[Dict[int, Any], Dict[int, Any]]:
611+
) -> Tuple[Dict[Union[str, int], Any], Dict[Union[str, int], Any]]:
612612
p = to_cose_header(protected)
613613
u = to_cose_header(unprotected)
614614
if key is not None:
@@ -635,30 +635,32 @@ def _encode_headers(
635635
raise ValueError("protected header MUST be zero-length")
636636
return p, u
637637

638-
def _decode_headers(self, protected: Any, unprotected: Any) -> Tuple[Dict[int, Any], Dict[int, Any]]:
639-
p: Union[Dict[int, Any], bytes]
638+
def _decode_headers(
639+
self, protected: Any, unprotected: Any
640+
) -> Tuple[Dict[Union[str, int], Any], Dict[Union[str, int], Any]]:
641+
p: Union[Dict[Union[str, int], Any], bytes]
640642
p = self._loads(protected) if protected else {}
641643
if isinstance(p, bytes):
642644
if len(p) > 0:
643645
raise ValueError("Invalid protected header.")
644646
p = {}
645-
u: Dict[int, Any] = unprotected
647+
u: Dict[Union[str, int], Any] = unprotected
646648
if not isinstance(u, dict):
647649
raise ValueError("unprotected header should be dict.")
648650
return p, u
649651

650652
def _validate_cose_message(
651653
self,
652654
key: Optional[COSEKeyInterface],
653-
p: Dict[int, Any],
654-
u: Dict[int, Any],
655+
p: Dict[Union[str, int], Any],
656+
u: Dict[Union[str, int], Any],
655657
recipients: List[RecipientInterface],
656658
signers: List[Signer],
657659
) -> int:
658660
if len(recipients) > 0 and len(signers) > 0:
659661
raise ValueError("Both recipients and signers are specified.")
660662

661-
h: Dict[int, Any] = {}
663+
h: Dict[Union[str, int], Any] = {}
662664
iv_count: int = 0
663665
for k, v in p.items():
664666
if k == 2: # crit
@@ -780,8 +782,8 @@ def _encode_and_encrypt(
780782
self,
781783
payload: bytes,
782784
key: Optional[COSEKeyInterface],
783-
p: Dict[int, Any],
784-
u: Dict[int, Any],
785+
p: Dict[Union[str, int], Any],
786+
u: Dict[Union[str, int], Any],
785787
recipients: List[RecipientInterface],
786788
external_aad: bytes,
787789
out: str,
@@ -849,8 +851,8 @@ def _encode_and_mac(
849851
self,
850852
payload: bytes,
851853
key: Optional[COSEKeyInterface],
852-
p: Dict[int, Any],
853-
u: Dict[int, Any],
854+
p: Dict[Union[str, int], Any],
855+
u: Dict[Union[str, int], Any],
854856
recipients: List[RecipientInterface],
855857
external_aad: bytes,
856858
out: str,
@@ -895,8 +897,8 @@ def _encode_and_sign(
895897
self,
896898
payload: bytes,
897899
key: Optional[COSEKeyInterface],
898-
p: Dict[int, Any],
899-
u: Dict[int, Any],
900+
p: Dict[Union[str, int], Any],
901+
u: Dict[Union[str, int], Any],
900902
signers: List[Signer],
901903
external_aad: bytes,
902904
out: str,

cwt/cose_message.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from __future__ import annotations
22

3-
from typing import Any, Dict, List, Optional, Tuple
3+
from typing import Any, Dict, List, Optional, Tuple, Union
44

55
from cbor2 import CBORTag, loads
66

@@ -132,14 +132,14 @@ def type(self) -> COSETypes:
132132
return self._type
133133

134134
@property
135-
def protected(self) -> Dict[int, Any]:
135+
def protected(self) -> Dict[Union[str, int], Any]:
136136
"""
137137
The protected headers as a CBOR object.
138138
"""
139139
return self._loads(self._protected)
140140

141141
@property
142-
def unprotected(self) -> Dict[int, Any]:
142+
def unprotected(self) -> Dict[Union[str, int], Any]:
143143
"""
144144
The unprotected headers as a CBOR object.
145145
"""

cwt/cwt.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ def decode(
316316
data: bytes,
317317
keys: Union[COSEKeyInterface, List[COSEKeyInterface]],
318318
no_verify: bool = False,
319-
) -> Union[Dict[int, Any], bytes]:
319+
) -> Union[Dict[Union[str, int], Any], bytes]:
320320
"""
321321
Verifies and decodes CWT.
322322
@@ -333,11 +333,11 @@ def decode(
333333
DecodeError: Failed to decode the CWT.
334334
VerifyError: Failed to verify the CWT.
335335
"""
336-
cwt: Union[bytes, CBORTag, Dict[int, Any]] = self._loads(data)
336+
cwt: Union[bytes, CBORTag, Dict[Union[str, int], Any]] = self._loads(data)
337337
if isinstance(cwt, CBORTag) and cwt.tag == CWT.CBOR_TAG:
338338
cwt = cwt.value
339339
keys = [keys] if isinstance(keys, COSEKeyInterface) else keys
340-
p: Dict[int, Any] = {}
340+
p: Dict[Union[str, int], Any] = {}
341341
while isinstance(cwt, CBORTag):
342342
p, u, cwt = self._cose.decode_with_headers(cwt, keys)
343343
cwt = self._loads(cwt)
@@ -399,7 +399,7 @@ def _validate(self, claims: Union[Dict[int, Any], bytes]):
399399
Claims.validate(claims)
400400
return
401401

402-
def _verify(self, claims: Union[Dict[int, Any], bytes], protected: Dict[int, Any] = {}):
402+
def _verify(self, claims: Union[Dict[Union[str, int], Any], bytes], protected: Dict[Union[str, int], Any] = {}):
403403
if not isinstance(claims, dict):
404404
raise DecodeError("Failed to decode.")
405405

@@ -484,7 +484,7 @@ def decode(
484484
data: bytes,
485485
keys: Union[COSEKeyInterface, List[COSEKeyInterface]],
486486
no_verify: bool = False,
487-
) -> Union[Dict[int, Any], bytes]:
487+
) -> Union[Dict[Union[str, int], Any], bytes]:
488488
return _cwt.decode(data, keys, no_verify)
489489

490490

cwt/recipient.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from .recipient_algs.ecdh_direct_hkdf import ECDH_DirectHKDF
2121
from .recipient_algs.hpke import HPKE
2222
from .recipient_interface import RecipientInterface
23-
from .utils import to_cose_header, to_recipient_context
23+
from .utils import ResolvedHeader, to_cose_header, to_recipient_context
2424

2525

2626
class Recipient:
@@ -31,8 +31,8 @@ class Recipient:
3131
@classmethod
3232
def new(
3333
cls,
34-
protected: dict = {},
35-
unprotected: dict = {},
34+
protected: Union[dict, ResolvedHeader] = {},
35+
unprotected: Union[dict, ResolvedHeader] = {},
3636
ciphertext: bytes = b"",
3737
recipients: List[Any] = [],
3838
sender_key: Optional[COSEKeyInterface] = None,
@@ -46,8 +46,8 @@ def new(
4646
Creates a recipient from a CBOR-like dictionary with numeric keys.
4747
4848
Args:
49-
protected (dict): Parameters that are to be cryptographically protected.
50-
unprotected (dict): Parameters that are not cryptographically protected.
49+
protected (Union[dict, ResolvedHeader]): Parameters that are to be cryptographically protected.
50+
unprotected (Union[dict, ResolvedHeader]): Parameters that are not cryptographically protected.
5151
ciphertext (List[Any]): A cipher text.
5252
sender_key (Optional[COSEKeyInterface]): A sender private key as COSEKey.
5353
recipient_key (Optional[COSEKeyInterface]): A recipient public key as COSEKey.
@@ -80,7 +80,7 @@ def new(
8080
if alg == -6:
8181
return DirectKey(p, u)
8282
if alg in COSE_ALGORITHMS_KEY_WRAP.values():
83-
if len(protected) > 0:
83+
if len(p) > 0:
8484
raise ValueError("The protected header must be a zero-length string in key wrap mode with an AE algorithm.")
8585
if not sender_key:
8686
sender_key = COSEKey.from_symmetric_key(alg=alg)

cwt/recipient_algs/aes_key_wrap.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class AESKeyWrap(RecipientInterface):
1515

1616
def __init__(
1717
self,
18-
unprotected: Dict[int, Any],
18+
unprotected: Dict[Union[str, int], Any],
1919
ciphertext: bytes = b"",
2020
recipients: List[Any] = [],
2121
sender_key: Optional[COSEKeyInterface] = None,

cwt/recipient_algs/direct.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
from typing import Any, Dict, List
1+
from typing import Any, Dict, List, Union
22

33
from ..recipient_interface import RecipientInterface
44

55

66
class Direct(RecipientInterface):
77
def __init__(
88
self,
9-
protected: Dict[int, Any],
10-
unprotected: Dict[int, Any],
9+
protected: Dict[Union[str, int], Any],
10+
unprotected: Dict[Union[str, int], Any],
1111
ciphertext: bytes = b"",
1212
recipients: List[Any] = [],
1313
):

0 commit comments

Comments
 (0)