Skip to content

Commit 98d1ef9

Browse files
authored
Merge pull request #441 from kentakayama/add-aes-ctr-and-cbc-support
Add AES-CTR and AES-CBC support
2 parents cd6bc63 + 94deb1a commit 98d1ef9

9 files changed

Lines changed: 724 additions & 2 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1744,6 +1744,7 @@ Python CWT is (partially) compliant with following specifications:
17441744
- [RFC8747: Proof-of-Possession Key Semantics for CBOR Web Tokens (CWTs)](https://tools.ietf.org/html/rfc8747)
17451745
- [RFC8392: CWT (CBOR Web Token)](https://tools.ietf.org/html/rfc8392)
17461746
- [RFC8230: Using RSA Algorithms with COSE Messages](https://tools.ietf.org/html/rfc8230)
1747+
- [RFC9459: CBOR Object Signing and Encryption (COSE): AES-CTR and AES-CBC](https://www.rfc-editor.org/rfc/rfc9459.html) - experimental
17471748
- [RFC8152: CBOR Object Signing and Encryption (COSE)](https://tools.ietf.org/html/rfc8152)
17481749
- [draft-05: Use of HPKE with COSE](https://www.ietf.org/archive/id/draft-ietf-cose-hpke-06.html) - experimental
17491750
- [draft-06: CWT Claims in COSE Headers](https://www.ietf.org/archive/id/draft-ietf-cose-cwt-claims-in-headers-06.html) - experimental

cwt/algs/non_aead.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import os
2+
3+
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
4+
5+
6+
class AESCTR:
7+
_MAX_SIZE = 2**31 - 1
8+
9+
def __init__(self, key: bytes):
10+
if len(key) not in (16, 24, 32):
11+
raise ValueError("AESCTR key must be 128, 192, or 256 bits.")
12+
13+
self._key = key
14+
15+
@classmethod
16+
def generate_key(cls, bit_length: int) -> bytes:
17+
if not isinstance(bit_length, int):
18+
raise TypeError("bit_length must be an integer")
19+
20+
if bit_length not in (128, 192, 256):
21+
raise ValueError("bit_length must be 128, 192, or 256")
22+
23+
return os.urandom(bit_length // 8)
24+
25+
def encrypt(
26+
self,
27+
nonce: bytes,
28+
data: bytes,
29+
) -> bytes:
30+
encryptor = Cipher(
31+
algorithms.AES(self._key),
32+
modes.CTR(nonce),
33+
).encryptor()
34+
35+
return encryptor.update(data) + encryptor.finalize()
36+
37+
def decrypt(
38+
self,
39+
nonce: bytes,
40+
data: bytes,
41+
) -> bytes:
42+
decryptor = Cipher(
43+
algorithms.AES(self._key),
44+
modes.CTR(nonce),
45+
).decryptor()
46+
47+
return decryptor.update(data) + decryptor.finalize()
48+
49+
50+
class AESCBC:
51+
_MAX_SIZE = 2**31 - 1
52+
53+
def __init__(self, key: bytes):
54+
if len(key) not in (16, 24, 32):
55+
raise ValueError("AESCBC key must be 128, 192, or 256 bits.")
56+
57+
self._key = key
58+
59+
@classmethod
60+
def generate_key(cls, bit_length: int) -> bytes:
61+
if not isinstance(bit_length, int):
62+
raise TypeError("bit_length must be an integer")
63+
64+
if bit_length not in (128, 192, 256):
65+
raise ValueError("bit_length must be 128, 192, or 256")
66+
67+
return os.urandom(bit_length // 8)
68+
69+
def encrypt(
70+
self,
71+
nonce: bytes,
72+
data: bytes,
73+
) -> bytes:
74+
encryptor = Cipher(
75+
algorithms.AES(self._key),
76+
modes.CBC(nonce),
77+
).encryptor()
78+
79+
return encryptor.update(data) + encryptor.finalize()
80+
81+
def decrypt(
82+
self,
83+
nonce: bytes,
84+
data: bytes,
85+
) -> bytes:
86+
decryptor = Cipher(
87+
algorithms.AES(self._key),
88+
modes.CBC(nonce),
89+
).decryptor()
90+
91+
return decryptor.update(data) + decryptor.finalize()

cwt/algs/symmetric.py

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
from ..const import COSE_KEY_OPERATION_VALUES
1010
from ..cose_key_interface import COSEKeyInterface
1111
from ..exceptions import DecodeError, EncodeError, VerifyError
12+
from .non_aead import AESCBC, AESCTR
1213

1314
_CWT_DEFAULT_KEY_SIZE_HMAC256 = 32 # bytes
1415
_CWT_DEFAULT_KEY_SIZE_HMAC384 = 48
1516
_CWT_DEFAULT_KEY_SIZE_HMAC512 = 64
1617
_CWT_NONCE_SIZE_AESGCM = 12
1718
_CWT_NONCE_SIZE_CHACHA20_POLY1305 = 12
19+
_CWT_NONCE_SIZE_AES = 16
1820

1921

2022
class SymmetricKey(COSEKeyInterface):
@@ -353,3 +355,107 @@ def unwrap_key(self, wrapped_key: bytes) -> bytes:
353355
return aes_key_unwrap(self._key, wrapped_key)
354356
except Exception as err:
355357
raise DecodeError("Failed to unwrap key.") from err
358+
359+
360+
class AESCTRKey(ContentEncryptionKey):
361+
""" """
362+
363+
def __init__(self, params: Dict[int, Any]):
364+
""" """
365+
super().__init__(params)
366+
367+
self._cipher: AESCTR
368+
369+
# Validate alg.
370+
if self._alg == -65534: # A128CTR
371+
if not self._key:
372+
self._key = AESCTR.generate_key(bit_length=128)
373+
if len(self._key) != 16:
374+
raise ValueError("The length of A128CTR key should be 16 bytes.")
375+
elif self._alg == -65533: # A192CTR
376+
if not self._key:
377+
self._key = AESCTR.generate_key(bit_length=192)
378+
if len(self._key) != 24:
379+
raise ValueError("The length of A192CTR key should be 24 bytes.")
380+
elif self._alg == -65532: # A256CTR
381+
if not self._key:
382+
self._key = AESCTR.generate_key(bit_length=256)
383+
if len(self._key) != 32:
384+
raise ValueError("The length of A256CTR key should be 32 bytes.")
385+
else:
386+
raise ValueError(f"Unsupported or unknown alg(3) for AES CTR: {self._alg}.")
387+
388+
self._cipher = AESCTR(self._key)
389+
return
390+
391+
def generate_nonce(self):
392+
return token_bytes(_CWT_NONCE_SIZE_AES)
393+
394+
def encrypt(self, msg: bytes, nonce: bytes, aad: Optional[bytes] = None) -> bytes:
395+
""" """
396+
try:
397+
return self._cipher.encrypt(nonce, msg)
398+
except Exception as err:
399+
raise EncodeError("Failed to encrypt.") from err
400+
401+
def decrypt(self, msg: bytes, nonce: bytes, aad: Optional[bytes] = None) -> bytes:
402+
""" """
403+
try:
404+
return self._cipher.decrypt(nonce, msg)
405+
except Exception as err:
406+
raise DecodeError("Failed to decrypt.") from err
407+
408+
409+
class AESCBCKey(ContentEncryptionKey):
410+
""" """
411+
412+
def __init__(self, params: Dict[int, Any]):
413+
""" """
414+
super().__init__(params)
415+
416+
self._cipher: AESCBC
417+
418+
# Validate alg.
419+
if self._alg == -65531: # A128CBC
420+
if not self._key:
421+
self._key = AESCBC.generate_key(bit_length=128)
422+
if len(self._key) != 16:
423+
raise ValueError("The length of A128CBC key should be 16 bytes.")
424+
elif self._alg == -65530: # A192CBC
425+
if not self._key:
426+
self._key = AESCBC.generate_key(bit_length=192)
427+
if len(self._key) != 24:
428+
raise ValueError("The length of A192CBC key should be 24 bytes.")
429+
elif self._alg == -65529: # A256CBC
430+
if not self._key:
431+
self._key = AESCBC.generate_key(bit_length=256)
432+
if len(self._key) != 32:
433+
raise ValueError("The length of A256CBC key should be 32 bytes.")
434+
else:
435+
raise ValueError(f"Unsupported or unknown alg(3) for AES CBC: {self._alg}.")
436+
437+
self._cipher = AESCBC(self._key)
438+
return
439+
440+
def generate_nonce(self):
441+
return token_bytes(_CWT_NONCE_SIZE_AES)
442+
443+
def encrypt(self, msg: bytes, nonce: bytes, aad: Optional[bytes] = None) -> bytes:
444+
""" """
445+
try:
446+
# Add padding (see RFC 9459 and 5652)
447+
padding_value = 16 - len(msg) % 16
448+
padding_length = 16 if padding_value == 0 else padding_value
449+
padding = (padding_value).to_bytes(1, "big") * padding_length
450+
return self._cipher.encrypt(nonce, msg + padding)
451+
except Exception as err:
452+
raise EncodeError("Failed to encrypt.") from err
453+
454+
def decrypt(self, msg: bytes, nonce: bytes, aad: Optional[bytes] = None) -> bytes:
455+
""" """
456+
try:
457+
decrypted = self._cipher.decrypt(nonce, msg)
458+
# Remove padding (see RFC 9459 and 5652)
459+
return decrypted[0 : -(decrypted[-1])]
460+
except Exception as err:
461+
raise DecodeError("Failed to decrypt.") from err

cwt/const.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,12 @@
120120

121121
# COSE Algorithms for Content Encryption Key (CEK).
122122
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)
123129
"A128GCM": 1, # AES-GCM mode w/ 128-bit key, 128-bit tag
124130
"A192GCM": 2, # AES-GCM mode w/ 192-bit key, 128-bit tag
125131
"A256GCM": 3, # AES-GCM mode w/ 256-bit key, 128-bit tag
@@ -136,6 +142,12 @@
136142
}
137143

138144
COSE_KEY_LEN = {
145+
-65534: 128, # AES-CTR w/ 128-bit key (Deprecated)
146+
-65533: 192, # AES-CTR w/ 192-bit key (Deprecated)
147+
-65532: 256, # AES-CTR w/ 256-bit key (Deprecated)
148+
-65531: 128, # AES-CBC w/ 128-bit key (Deprecated)
149+
-65530: 192, # AES-CBC w/ 192-bit key (Deprecated)
150+
-65529: 256, # AES-CBC w/ 256-bit key (Deprecated)
139151
-5: 256, # AES Key Wrap w/ 256-bit key
140152
-4: 192, # AES Key Wrap w/ 192-bit key
141153
-3: 128, # AES Key Wrap w/ 128-bit key

cwt/cose_key.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,15 @@
1616
from .algs.okp import OKPKey
1717
from .algs.raw import RawKey
1818
from .algs.rsa import RSAKey
19-
from .algs.symmetric import AESCCMKey, AESGCMKey, AESKeyWrap, ChaCha20Key, HMACKey
19+
from .algs.symmetric import (
20+
AESCBCKey,
21+
AESCCMKey,
22+
AESCTRKey,
23+
AESGCMKey,
24+
AESKeyWrap,
25+
ChaCha20Key,
26+
HMACKey,
27+
)
2028
from .const import (
2129
COSE_ALGORITHMS_CKDM_KEY_AGREEMENT,
2230
COSE_ALGORITHMS_RSA,
@@ -76,6 +84,10 @@ def new(params: Dict[int, Any]) -> COSEKeyInterface:
7684
return ChaCha20Key(params)
7785
if params[COSEKeyParams.ALG] in [-3, -4, -5]:
7886
return AESKeyWrap(params)
87+
if params[COSEKeyParams.ALG] in [-65534, -65533, -65532]:
88+
return AESCTRKey(params)
89+
if params[COSEKeyParams.ALG] in [-65531, -65530, -65529]:
90+
return AESCBCKey(params)
7991
raise ValueError(f"Unsupported or unknown alg(3): {params[3]}.")
8092

8193
@classmethod

cwt/enums.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,12 @@ class COSEKeyParams(enum.IntEnum):
6060

6161

6262
class COSEAlgs(enum.IntEnum):
63+
A128CTR = -65534
64+
A192CTR = -65533
65+
A256CTR = -65532
66+
A128CBC = -65531
67+
A192CBC = -65530
68+
A256CBC = -65529
6369
RS512 = -259
6470
RS384 = -258
6571
RS256 = -257

docs/algorithms.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,30 @@ COSE Key Types
3030
COSE Algorithms
3131
---------------
3232

33+
-65534: 128, # AES-CTR w/ 128-bit key (Deprecated)
34+
-65533: 192, # AES-CTR w/ 192-bit key (Deprecated)
35+
-65532: 256, # AES-CTR w/ 256-bit key (Deprecated)
36+
-65531: 128, # AES-CBC w/ 128-bit key (Deprecated)
37+
-65530: 192, # AES-CBC w/ 192-bit key (Deprecated)
38+
-65529: 256, # AES-CBC w/ 256-bit key (Deprecated)
39+
3340
+------------------------+--------+-------+-----------------------------------------------------+
3441
| Name | Status | Value | Description |
3542
+========================+========+=======+=====================================================+
3643
| RS1 || -65535| RSASSA-PKCS1-v1_5 using SHA-1 |
3744
+------------------------+--------+-------+-----------------------------------------------------+
45+
| A128CTR || -65534| AES-CTR w/ 128-bit key |
46+
+------------------------+--------+-------+-----------------------------------------------------+
47+
| A192CTR || -65533| AES-CTR w/ 192-bit key |
48+
+------------------------+--------+-------+-----------------------------------------------------+
49+
| A256CTR || -65532| AES-CTR w/ 256-bit key |
50+
+------------------------+--------+-------+-----------------------------------------------------+
51+
| A128CBC || -65531| AES-CBC w/ 128-bit key |
52+
+------------------------+--------+-------+-----------------------------------------------------+
53+
| A192CBC || -65530| AES-CBC w/ 192-bit key |
54+
+------------------------+--------+-------+-----------------------------------------------------+
55+
| A256CBC || -65529| AES-CBC w/ 256-bit key |
56+
+------------------------+--------+-------+-----------------------------------------------------+
3857
| WalnutDSA | | -260 | WalnutDSA signature |
3958
+------------------------+--------+-------+-----------------------------------------------------+
4059
| RS512 || -259 | RSASSA-PKCS1-v1_5 using SHA-512 |

0 commit comments

Comments
 (0)