Skip to content

Commit fec2e3a

Browse files
committed
Add support for HPKE key wrapping.
1 parent aaae0ae commit fec2e3a

7 files changed

Lines changed: 101 additions & 48 deletions

File tree

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,7 @@ Create a COSE-HPKE Encrypt message and decrypt it as follows:
623623
from cwt import COSE, COSEKey, Recipient
624624

625625
# The sender side:
626+
enc_key = COSEKey.from_symmetric_key(alg="A128GCM")
626627
rpk = COSEKey.from_jwk(
627628
{
628629
"kty": "EC",
@@ -645,10 +646,13 @@ r = Recipient.new(
645646
},
646647
},
647648
)
648-
r.apply(recipient_key=rpk)
649+
r.encode(enc_key.key, recipient_key=rpk)
649650
sender = COSE.new()
650651
encoded = sender.encode_and_encrypt(
651652
b"This is the content.",
653+
protected={
654+
1: 1, # alg: "A128GCM"
655+
},
652656
recipients=[r],
653657
)
654658

cwt/cose.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -169,8 +169,8 @@ def encode_and_encrypt(
169169
if not recipients:
170170
if 1 in p and p[1] == -1: # HPKE
171171
hpke = HPKE(p, u)
172-
hpke.apply(recipient_key=key)
173-
res = CBORTag(16, hpke.to_list(payload, external_aad, "Encrypt0"))
172+
hpke.encode(payload, recipient_key=key, external_aad=external_aad, aad_context="Encrypt0")
173+
res = CBORTag(16, hpke.to_list())
174174
return res if out == "cbor2/CBORTag" else self._dumps(res)
175175
if key is None:
176176
raise ValueError("key should be set.")
@@ -412,7 +412,7 @@ def decode(
412412
try:
413413
if not isinstance(protected, bytes) and alg == -1: # HPKE
414414
hpke = HPKE(protected, unprotected, data.value[2])
415-
return hpke.decrypt(k, external_aad=external_aad, aad_context="Encrypt0")
415+
return hpke.decode(k, external_aad=external_aad, aad_context="Encrypt0")
416416
return k.decrypt(data.value[2], nonce, aad)
417417
except Exception as e:
418418
err = e

cwt/recipient_algs/hpke.py

Lines changed: 26 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from pyhpke import AEADId, CipherSuite, KDFId, KEMId, KEMKey, KEMKeyInterface
44

5+
from ..cose_key import COSEKey
56
from ..cose_key_interface import COSEKeyInterface
67
from ..exceptions import DecodeError, EncodeError
78
from ..recipient_interface import RecipientInterface
@@ -30,62 +31,58 @@ def __init__(
3031
self._suite = CipherSuite.new(KEMId(unprotected[-4][1]), KDFId(unprotected[-4][2]), AEADId(unprotected[-4][3]))
3132
return
3233

33-
def apply(
34+
def encode(
3435
self,
35-
key: Optional[COSEKeyInterface] = None,
36+
plaintext: bytes,
3637
recipient_key: Optional[COSEKeyInterface] = None,
3738
salt: Optional[bytes] = None,
3839
context: Optional[Union[List[Any], Dict[str, Any]]] = None,
3940
external_aad: bytes = b"",
4041
aad_context: str = "Enc_Recipient",
41-
) -> COSEKeyInterface:
42-
# if not key:
43-
# raise ValueError("key should be set.")
42+
) -> Optional[COSEKeyInterface]:
4443
if not recipient_key:
4544
raise ValueError("recipient_key should be set.")
46-
# if recipient_key.kid:
47-
# self._protected[4] = key.kid
4845
self._recipient_key = recipient_key
4946
self._kem_key = self._to_kem_key(recipient_key)
50-
# enc_structure = ["Enc_Recipient", self._dumps(self._protected), external_aad]
51-
# aad = self._dumps(enc_structure)
52-
# enc, sender = self._suite.create_sender_context(self._kem_key)
53-
# self._unprotected[-4][4] = enc
54-
# try:
55-
# self._ciphertext = sender.seal(key.key, aad=aad)
56-
# except Exception as err:
57-
# raise EncodeError("Failed to seal.") from err
58-
return self._recipient_key
59-
60-
def to_list(self, payload: bytes = b"", external_aad: bytes = b"", aad_context: str = "Enc_Recipient") -> List[Any]:
6147
enc_structure = [aad_context, self._dumps(self._protected), external_aad]
6248
aad = self._dumps(enc_structure)
63-
enc, sender = self._suite.create_sender_context(self._kem_key)
64-
self._unprotected[-4][4] = enc
6549
try:
66-
self._ciphertext = sender.seal(payload, aad=aad)
67-
return super().to_list(payload, external_aad, aad_context)
50+
enc, ctx = self._suite.create_sender_context(self._kem_key)
51+
self._unprotected[-4][4] = enc
52+
self._ciphertext = ctx.seal(plaintext, aad=aad)
6853
except Exception as err:
6954
raise EncodeError("Failed to seal.") from err
55+
return None
7056

71-
def decrypt(
57+
def decode(
7258
self,
7359
key: COSEKeyInterface,
74-
alg: Optional[int] = None,
7560
context: Optional[Union[List[Any], Dict[str, Any]]] = None,
76-
payload: bytes = b"",
77-
nonce: bytes = b"",
78-
aad: bytes = b"",
7961
external_aad: bytes = b"",
8062
aad_context: str = "Enc_Recipient",
8163
) -> bytes:
8264
enc_structure = [aad_context, self._dumps(self._protected), external_aad]
8365
aad = self._dumps(enc_structure)
84-
recipient = self._suite.create_recipient_context(self._unprotected[-4][4], self._to_kem_key(key))
8566
try:
86-
return recipient.open(self._ciphertext, aad=aad)
67+
ctx = self._suite.create_recipient_context(self._unprotected[-4][4], self._to_kem_key(key))
68+
return ctx.open(self._ciphertext, aad=aad)
8769
except Exception as err:
8870
raise DecodeError("Failed to open.") from err
8971

72+
def decrypt(
73+
self,
74+
key: COSEKeyInterface,
75+
alg: Optional[int] = None,
76+
context: Optional[Union[List[Any], Dict[str, Any]]] = None,
77+
payload: bytes = b"",
78+
nonce: bytes = b"",
79+
aad: bytes = b"",
80+
external_aad: bytes = b"",
81+
aad_context: str = "Enc_Recipient",
82+
) -> bytes:
83+
alg = alg if isinstance(alg, int) else 0
84+
raw = self.decode(key, context, external_aad, aad_context)
85+
return COSEKey.from_symmetric_key(raw, alg=alg, kid=self._kid).decrypt(payload, nonce, aad)
86+
9087
def _to_kem_key(self, src: COSEKeyInterface) -> KEMKeyInterface:
9188
return KEMKey.from_pyca_cryptography_key(src.key)

cwt/recipient_interface.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,45 @@ def to_list(self, payload: bytes = b"", external_aad: bytes = b"", aad_context:
154154
res.append(children)
155155
return res
156156

157+
def encode(
158+
self,
159+
plaintext: bytes,
160+
recipient_key: Optional[COSEKeyInterface] = None,
161+
salt: Optional[bytes] = None,
162+
context: Optional[Union[List[Any], Dict[str, Any]]] = None,
163+
external_aad: bytes = b"",
164+
aad_context: str = "Enc_Recipient",
165+
) -> Optional[COSEKeyInterface]:
166+
"""
167+
Encodes a specified plaintext to a ciphertext with the recipient-specific
168+
method (e.g., key wrapping, key agreement, or the combination of them)
169+
and sets up the related information (context information or ciphertext)
170+
in the recipient structure.
171+
Therefore, it will be used by the sender of the recipient information
172+
before calling COSE.encode_* functions with the Recipient object. The
173+
key generated through this function will be set to ``key`` parameter
174+
of COSE.encode_* functions.
175+
176+
Args:
177+
plaintext (bytes): A plaing text to be encrypted. In most of the cases,
178+
the plaintext is a byte string of a content encryption key.
179+
recipient_key (Optional[COSEKeyInterface]): The external public
180+
key provided by the recipient used for ECDH key agreement, HPKE, etc.
181+
salt (Optional[bytes]): A salt used for deriving a key.
182+
context (Optional[Union[List[Any], Dict[str, Any]]]): Context
183+
information structure.
184+
external_aad (bytes): External additional authenticated data for AEAD.
185+
aad_context (bytes): An additional authenticated data context to build
186+
an Enc_structure internally.
187+
Returns:
188+
Optional[COSEKeyInterface]: A generated key or passed-through key
189+
which is used as ``key`` parameter of COSE.encode_* functions.
190+
Raises:
191+
ValueError: Invalid arguments.
192+
EncodeError: Failed to encode(e.g., wrap, derive) the key.
193+
"""
194+
raise NotImplementedError
195+
157196
def apply(
158197
self,
159198
key: Optional[COSEKeyInterface] = None,
@@ -164,7 +203,7 @@ def apply(
164203
aad_context: str = "Enc_Recipient",
165204
) -> COSEKeyInterface:
166205
"""
167-
Applies a COSEKey as a material to prepare a MAC/encryption key with
206+
[DEPRECATED] Applies a COSEKey as a material to prepare a MAC/encryption key with
168207
the recipient-specific method (e.g., key wrapping, key agreement,
169208
or the combination of them) and sets up the related information
170209
(context information or ciphertext) in the recipient structure.

tests/test_cose_sample.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -421,6 +421,7 @@ def test_cose_usage_examples_cose_encrypt(self):
421421
def test_cose_usage_examples_cose_encrypt_hpke(self):
422422

423423
# The sender side:
424+
enc_key = COSEKey.from_symmetric_key(alg="A128GCM")
424425
rpk = COSEKey.from_jwk(
425426
{
426427
"kty": "EC",
@@ -443,10 +444,11 @@ def test_cose_usage_examples_cose_encrypt_hpke(self):
443444
},
444445
},
445446
)
446-
r.apply(recipient_key=rpk)
447-
sender = COSE.new()
447+
r.encode(enc_key.key, recipient_key=rpk)
448+
sender = COSE.new(alg_auto_inclusion=True)
448449
encoded = sender.encode_and_encrypt(
449450
b"This is the content.",
451+
key=enc_key,
450452
recipients=[r],
451453
)
452454

@@ -469,6 +471,7 @@ def test_cose_usage_examples_cose_encrypt_hpke(self):
469471
def test_cose_usage_examples_cose_encrypt_hpke_with_1st_layer_hpke(self):
470472

471473
# The sender side:
474+
enc_key = COSEKey.from_symmetric_key(alg="A128GCM")
472475
rpk = COSEKey.from_jwk(
473476
{
474477
"kty": "EC",
@@ -491,7 +494,7 @@ def test_cose_usage_examples_cose_encrypt_hpke_with_1st_layer_hpke(self):
491494
},
492495
},
493496
)
494-
r.apply(recipient_key=rpk)
497+
r.encode(enc_key.key, recipient_key=rpk)
495498
sender = COSE.new()
496499
with pytest.raises(ValueError) as err:
497500
sender.encode_and_encrypt(
@@ -515,6 +518,7 @@ def test_cose_usage_examples_cose_encrypt_hpke_with_1st_layer_hpke(self):
515518
def test_cose_usage_examples_cose_encrypt_hpke_with_nonce(self):
516519

517520
# The sender side:
521+
enc_key = COSEKey.from_symmetric_key(alg="A128GCM")
518522
rpk = COSEKey.from_jwk(
519523
{
520524
"kty": "EC",
@@ -537,7 +541,7 @@ def test_cose_usage_examples_cose_encrypt_hpke_with_nonce(self):
537541
},
538542
},
539543
)
540-
r.apply(recipient_key=rpk)
544+
r.encode(enc_key.key, recipient_key=rpk)
541545
sender = COSE.new()
542546
with pytest.raises(ValueError) as err:
543547
sender.encode_and_encrypt(

tests/test_recipient.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -649,6 +649,7 @@ def test_recipients_open_with_empty_recipients(self, rsk1):
649649
assert "No recipients." in str(err.value)
650650

651651
def test_recipients_open_with_rpk_without_kid(self, rsk1, rsk2):
652+
enc_key = COSEKey.from_symmetric_key(alg="A128GCM")
652653
r = Recipient.new(protected={1: -1}, unprotected={-4: {1: 0x0010, 2: 0x0001, 3: 0x0001}})
653654
rpk = COSEKey.from_jwk(
654655
{
@@ -659,19 +660,21 @@ def test_recipients_open_with_rpk_without_kid(self, rsk1, rsk2):
659660
"y": "BGU5soLgsu_y7GN2I3EPUXS9EZ7Sw0qif-V70JtInFI",
660661
}
661662
)
662-
r.apply(recipient_key=rpk)
663+
r.encode(enc_key.key, recipient_key=rpk)
663664
sender = COSE.new()
664665
encoded = sender.encode_and_encrypt(
665666
b"This is the content.",
666-
# protected={
667-
# 1: -1, # alg: "HPKE"
668-
# },
667+
enc_key,
668+
protected={
669+
1: 1, # alg: "A128GCM"
670+
},
669671
recipients=[r],
670672
)
671673
recipient = COSE.new()
672674
assert b"This is the content." == recipient.decode(encoded, [rsk1, rsk2])
673675

674676
def test_recipients_open_with_verify_kid_and_rpk_without_kid(self, rsk1, rsk2):
677+
enc_key = COSEKey.from_symmetric_key(alg="A128GCM")
675678
r = Recipient.new(protected={1: -1}, unprotected={-4: {1: 0x0010, 2: 0x0001, 3: 0x0001}})
676679
rpk = COSEKey.from_jwk(
677680
{
@@ -682,7 +685,7 @@ def test_recipients_open_with_verify_kid_and_rpk_without_kid(self, rsk1, rsk2):
682685
"y": "BGU5soLgsu_y7GN2I3EPUXS9EZ7Sw0qif-V70JtInFI",
683686
}
684687
)
685-
r.apply(recipient_key=rpk)
688+
r.encode(enc_key.key, recipient_key=rpk)
686689
sender = COSE.new()
687690
encoded = sender.encode_and_encrypt(
688691
b"This is the content.",
@@ -698,6 +701,7 @@ def test_recipients_open_with_verify_kid_and_rpk_without_kid(self, rsk1, rsk2):
698701
assert "kid should be specified in recipient." in str(err.value)
699702

700703
def test_recipients_open_failed_with_rpk_without_kid(self, rsk1):
704+
enc_key = COSEKey.from_symmetric_key(alg="A128GCM")
701705
r = Recipient.new(protected={1: -1}, unprotected={-4: {1: 0x0010, 2: 0x0001, 3: 0x0001}})
702706
rpk = COSEKey.from_jwk(
703707
{
@@ -708,7 +712,7 @@ def test_recipients_open_failed_with_rpk_without_kid(self, rsk1):
708712
"y": "BGU5soLgsu_y7GN2I3EPUXS9EZ7Sw0qif-V70JtInFI",
709713
}
710714
)
711-
r.apply(recipient_key=rpk)
715+
r.encode(enc_key.key, recipient_key=rpk)
712716
sender = COSE.new()
713717
encoded = sender.encode_and_encrypt(
714718
b"This is the content.",
@@ -724,11 +728,13 @@ def test_recipients_open_failed_with_rpk_without_kid(self, rsk1):
724728
assert "Failed to open." in str(err.value)
725729

726730
def test_recipients_open_with_multiple_rsks(self, rpk2, rsk1, rsk2):
731+
enc_key = COSEKey.from_symmetric_key(alg="A128GCM")
727732
r = Recipient.new(protected={1: -1}, unprotected={4: b"02", -4: {1: 0x0010, 2: 0x0001, 3: 0x0001}})
728-
r.apply(recipient_key=rpk2)
729-
sender = COSE.new()
733+
r.encode(enc_key.key, recipient_key=rpk2)
734+
sender = COSE.new(alg_auto_inclusion=True)
730735
encoded = sender.encode_and_encrypt(
731736
b"This is the content.",
737+
key=enc_key,
732738
# protected={
733739
# 1: -1, # alg: "HPKE"
734740
# },
@@ -738,8 +744,9 @@ def test_recipients_open_with_multiple_rsks(self, rpk2, rsk1, rsk2):
738744
assert b"This is the content." == recipient.decode(encoded, [rsk1, rsk2])
739745

740746
def test_recipients_open_with_invalid_rsk(self, rpk1):
747+
enc_key = COSEKey.from_symmetric_key(alg="A128GCM")
741748
r = Recipient.new(protected={1: -1}, unprotected={4: b"02", -4: {1: 0x0010, 2: 0x0001, 3: 0x0001}})
742-
r.apply(recipient_key=rpk1)
749+
r.encode(enc_key.key, recipient_key=rpk1)
743750
sender = COSE.new()
744751
encoded = sender.encode_and_encrypt(
745752
b"This is the content.",

tests/test_recipient_algs_hpke.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
import pytest
66

7+
from cwt import COSEKey
78
from cwt.recipient_algs.hpke import HPKE
89

910

@@ -18,9 +19,10 @@ def test_recipient_algs_hpke(self):
1819
assert ctx.alg == -1
1920

2021
def test_recipient_algs_hpke_apply_without_recipient_key(self):
22+
enc_key = COSEKey.from_symmetric_key(alg="A128GCM")
2123
ctx = HPKE({1: -1}, {-4: {1: 0x0010, 2: 0x0001, 3: 0x0001}})
2224
with pytest.raises(ValueError) as err:
23-
ctx.apply()
25+
ctx.encode(enc_key.key)
2426
pytest.fail("apply should fail.")
2527
assert "recipient_key should be set." in str(err.value)
2628

0 commit comments

Comments
 (0)