diff --git a/CHANGES.rst b/CHANGES.rst index 0ef2dd1..2016cca 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,19 @@ Changes Unreleased ---------- +Version 2.6.0 +------------- + +Released 2023-10-09 + +- Add enum COSEKeyTypes. `#437 `__ +- Add enum COSEKeyCrvs. `#437 `__ +- Add enum COSEKeyOps. `#437 `__ +- Follow draft-cose-hpke-06. `#437 `__ +- Fix typo of private attribute. `#435 `__ +- Update dev dependencies. + - Bump urllib3 to 2.0.6. `#436 `__ + Version 2.5.1 ------------- diff --git a/README.md b/README.md index 3128e9c..13da454 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ implementation compliant with: - [RFC9053: CBOR Object Signing and Encryption (COSE): Initial Algorithms](https://www.rfc-editor.org/rfc/rfc9053.html) - [RFC9338: CBOR Object Signing and Encryption (COSE): Countersignatures](https://www.rfc-editor.org/rfc/rfc9338.html) - experimental - [RFC8392: CWT (CBOR Web Token)](https://tools.ietf.org/html/rfc8392) -- [draft-05: Use of HPKE with COSE](https://www.ietf.org/archive/id/draft-ietf-cose-hpke-05.html) - experimental +- [draft-06: Use of HPKE with COSE](https://www.ietf.org/archive/id/draft-ietf-cose-hpke-06.html) - experimental - [draft-06: CWT Claims in COSE Headers](https://www.ietf.org/archive/id/draft-ietf-cose-cwt-claims-in-headers-06.html) - experimental - and related various specifications. See [Referenced Specifications](#referenced-specifications). @@ -517,7 +517,7 @@ assert countersignature.unprotected[4] == b"01" # kid: b"01" Create a COSE-HPKE MAC message, verify and decode it as follows: ```py -from cwt import COSE, COSEHeaders, COSEKey, Recipient +from cwt import COSE, COSEAlgs, COSEHeaders, COSEKey, Recipient # The sender side: mac_key = COSEKey.generate_symmetric_key(alg="HS256") @@ -532,15 +532,10 @@ rpk = COSEKey.from_jwk( ) r = Recipient.new( protected={ - COSEHeaders.ALG: -1, # alg: "HPKE" + COSEHeaders.ALG: COSEAlgs.HPKE_BASE_P256_SHA256_AES128GCM, }, unprotected={ COSEHeaders.KID: b"01", # kid: "01" - COSEHeaders.HPKE_SENDER_INFO: [ # HPKE sender information - 0x0010, # kem: DHKEM(P-256, HKDF-SHA256) - 0x0001, # kdf: HKDF-SHA256 - 0x0001, # aead: AES-128-GCM - ], }, recipient_key=rpk, ) @@ -548,7 +543,7 @@ sender = COSE.new() encoded = sender.encode( b"This is the content.", mac_key, - protected={COSEHeaders.ALG: 5}, # alg: HS256 + protected={COSEHeaders.ALG: COSEAlgs.HS256}, recipients=[r], ) @@ -667,7 +662,7 @@ assert countersignature.unprotected[4] == b"01" # kid: b"01" Create a COSE-HPKE Encrypt0 message and decrypt it as follows: ```py -from cwt import COSE, COSEHeaders, COSEKey +from cwt import COSE, COSEAlgs, COSEHeaders, COSEKey # The sender side: rpk = COSEKey.from_jwk( @@ -685,15 +680,10 @@ encoded = sender.encode( b"This is the content.", rpk, protected={ - COSEHeaders.ALG: -1, # alg: "HPKE" + COSEHeaders.ALG: COSEAlgs.HPKE_BASE_P256_SHA256_AES128GCM, }, unprotected={ COSEHeaders.KID: b"01", # kid: "01" - COSEHeaders.HPKE_SENDER_INFO: [ # HPKE sender information - 0x0010, # kem: DHKEM(P-256, HKDF-SHA256) - 0x0001, # kdf: HKDF-SHA256 - 0x0001, # aead: AES-128-GCM - ], }, ) @@ -981,7 +971,7 @@ assert countersignature.unprotected[4] == b"01" # kid: b"01" Create a COSE-HPKE Encrypt message and decrypt it as follows: ```py -from cwt import COSE, COSEHeaders, COSEKey, Recipient +from cwt import COSE, COSEAlgs, COSEHeaders, COSEKey, Recipient # The sender side: enc_key = COSEKey.generate_symmetric_key(alg="A128GCM") @@ -996,15 +986,10 @@ rpk = COSEKey.from_jwk( ) r = Recipient.new( protected={ - COSEHeaders.ALG: -1, # alg: "HPKE" + COSEHeaders.ALG: COSEAlgs.HPKE_BASE_P256_SHA256_AES128GCM, }, unprotected={ COSEHeaders.KID: b"01", # kid: "01" - COSEHeaders.HPKE_SENDER_INFO: [ # HPKE sender information - 0x0010, # kem: DHKEM(P-256, HKDF-SHA256) - 0x0001, # kdf: HKDF-SHA256 - 0x0001, # aead: AES-128-GCM - ], }, recipient_key=rpk, ) @@ -1760,7 +1745,7 @@ Python CWT is (partially) compliant with following specifications: - [RFC8392: CWT (CBOR Web Token)](https://tools.ietf.org/html/rfc8392) - [RFC8230: Using RSA Algorithms with COSE Messages](https://tools.ietf.org/html/rfc8230) - [RFC8152: CBOR Object Signing and Encryption (COSE)](https://tools.ietf.org/html/rfc8152) -- [draft-05: Use of HPKE with COSE](https://www.ietf.org/archive/id/draft-ietf-cose-hpke-05.html) - experimental +- [draft-05: Use of HPKE with COSE](https://www.ietf.org/archive/id/draft-ietf-cose-hpke-06.html) - experimental - [draft-06: CWT Claims in COSE Headers](https://www.ietf.org/archive/id/draft-ietf-cose-cwt-claims-in-headers-06.html) - experimental - [Electronic Health Certificate Specification](https://github.com/ehn-dcc-development/hcert-spec/blob/main/hcert_spec.md) - [Technical Specifications for Digital Green Certificates Volume 1](https://ec.europa.eu/health/sites/default/files/ehealth/docs/digital-green-certificates_v1_en.pdf) diff --git a/SECURITY.md b/SECURITY.md index 0d1078d..7203fe4 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -4,8 +4,8 @@ | Version | Supported | | ------- | ------------------ | -| 2.5.x | :white_check_mark: | -| < 2.5 | :x: | +| 2.6.x | :white_check_mark: | +| < 2.6 | :x: | ## Reporting a Vulnerability diff --git a/cwt/__init__.py b/cwt/__init__.py index e9c0556..edddb03 100644 --- a/cwt/__init__.py +++ b/cwt/__init__.py @@ -12,13 +12,22 @@ set_private_claim_names, ) from .encrypted_cose_key import EncryptedCOSEKey -from .enums import COSEAlgs, COSEHeaders, COSEKeyParams, COSETypes, CWTClaims +from .enums import ( + COSEAlgs, + COSEHeaders, + COSEKeyCrvs, + COSEKeyOps, + COSEKeyParams, + COSEKeyTypes, + COSETypes, + CWTClaims, +) from .exceptions import CWTError, DecodeError, EncodeError, VerifyError from .helpers.hcert import load_pem_hcert_dsc from .recipient import Recipient from .signer import Signer -__version__ = "2.5.1" +__version__ = "2.6.0" __title__ = "cwt" __description__ = "A Python implementation of CWT/COSE" __url__ = "https://python-cwt.readthedocs.io" @@ -38,7 +47,10 @@ "COSE", "COSEAlgs", "COSEHeaders", + "COSEKeyCrvs", + "COSEKeyOps", "COSEKeyParams", + "COSEKeyTypes", "COSETypes", "COSEKey", "COSEMessage", diff --git a/cwt/algs/okp.py b/cwt/algs/okp.py index e4ffd6e..a2113e9 100644 --- a/cwt/algs/okp.py +++ b/cwt/algs/okp.py @@ -68,17 +68,21 @@ def __init__(self, params: Dict[int, Any]): if self._crv not in [4, 5, 6, 7]: raise ValueError(f"Unsupported or unknown crv(-1) for OKP: {self._crv}.") if self._crv in [4, 5]: - if not self._alg: - raise ValueError("X25519/X448 needs alg explicitly.") + # if not self._alg: + # raise ValueError("X25519/X448 needs alg explicitly.") if self._alg in [-25, -27]: self._hash_alg = hashes.SHA256 elif self._alg in [-26, -28]: self._hash_alg = hashes.SHA512 - elif self._alg == -1: + elif self._alg in COSE_ALGORITHMS_HPKE.values(): self._hash_alg = hashes.SHA256 if self._crv == 4 else hashes.SHA512 - else: + elif self._alg is not None: raise ValueError(f"Unsupported or unknown alg used with X25519/X448: {self._alg}.") + # Check the existence of the key. + if -2 not in params and -4 not in params: + raise ValueError("The body of the key not found.") + # Validate alg and key_ops. if self._key_ops: if set(self._key_ops) & set([3, 4, 5, 6, 9, 10]): @@ -126,11 +130,13 @@ def __init__(self, params: Dict[int, Any]): self._alg = -8 # EdDSA else: # public key. - if 2 in self._key_ops: - if len(self._key_ops) > 1: + if self._crv in [4, 5]: # X25519/X448 + if not set(self._key_ops) & set([7, 8]): raise ValueError("Invalid key_ops for public key.") - else: - raise ValueError("Invalid key_ops for public key.") + else: # Ed25519/Ed448 + if len(self._key_ops) != 1 or self._key_ops[0] != 2: + raise ValueError("Invalid key_ops for public key.") + self._alg = -8 # EdDSA if self._alg in COSE_ALGORITHMS_CKDM_KEY_AGREEMENT_ES.values(): if -2 not in params: diff --git a/cwt/const.py b/cwt/const.py index b67c4d1..60dc713 100644 --- a/cwt/const.py +++ b/cwt/const.py @@ -173,7 +173,16 @@ } COSE_ALGORITHMS_HPKE = { - "HPKE": -1, # HPKE + "HPKE-Base-P256-SHA256-AES128GCM": 35, + "HPKE-Base-P256-SHA256-ChaCha20Poly1305": 36, + "HPKE-Base-P384-SHA384-AES256GCM": 37, + "HPKE-Base-P384-SHA384-ChaCha20Poly1305": 38, + "HPKE-Base-P521-SHA512-AES256GCM": 39, + "HPKE-Base-P521-SHA512-ChaCha20Poly1305": 40, + "HPKE-Base-X448-SHA512-AES256GCM": 43, + "HPKE-Base-X448-SHA512-ChaCha20Poly1305": 44, + "HPKE-Base-X25519-SHA256-AES128GCM": 41, + "HPKE-Base-X25519-SHA256-ChaCha20Poly1305": 42, } COSE_ALGORITHMS_CKDM_KEY_AGREEMENT_WITH_KEY_WRAP_SS = { diff --git a/cwt/cose.py b/cwt/cose.py index 47dea59..c284e73 100644 --- a/cwt/cose.py +++ b/cwt/cose.py @@ -395,7 +395,7 @@ def decode_with_headers( if k.kid != kid: continue try: - if not isinstance(p, bytes) and alg == -1: # HPKE + if not isinstance(p, bytes) and alg in COSE_ALGORITHMS_HPKE.values(): # HPKE hpke = HPKE(p, u, data.value[2]) res = hpke.decode(k, aad) if not isinstance(res, bytes): @@ -685,7 +685,7 @@ def _encode_and_encrypt( if len(recipients) == 0: enc_structure = ["Encrypt0", b_protected, external_aad] aad = self._dumps(enc_structure) - if 1 in p and p[1] == -1: # HPKE + if 1 in p and p[1] in COSE_ALGORITHMS_HPKE.values(): # HPKE hpke = HPKE(p, u, recipient_key=key) encoded, _ = hpke.encode(payload, aad) res = CBORTag(16, encoded) diff --git a/cwt/enums.py b/cwt/enums.py index 7a48eeb..fcb217e 100644 --- a/cwt/enums.py +++ b/cwt/enums.py @@ -14,7 +14,6 @@ class COSETypes(enum.IntEnum): class COSEHeaders(enum.IntEnum): - HPKE_SENDER_INFO = -4 ALG = 1 CRIT = 2 CTY = 3 @@ -88,7 +87,6 @@ class COSEAlgs(enum.IntEnum): A256KW = -5 A192KW = -4 A128KW = -3 - HPKE_V1_BASE = -1 A128GCM = 1 A192GCM = 2 A256GCM = 3 @@ -105,6 +103,16 @@ class COSEAlgs(enum.IntEnum): AES_CCM_16_128_256 = 31 AES_CCM_64_128_128 = 32 AES_CCM_64_128_256 = 33 + HPKE_BASE_P256_SHA256_AES128GCM = 35 + HPKE_BASE_P256_SHA256_CHACHA20POLY1305 = 36 + HPKE_BASE_P384_SHA384_AES256GCM = 37 + HPKE_BASE_P384_SHA384_CHACHA20POLY1305 = 38 + HPKE_BASE_P521_SHA512_AES256GCM = 39 + HPKE_BASE_P521_SHA512_CHACHA20POLY1305 = 40 + HPKE_BASE_X25519_SHA256_AES128GCM = 41 + HPKE_BASE_X25519_SHA256_CHACHA20POLY1305 = 42 + HPKE_BASE_X448_SHA512_AES256GCM = 43 + HPKE_BASE_X448_SHA512_CHACHA20POLY1305 = 44 class CWTClaims(enum.IntEnum): @@ -129,3 +137,36 @@ class CWTClaims(enum.IntEnum): LOCATION = 17 EAT_PROFILE = 18 SUBMODS = 20 + + +class COSEKeyTypes(enum.IntEnum): + OKP = 1 + EC2 = 2 + RSA = 3 + ASYMMETRIC = 4 + # HSS_LMS = 5 + # WALNUT_DSA = 6 + + +class COSEKeyCrvs(enum.IntEnum): + P256 = 1 + P384 = 2 + P521 = 3 + X25519 = 4 + X448 = 5 + ED25519 = 6 + ED448 = 7 + SECP256K1 = 8 + + +class COSEKeyOps(enum.IntEnum): + SIGN = 1 + VERIFY = 2 + ENCRYPT = 3 + DECRYPT = 4 + WRAP_KEY = 5 + UNWRAP_KEY = 6 + DERIVE_KEY = 7 + DERIVE_BITS = 8 + MAC_CREATE = 9 + MAC_VERIFY = 10 diff --git a/cwt/recipient_algs/hpke.py b/cwt/recipient_algs/hpke.py index d837483..41f3e42 100644 --- a/cwt/recipient_algs/hpke.py +++ b/cwt/recipient_algs/hpke.py @@ -4,10 +4,35 @@ from ..cose_key import COSEKey from ..cose_key_interface import COSEKeyInterface +from ..enums import COSEAlgs from ..exceptions import DecodeError, EncodeError from ..recipient_interface import RecipientInterface +def to_hpke_ciphersuites(alg: int) -> Tuple[int, int, int]: + if alg == COSEAlgs.HPKE_BASE_P256_SHA256_AES128GCM: + return 16, 1, 1 + if alg == COSEAlgs.HPKE_BASE_P256_SHA256_CHACHA20POLY1305: + return 16, 1, 3 + if alg == COSEAlgs.HPKE_BASE_P384_SHA384_AES256GCM: + return 17, 2, 2 + if alg == COSEAlgs.HPKE_BASE_P384_SHA384_CHACHA20POLY1305: + return 17, 2, 3 + if alg == COSEAlgs.HPKE_BASE_P521_SHA512_AES256GCM: + return 18, 3, 2 + if alg == COSEAlgs.HPKE_BASE_P521_SHA512_CHACHA20POLY1305: + return 18, 3, 3 + if alg == COSEAlgs.HPKE_BASE_X25519_SHA256_AES128GCM: + return 32, 1, 1 + if alg == COSEAlgs.HPKE_BASE_X25519_SHA256_CHACHA20POLY1305: + return 32, 1, 3 + if alg == COSEAlgs.HPKE_BASE_X448_SHA512_AES256GCM: + return 33, 3, 2 + if alg == COSEAlgs.HPKE_BASE_X448_SHA512_CHACHA20POLY1305: + return 33, 3, 3 + raise ValueError("alg should be one of the HPKE algorithms.") + + class HPKE(RecipientInterface): def __init__( self, @@ -19,14 +44,8 @@ def __init__( ): super().__init__(protected, unprotected, ciphertext, recipients) self._recipient_key = recipient_key - - if self._alg != -1: - raise ValueError("alg should be HPKE(-1).") - if -4 not in unprotected: - raise ValueError("HPKE sender information(-4) not found.") - if not isinstance(unprotected[-4], list) or len(unprotected[-4]) not in [3, 4]: - raise ValueError("HPKE sender information(-4) should be a list of length 3 or 4.") - self._suite = CipherSuite.new(KEMId(unprotected[-4][0]), KDFId(unprotected[-4][1]), AEADId(unprotected[-4][2])) + kem, kdf, aead = to_hpke_ciphersuites(self._alg) + self._suite = CipherSuite.new(KEMId(kem), KDFId(kdf), AEADId(aead)) return 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 self._kem_key = self._to_kem_key(self._recipient_key) try: enc, ctx = self._suite.create_sender_context(self._kem_key) - if len(self._unprotected[-4]) == 3: - self._unprotected[-4].append(enc) - else: - self._unprotected[-4][3] = enc + self._unprotected[-4] = enc self._ciphertext = ctx.seal(plaintext, aad=aad) except Exception as err: raise EncodeError("Failed to seal.") from err @@ -52,7 +68,7 @@ def decode( as_cose_key: bool = False, ) -> Union[bytes, COSEKeyInterface]: try: - ctx = self._suite.create_recipient_context(self._unprotected[-4][3], self._to_kem_key(key)) + ctx = self._suite.create_recipient_context(self._unprotected[-4], self._to_kem_key(key)) raw = ctx.open(self._ciphertext, aad=aad) if not as_cose_key: return raw diff --git a/pyproject.toml b/pyproject.toml index 7eb132e..47d4f08 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "cwt" -version = "2.5.1" +version = "2.6.0" description = "A Python implementation of CWT/COSE." authors = ["Ajitomi Daisuke "] license = "MIT" diff --git a/tests/test_algs_ec2.py b/tests/test_algs_ec2.py index e4929d3..323ec56 100644 --- a/tests/test_algs_ec2.py +++ b/tests/test_algs_ec2.py @@ -830,7 +830,7 @@ def test_ec2_key_hpke_with_alg_hpke_and_invalid_key_ops(self, key_ops): "kty": "EC", "kid": "01", "crv": "P-256", - "alg": "HPKE", + "alg": "HPKE-Base-P256-SHA256-AES128GCM", "key_ops": key_ops, "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4", @@ -852,7 +852,7 @@ def test_ec2_key_hpke_with_invalid_key_ops(self, key_ops): "kty": "EC", "kid": "01", "crv": "P-256", - # "alg": "HPKE", + # "alg": "HPKE-Base-P256-SHA256-AES128GCM", "key_ops": key_ops, "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8", "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4", diff --git a/tests/test_algs_okp.py b/tests/test_algs_okp.py index fe94f6b..bdd1b06 100644 --- a/tests/test_algs_okp.py +++ b/tests/test_algs_okp.py @@ -7,6 +7,7 @@ from cwt.algs.okp import OKPKey from cwt.cose_key import COSEKey +from cwt.enums import COSEKeyCrvs, COSEKeyOps, COSEKeyParams, COSEKeyTypes from cwt.exceptions import VerifyError from .utils import key_path @@ -20,19 +21,19 @@ class TestOKPKey: def test_okp_key_constructor_with_ed25519_key(self): private_key = OKPKey( { - 1: 1, - 3: -8, - -1: 6, - -2: b"\x18Es\xe0\x9a\x83\xfd\x0e\xe9K\xa8n\xf39i\x17\xfe\n2+|\xd1q\xcc\x87\xd2\xe9\xa9\xe8 \x9b\xd9", - -4: b"B\xc6u\xd0|-\x07\xe7)\x8d\x1c\x13\x14\xa2\x8dFC1\xdf3sQ\x049|\x14\xc1\xed\x01\xe5\xdb\xa9", + COSEKeyParams.KTY: COSEKeyTypes.OKP, + COSEKeyParams.ALG: -8, + COSEKeyParams.CRV: COSEKeyCrvs.ED25519, + COSEKeyParams.X: b"\x18Es\xe0\x9a\x83\xfd\x0e\xe9K\xa8n\xf39i\x17\xfe\n2+|\xd1q\xcc\x87\xd2\xe9\xa9\xe8 \x9b\xd9", + COSEKeyParams.D: b"B\xc6u\xd0|-\x07\xe7)\x8d\x1c\x13\x14\xa2\x8dFC1\xdf3sQ\x049|\x14\xc1\xed\x01\xe5\xdb\xa9", } ) public_key = OKPKey( { - 1: 1, - 3: -8, - -1: 6, - -2: b"\x18Es\xe0\x9a\x83\xfd\x0e\xe9K\xa8n\xf39i\x17\xfe\n2+|\xd1q\xcc\x87\xd2\xe9\xa9\xe8 \x9b\xd9", + COSEKeyParams.KTY: COSEKeyTypes.OKP, + COSEKeyParams.ALG: -8, + COSEKeyParams.CRV: COSEKeyCrvs.ED25519, + COSEKeyParams.X: b"\x18Es\xe0\x9a\x83\xfd\x0e\xe9K\xa8n\xf39i\x17\xfe\n2+|\xd1q\xcc\x87\xd2\xe9\xa9\xe8 \x9b\xd9", } ) assert private_key.kty == 1 @@ -69,19 +70,19 @@ def test_okp_key_constructor_with_ed25519_key(self): def test_okp_key_constructor_with_ed448_key(self): private_key = OKPKey( { - 1: 1, - 3: -8, - -1: 7, - -2: b"\xdb\x98\xacQb(\xb1I\x0c\xdb)\xce