Skip to content

Commit

Permalink
Merge pull request #437 from dajiaji/v2_6_0
Browse files Browse the repository at this point in the history
Bump version to v2.6.0.
  • Loading branch information
dajiaji authored Oct 9, 2023
2 parents 3cc26ae + bf3845d commit ae4512a
Show file tree
Hide file tree
Showing 17 changed files with 474 additions and 489 deletions.
13 changes: 13 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@ Changes
Unreleased
----------

Version 2.6.0
-------------

Released 2023-10-09

- Add enum COSEKeyTypes. `#437 <https://github.com/dajiaji/python-cwt/pull/437>`__
- Add enum COSEKeyCrvs. `#437 <https://github.com/dajiaji/python-cwt/pull/437>`__
- Add enum COSEKeyOps. `#437 <https://github.com/dajiaji/python-cwt/pull/437>`__
- Follow draft-cose-hpke-06. `#437 <https://github.com/dajiaji/python-cwt/pull/437>`__
- Fix typo of private attribute. `#435 <https://github.com/dajiaji/python-cwt/pull/435>`__
- Update dev dependencies.
- Bump urllib3 to 2.0.6. `#436 <https://github.com/dajiaji/python-cwt/pull/436>`__

Version 2.5.1
-------------

Expand Down
33 changes: 9 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).

Expand Down Expand Up @@ -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")
Expand All @@ -532,23 +532,18 @@ 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,
)
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],
)

Expand Down Expand Up @@ -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(
Expand All @@ -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
],
},
)

Expand Down Expand Up @@ -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")
Expand All @@ -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,
)
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
16 changes: 14 additions & 2 deletions cwt/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -38,7 +47,10 @@
"COSE",
"COSEAlgs",
"COSEHeaders",
"COSEKeyCrvs",
"COSEKeyOps",
"COSEKeyParams",
"COSEKeyTypes",
"COSETypes",
"COSEKey",
"COSEMessage",
Expand Down
22 changes: 14 additions & 8 deletions cwt/algs/okp.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]):
Expand Down Expand Up @@ -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:
Expand Down
11 changes: 10 additions & 1 deletion cwt/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
4 changes: 2 additions & 2 deletions cwt/cose.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand Down
45 changes: 43 additions & 2 deletions cwt/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ class COSETypes(enum.IntEnum):


class COSEHeaders(enum.IntEnum):
HPKE_SENDER_INFO = -4
ALG = 1
CRIT = 2
CTY = 3
Expand Down Expand Up @@ -88,7 +87,6 @@ class COSEAlgs(enum.IntEnum):
A256KW = -5
A192KW = -4
A128KW = -3
HPKE_V1_BASE = -1
A128GCM = 1
A192GCM = 2
A256GCM = 3
Expand All @@ -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):
Expand All @@ -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
42 changes: 29 additions & 13 deletions cwt/recipient_algs/hpke.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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]]:
Expand All @@ -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
Expand All @@ -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
Expand Down
Loading

0 comments on commit ae4512a

Please sign in to comment.