Skip to content

Commit 281c742

Browse files
authored
Merge pull request #463 from kentakayama/fix-generate-zero-length-protected-header-for-non-aead
Generate zero-length protected header for non AEAD
2 parents 10f7efc + 3392e5e commit 281c742

3 files changed

Lines changed: 56 additions & 16 deletions

File tree

cwt/const.py

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,9 @@
118118
"verifyMAC": 10, # JWK-like lowerCamelCase
119119
}
120120

121-
# COSE Algorithms for Content Encryption Key (CEK).
122-
COSE_ALGORITHMS_CEK = {
123-
"A128CTR": -65534, # AES-CTR mode w/ 128-bit key (Deprecated)
124-
"A192CTR": -65533, # AES-CTR mode w/ 192-bit key (Deprecated)
125-
"A256CTR": -65532, # AES-CTR mode w/ 256-bit key (Deprecated)
126-
"A128CBC": -65531, # AES-CBC mode w/ 128-bit key (Deprecated)
127-
"A192CBC": -65530, # AES-CBC mode w/ 192-bit key (Deprecated)
128-
"A256CBC": -65529, # AES-CBC mode w/ 256-bit key (Deprecated)
121+
122+
# COSE AEAD Algorithms
123+
COSE_ALGORITHMS_CEK_AEAD = {
129124
"A128GCM": 1, # AES-GCM mode w/ 128-bit key, 128-bit tag
130125
"A192GCM": 2, # AES-GCM mode w/ 192-bit key, 128-bit tag
131126
"A256GCM": 3, # AES-GCM mode w/ 256-bit key, 128-bit tag
@@ -141,6 +136,22 @@
141136
# etc.
142137
}
143138

139+
# COSE non AEAD Algorithms defined in RFC9459
140+
COSE_ALGORITHMS_CEK_NON_AEAD = {
141+
"A128CTR": -65534, # AES-CTR mode w/ 128-bit key (Deprecated)
142+
"A192CTR": -65533, # AES-CTR mode w/ 192-bit key (Deprecated)
143+
"A256CTR": -65532, # AES-CTR mode w/ 256-bit key (Deprecated)
144+
"A128CBC": -65531, # AES-CBC mode w/ 128-bit key (Deprecated)
145+
"A192CBC": -65530, # AES-CBC mode w/ 192-bit key (Deprecated)
146+
"A256CBC": -65529, # AES-CBC mode w/ 256-bit key (Deprecated)
147+
}
148+
149+
# COSE Algorithms for Content Encryption Key (CEK).
150+
COSE_ALGORITHMS_CEK = {
151+
**COSE_ALGORITHMS_CEK_AEAD,
152+
**COSE_ALGORITHMS_CEK_NON_AEAD,
153+
}
154+
144155
COSE_KEY_LEN = {
145156
-65534: 128, # AES-CTR w/ 128-bit key (Deprecated)
146157
-65533: 192, # AES-CTR w/ 192-bit key (Deprecated)

cwt/cose.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from .cbor_processor import CBORProcessor
77
from .const import (
88
COSE_ALGORITHMS_CEK,
9+
COSE_ALGORITHMS_CEK_NON_AEAD,
910
COSE_ALGORITHMS_CKDM,
1011
COSE_ALGORITHMS_CKDM_KEY_AGREEMENT,
1112
COSE_ALGORITHMS_CKDM_KEY_AGREEMENT_DIRECT,
@@ -393,7 +394,7 @@ def decode_with_headers(
393394
# if not isinstance(unprotected, dict):
394395
# raise ValueError("unprotected header should be dict.")
395396
p, u = self._decode_headers(data.value[0], data.value[1])
396-
alg = self._get_alg(p)
397+
alg = p[1] if 1 in p else u.get(1, 0)
397398

398399
# Local variable `protected` is byte encoded protected header
399400
# Sender is allowed to encode empty protected header into a bstr-wrapped zero-length map << {} >> (0x40A0)
@@ -553,9 +554,18 @@ def _encode_headers(
553554
u = to_cose_header(unprotected)
554555
if key is not None:
555556
if self._alg_auto_inclusion:
556-
p[1] = key.alg
557+
if key.alg in COSE_ALGORITHMS_CEK_NON_AEAD.values():
558+
u[1] = key.alg
559+
else:
560+
p[1] = key.alg
557561
if self._kid_auto_inclusion and key.kid:
558562
u[4] = key.kid
563+
564+
# Check the protected header is empty if the algorithm is non AEAD (AES-CBC or AES-CTR)
565+
# because section 4 of RFC9459 says "The 'protected' header MUST be a zero-length byte string."
566+
alg = p[1] if 1 in p else u.get(1, 0)
567+
if alg in COSE_ALGORITHMS_CEK_NON_AEAD.values() and len(p) > 0:
568+
raise ValueError("protected header MUST be zero-length")
559569
return p, u
560570

561571
def _decode_headers(self, protected: Any, unprotected: Any) -> Tuple[Dict[int, Any], Dict[int, Any]]:

tests/test_recipient.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,25 @@ def test_recipients_aes(self, kw_alg, enc_alg):
797797
kw_key = COSEKey.from_symmetric_key(alg=kw_alg)
798798
enc_key = COSEKey.from_symmetric_key(alg=enc_alg)
799799

800+
# The sender side (must fail):
801+
with pytest.raises(ValueError) as err:
802+
r = Recipient.new(protected={"alg": kw_alg}, sender_key=kw_key)
803+
pytest.fail("encode_and_encrypt() should fail.")
804+
assert "The protected header must be a zero-length string in key wrap mode with an AE algorithm." in str(err.value)
805+
806+
# The sender side (must fail):
807+
r = Recipient.new(unprotected={"alg": kw_alg}, sender_key=kw_key)
808+
sender = COSE.new(alg_auto_inclusion=True)
809+
with pytest.raises(ValueError) as err:
810+
encoded = sender.encode_and_encrypt(
811+
b"Hello world!",
812+
enc_key,
813+
protected={"kid": "actually-not-protected"},
814+
recipients=[r],
815+
)
816+
pytest.fail("encode_and_encrypt() should fail.")
817+
assert "protected header MUST be zero-length" in str(err.value)
818+
800819
# The sender side:
801820
r = Recipient.new(unprotected={"alg": kw_alg}, sender_key=kw_key)
802821
sender = COSE.new(alg_auto_inclusion=True)
@@ -832,13 +851,13 @@ def test_recipients_hpke(self, rsk1, rsk2, enc_alg):
832851
"y": "BGU5soLgsu_y7GN2I3EPUXS9EZ7Sw0qif-V70JtInFI",
833852
}
834853
)
835-
r = Recipient.new(protected={1: 35}, recipient_key=rpk)
854+
r = Recipient.new(unprotected={1: 35}, recipient_key=rpk)
836855
r.encode(enc_key.key)
837856
sender = COSE.new()
838857
encoded = sender.encode_and_encrypt(
839858
b"This is the content.",
840859
enc_key,
841-
protected={"alg": enc_alg},
860+
unprotected={"alg": enc_alg},
842861
recipients=[r],
843862
)
844863
recipient = COSE.new()
@@ -861,7 +880,7 @@ def test_recipients_ecdh_es(self, key_agreement_alg, key_agreement_alg_id, kw_al
861880
"alg": kw_alg,
862881
"supp_pub": {
863882
"key_data_length": len(enc_key.key) * 8,
864-
"protected": {1: key_agreement_alg_id},
883+
"protected": {},
865884
},
866885
}
867886

@@ -886,15 +905,15 @@ def test_recipients_ecdh_es(self, key_agreement_alg, key_agreement_alg_id, kw_al
886905
"y": "BGU5soLgsu_y7GN2I3EPUXS9EZ7Sw0qif-V70JtInFI",
887906
}
888907
)
889-
r = Recipient.new(protected={"alg": key_agreement_alg}, sender_key=rsk1, recipient_key=rpk2, context=context)
908+
r = Recipient.new(unprotected={"alg": key_agreement_alg}, sender_key=rsk1, recipient_key=rpk2, context=context)
890909

891910
nonce = enc_key.generate_nonce()
892911
sender = COSE.new()
893912
encoded = sender.encode(
894913
b"Hello world!",
895914
enc_key,
896-
protected={"alg": enc_alg},
897-
unprotected={"iv": nonce},
915+
protected={},
916+
unprotected={"alg": enc_alg, "iv": nonce},
898917
recipients=[r],
899918
)
900919

0 commit comments

Comments
 (0)