From b84fc66f17009c97810a99898e74e7a95ede23e2 Mon Sep 17 00:00:00 2001 From: Amaury Chamayou Date: Thu, 5 Nov 2026 17:03:33 +0000 Subject: [PATCH] Correct COSE signing header annotations for strict type checking Accept parameterized dictionaries and ResolvedHeader for both signing headers. Document wrapper bypass semantics and add complementary signing regression coverage without changing runtime behavior or introducing new tooling. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGES.rst | 2 ++ cwt/cose.py | 12 ++++++++---- tests/test_cose.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 54 insertions(+), 4 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 20c3bac4..4e70e63c 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,8 @@ Changes Unreleased ---------- +- Correct COSE signing header annotations to accept dictionaries and ResolvedHeader in strict type checkers. + Version 3.3.0 ------------- diff --git a/cwt/cose.py b/cwt/cose.py index 838deaf6..e6bc78e9 100644 --- a/cwt/cose.py +++ b/cwt/cose.py @@ -274,8 +274,8 @@ def encode_and_sign( self, payload: bytes, key: Optional[COSEKeyInterface] = None, - protected: Optional[dict] = None, - unprotected: Optional[dict] = None, + protected: Optional[Union[Dict[Any, Any], ResolvedHeader]] = None, + unprotected: Optional[Union[Dict[Any, Any], ResolvedHeader]] = None, signers: List[Signer] = [], external_aad: bytes = b"", out: str = "", @@ -288,8 +288,12 @@ def encode_and_sign( key (Optional[COSEKeyInterface]): A signing key for single signer cases. When the ``signers`` parameter is set, this ``key`` will be ignored and should not be set. - protected (Optional[dict]): Parameters that are to be cryptographically protected. - unprotected (Optional[dict]): Parameters that are not cryptographically protected. + protected (Optional[Union[Dict[Any, Any], ResolvedHeader]]): Parameters that are + to be cryptographically protected. Use ``ResolvedHeader`` to bypass + named-header resolution and value conversion. + unprotected (Optional[Union[Dict[Any, Any], ResolvedHeader]]): Parameters that are + not cryptographically protected. Use ``ResolvedHeader`` to bypass + named-header resolution and value conversion. signers (List[Signer]): A list of signer information objects for multiple signer cases. external_aad(bytes): External additional authenticated data supplied diff --git a/tests/test_cose.py b/tests/test_cose.py index 89e67c10..22aa1fb5 100644 --- a/tests/test_cose.py +++ b/tests/test_cose.py @@ -10,6 +10,7 @@ import base64 import datetime from secrets import token_bytes +from typing import Any, Dict, Union import cbor2 import pytest @@ -20,6 +21,7 @@ from cwt.enums import COSEAlgs, COSEHeaders from cwt.recipient_interface import RecipientInterface from cwt.signer import Signer +from cwt.utils import ResolvedHeader from .utils import key_path @@ -133,6 +135,48 @@ def test_cose_encode_and_decode_signature1_with_options(self): encoded = ctx.encode_and_sign(b"Hello world!", sig_key) assert b"Hello world!" == ctx.decode(encoded, sig_key) + @pytest.mark.parametrize("wrap_protected", [False, True]) + @pytest.mark.parametrize("wrap_unprotected", [False, True]) + @pytest.mark.parametrize("out", ["", "cbor2/CBORTag"]) + def test_cose_signing_resolved_headers(self, ctx, wrap_protected, wrap_unprotected, out): + with open(key_path("private_key_ed25519.pem")) as key_file: + private_key = COSEKey.from_pem(key_file.read()) + with open(key_path("public_key_ed25519.pem")) as key_file: + public_key = COSEKey.from_pem(key_file.read(), kid="01") + protected: Dict[Union[str, int], Any] = {1: -8, -65537: "protected text", -65538: b"protected bytes"} + unprotected: Dict[Union[str, int], Any] = {4: b"01", -65539: "unprotected text", -65540: b"unprotected bytes"} + if wrap_protected: + protected["application"] = "protected application text" + if wrap_unprotected: + unprotected["kid"] = "literal application label and text" + encoded = ctx.encode_and_sign( + b"Hello world!", + private_key, + protected=ResolvedHeader(protected) if wrap_protected else protected, + unprotected=ResolvedHeader(unprotected) if wrap_unprotected else unprotected, + out=out, + ) + assert isinstance(encoded, CBORTag if out else bytes) + p, u, payload = ctx.decode_with_headers(encoded, public_key) + assert p == protected + assert u == unprotected + assert payload == b"Hello world!" + + def test_cose_signing_named_headers(self, ctx): + with open(key_path("private_key_ed25519.pem")) as key_file: + key = COSEKey.from_pem(key_file.read(), kid="01") + encoded = ctx.encode_and_sign( + b"Hello world!", + key, + protected={"alg": "EdDSA", "kid": "01"}, + unprotected={"cty": "application/cose", "x5u": "https://example.com/cert"}, + ) + assert isinstance(encoded, bytes) + p, u, payload = ctx.decode_with_headers(encoded, key) + assert p == {1: -8, 4: b"01"} + assert u == {3: "application/cose", 35: "https://example.com/cert"} + assert payload == b"Hello world!" + # def test_cose_encode_and_decode_mac0_with_protected_bytes(self): # ctx = COSE.new(kid_auto_inclusion=True)