From f80e80989f1987b1a6e5e37c8be89723db0e6ea4 Mon Sep 17 00:00:00 2001 From: "Ajitomi, Daisuke" Date: Sun, 18 Apr 2021 19:18:06 +0900 Subject: [PATCH 1/9] Fix name of HMAC algs. --- README.md | 2 +- cwt/const.py | 8 ++++---- cwt/key_builder.py | 2 +- cwt/key_types/symmetric.py | 8 ++++---- docs/usage.rst | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index f7942cd..75f858c 100644 --- a/README.md +++ b/README.md @@ -38,7 +38,7 @@ Create a MACed CWT, verify and decode it as follows: import cwt from cwt import cose_key, claims -key = cose_key.from_symmetric_key("mysecretpassword") # Default algorithm is "HMAC256/256" +key = cose_key.from_symmetric_key("mysecretpassword") # Default algorithm is "HMAC 256/256" encoded = cwt.encode_and_mac( claims.from_json( {"iss": "https://as.example", "sub": "dajiaji", "cti": "123"} diff --git a/cwt/const.py b/cwt/const.py index 5341479..2de7b23 100644 --- a/cwt/const.py +++ b/cwt/const.py @@ -25,10 +25,10 @@ # COSE Algorithms for MAC. COSE_ALGORITHMS_MAC = { - "HMAC256/64": 4, # HMAC w/ SHA-256 truncated to 64 bits - "HMAC256/256": 5, # HMAC w/ SHA-256 - "HMAC384/384": 6, # HMAC w/ SHA-384 - "HMAC512/512": 7, # HMAC w/ SHA-512 + "HMAC 256/64": 4, # HMAC w/ SHA-256 truncated to 64 bits + "HMAC 256/256": 5, # HMAC w/ SHA-256 + "HMAC 384/384": 6, # HMAC w/ SHA-384 + "HMAC 512/512": 7, # HMAC w/ SHA-512 "AES-MAC128/64": 14, # AES-MAC 128-bit key, 64-bit tag "AES-MAC256/64": 15, # AES-MAC 256-bit key, 64-bit tag "AES-MAC128/128": 25, # AES-MAC 128-bit key, 128-bit tag diff --git a/cwt/key_builder.py b/cwt/key_builder.py index a1fd6a4..1fcc19d 100644 --- a/cwt/key_builder.py +++ b/cwt/key_builder.py @@ -57,7 +57,7 @@ def __init__(self, options: Optional[Dict[str, Any]] = None): return def from_symmetric_key( - self, key: Union[bytes, str], alg: str = "HMAC256/256" + self, key: Union[bytes, str], alg: str = "HMAC 256/256" ) -> COSEKey: """""" if isinstance(key, str): diff --git a/cwt/key_types/symmetric.py b/cwt/key_types/symmetric.py index be1bbaa..a3a5708 100644 --- a/cwt/key_types/symmetric.py +++ b/cwt/key_types/symmetric.py @@ -47,16 +47,16 @@ def __init__(self, cose_key: Dict[int, Any]): self._trunc = 0 # Validate alg. - if self._alg == 4: # HMAC256/64 + if self._alg == 4: # HMAC 256/64 self._hash_alg = hashlib.sha256 self._trunc = 8 - elif self._alg == 5: # HMAC256/256 + elif self._alg == 5: # HMAC 256/256 self._hash_alg = hashlib.sha256 self._trunc = 32 - elif self._alg == 6: # HMAC384/384 + elif self._alg == 6: # HMAC 384/384 self._hash_alg = hashlib.sha384 self._trunc = 48 - elif self._alg == 7: # HMAC512/512 + elif self._alg == 7: # HMAC 512/512 self._hash_alg = hashlib.sha512 self._trunc = 64 else: diff --git a/docs/usage.rst b/docs/usage.rst index d03b059..0977e73 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -13,7 +13,7 @@ Create a MACed CWT, verify and decode it as follows: key = cose_key.from_symmetric_key( "mysecretpassword" - ) # Default algorithm is "HMAC256/256" + ) # Default algorithm is "HMAC 256/256" encoded = cwt.encode_and_mac( claims.from_json({"iss": "https://as.example", "sub": "dajiaji", "cti": "123"}), key, From ab7aa64a789c4ffa2c2418860865e415d8d3bfbd Mon Sep 17 00:00:00 2001 From: "Ajitomi, Daisuke" Date: Sun, 18 Apr 2021 19:50:10 +0900 Subject: [PATCH 2/9] Make COSEKey public. --- cwt/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cwt/__init__.py b/cwt/__init__.py index f197d2a..57ce422 100644 --- a/cwt/__init__.py +++ b/cwt/__init__.py @@ -1,3 +1,4 @@ +from .cose_key import COSEKey from .claims import Claims, claims from .cose import COSE from .cwt import CWT, decode, encode_and_encrypt, encode_and_mac, encode_and_sign @@ -21,8 +22,9 @@ "decode", "CWT", "COSE", - "KeyBuilder", "Claims", + "COSEKey", + "KeyBuilder", "cose_key", "claims", "CWTError", From f33dc489bb65f171d78efec5702df677b2da1c8a Mon Sep 17 00:00:00 2001 From: "Ajitomi, Daisuke" Date: Sun, 18 Apr 2021 20:09:12 +0900 Subject: [PATCH 3/9] Rename InvalidSignatureError to VerifyError. --- cwt/__init__.py | 4 ++-- cwt/cose.py | 6 +++--- cwt/exceptions.py | 4 ++-- cwt/key_types/ec2.py | 10 +++++----- cwt/key_types/okp.py | 6 +++--- cwt/key_types/symmetric.py | 7 +++++-- 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/cwt/__init__.py b/cwt/__init__.py index 57ce422..ce46970 100644 --- a/cwt/__init__.py +++ b/cwt/__init__.py @@ -2,7 +2,7 @@ from .claims import Claims, claims from .cose import COSE from .cwt import CWT, decode, encode_and_encrypt, encode_and_mac, encode_and_sign -from .exceptions import CWTError, DecodeError, EncodeError, InvalidSignatureError +from .exceptions import CWTError, DecodeError, EncodeError, VerifyError from .key_builder import KeyBuilder, cose_key __version__ = "0.1.1" @@ -30,5 +30,5 @@ "CWTError", "EncodeError", "DecodeError", - "InvalidSignatureError", + "VerifyError", ] diff --git a/cwt/cose.py b/cwt/cose.py index 1ada677..6a13550 100644 --- a/cwt/cose.py +++ b/cwt/cose.py @@ -3,7 +3,7 @@ from cbor2 import CBORTag, dumps, loads from .cose_key import COSEKey -from .exceptions import InvalidSignatureError +from .exceptions import VerifyError class COSE: @@ -114,7 +114,7 @@ def decode(self, data: Union[bytes, CBORTag], key: COSEKey) -> Dict[int, Any]: msg = dumps(["MAC0", data.value[0], b"", data.value[2]]) try: key.verify(msg, data.value[3]) - except InvalidSignatureError: + except VerifyError: raise return loads(data.value[2]) @@ -130,7 +130,7 @@ def decode(self, data: Union[bytes, CBORTag], key: COSEKey) -> Dict[int, Any]: msg = dumps(["Signature1", data.value[0], b"", data.value[2]]) try: key.verify(msg, data.value[3]) - except InvalidSignatureError: + except VerifyError: raise return loads(data.value[2]) diff --git a/cwt/exceptions.py b/cwt/exceptions.py index dd26bc8..0222af8 100644 --- a/cwt/exceptions.py +++ b/cwt/exceptions.py @@ -6,9 +6,9 @@ class CWTError(Exception): pass -class InvalidSignatureError(CWTError): +class VerifyError(CWTError): """ - An Exception occurred when a signature verification process failed. + An Exception occurred when a verification process failed. """ pass diff --git a/cwt/key_types/ec2.py b/cwt/key_types/ec2.py index cfdf29a..dd1e1ca 100644 --- a/cwt/key_types/ec2.py +++ b/cwt/key_types/ec2.py @@ -9,7 +9,7 @@ ) from ..cose_key import COSEKey -from ..exceptions import InvalidSignatureError +from ..exceptions import VerifyError from ..utils import i2osp, os2ip @@ -106,7 +106,7 @@ def sign(self, msg: bytes) -> bytes: sig = self._private_key.sign(msg, ec.ECDSA(self._hash_alg())) return self._der_to_os(self._private_key.curve.key_size, sig) except ValueError as err: - raise InvalidSignatureError("Failed to sign.") from err + raise VerifyError("Failed to sign.") from err def verify(self, msg: bytes, sig: bytes): """""" @@ -119,10 +119,10 @@ def verify(self, msg: bytes, sig: bytes): else: der_sig = self._os_to_der(self._public_key.curve.key_size, sig) self._public_key.verify(der_sig, msg, ec.ECDSA(self._hash_alg())) - except cryptography.exceptions.InvalidSignature as err: - raise InvalidSignatureError("Failed to verify.") from err + except cryptography.exceptions.Verify as err: + raise VerifyError("Failed to verify.") from err except ValueError as err: - raise InvalidSignatureError("Invalid signature.") from err + raise VerifyError("Invalid signature.") from err def _der_to_os(self, key_size: int, sig: bytes) -> bytes: """""" diff --git a/cwt/key_types/okp.py b/cwt/key_types/okp.py index 56d76ac..5c765ce 100644 --- a/cwt/key_types/okp.py +++ b/cwt/key_types/okp.py @@ -16,7 +16,7 @@ ) from ..cose_key import COSEKey -from ..exceptions import InvalidSignatureError +from ..exceptions import VerifyError class OKPKey(COSEKey): @@ -87,8 +87,8 @@ def sign(self, msg: bytes) -> bytes: if self._public_key: raise ValueError("Public key cannot be used for signing.") return self._private_key.sign(msg) - except cryptography.exceptions.InvalidSignature as err: - raise InvalidSignatureError("Failed to verify.") from err + except cryptography.exceptions.Verify as err: + raise VerifyError("Failed to verify.") from err def verify(self, msg: bytes, sig: bytes) -> bool: """""" diff --git a/cwt/key_types/symmetric.py b/cwt/key_types/symmetric.py index a3a5708..61a9653 100644 --- a/cwt/key_types/symmetric.py +++ b/cwt/key_types/symmetric.py @@ -5,6 +5,7 @@ from cryptography.hazmat.primitives.ciphers.aead import AESCCM from ..cose_key import COSEKey +from ..exceptions import VerifyError class SymmetricKey(COSEKey): @@ -66,9 +67,11 @@ def sign(self, msg: bytes) -> bytes: """""" return hmac.new(self._key, msg, self._hash_alg).digest()[0 : self._trunc] - def verify(self, msg: bytes, sig: bytes) -> bool: + def verify(self, msg: bytes, sig: bytes): """""" - return hmac.compare_digest(sig, self.sign(msg)) + if hmac.compare_digest(sig, self.sign(msg)): + return + raise VerifyError("Failed to compare digest.") class AESCCMKey(SymmetricKey): From 8f2d2205f79a91b04143f4cf0b3eca22ab6b7dc4 Mon Sep 17 00:00:00 2001 From: "Ajitomi, Daisuke" Date: Sun, 18 Apr 2021 20:12:56 +0900 Subject: [PATCH 4/9] Update docstring for VerifyError. --- cwt/cwt.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cwt/cwt.py b/cwt/cwt.py index 2e50f94..61abadd 100644 --- a/cwt/cwt.py +++ b/cwt/cwt.py @@ -122,8 +122,8 @@ def decode(self, data: bytes, key: Union[COSEKey, List[COSEKey]]) -> bytes: bytes: A byte string of the decoded CWT. Raises: ValueError: Invalid arguments. - DecodeError: Failed to decode the claims. - InvalidSignatureError: Failed to verify the signature. + DecodeError: Failed to decode the CWT. + VerifyError: Failed to verify the CWT. """ cwt = loads(data) if isinstance(cwt, CBORTag) and cwt.tag == CWT.CBOR_TAG: From c460b95bfcf8f44ff61d8864e014289f6f675a15 Mon Sep 17 00:00:00 2001 From: "Ajitomi, Daisuke" Date: Sun, 18 Apr 2021 21:35:31 +0900 Subject: [PATCH 5/9] Remove redundant try/catch. --- cwt/cose.py | 10 ++-------- cwt/key_types/okp.py | 13 ++++++------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/cwt/cose.py b/cwt/cose.py index 6a13550..ade21e4 100644 --- a/cwt/cose.py +++ b/cwt/cose.py @@ -112,10 +112,7 @@ def decode(self, data: Union[bytes, CBORTag], key: COSEKey) -> Dict[int, Any]: raise ValueError("Invalid MAP0 format.") msg = dumps(["MAC0", data.value[0], b"", data.value[2]]) - try: - key.verify(msg, data.value[3]) - except VerifyError: - raise + key.verify(msg, data.value[3]) return loads(data.value[2]) # MAC @@ -128,10 +125,7 @@ def decode(self, data: Union[bytes, CBORTag], key: COSEKey) -> Dict[int, Any]: raise ValueError("Invalid Signature1 format.") msg = dumps(["Signature1", data.value[0], b"", data.value[2]]) - try: - key.verify(msg, data.value[3]) - except VerifyError: - raise + key.verify(msg, data.value[3]) return loads(data.value[2]) # Signature diff --git a/cwt/key_types/okp.py b/cwt/key_types/okp.py index 5c765ce..17b088b 100644 --- a/cwt/key_types/okp.py +++ b/cwt/key_types/okp.py @@ -16,7 +16,7 @@ ) from ..cose_key import COSEKey -from ..exceptions import VerifyError +from ..exceptions import EncodeError, VerifyError class OKPKey(COSEKey): @@ -87,16 +87,15 @@ def sign(self, msg: bytes) -> bytes: if self._public_key: raise ValueError("Public key cannot be used for signing.") return self._private_key.sign(msg) - except cryptography.exceptions.Verify as err: - raise VerifyError("Failed to verify.") from err + except Exception as err: + raise EncodeError("Failed to sign.") from err - def verify(self, msg: bytes, sig: bytes) -> bool: + def verify(self, msg: bytes, sig: bytes): """""" try: if self._private_key: self._private_key.public_key().verify(sig, msg) else: self._public_key.verify(sig, msg) - return True - except cryptography.exceptions.InvalidSignature: - return False + except cryptography.exceptions.InvalidSignature as err: + raise VerifyError("Failed to verify.") from err From b0b2bc07c5de2f483bf1220df5c301f21b65c4d0 Mon Sep 17 00:00:00 2001 From: "Ajitomi, Daisuke" Date: Sun, 18 Apr 2021 21:42:32 +0900 Subject: [PATCH 6/9] Fix lint error. --- cwt/__init__.py | 2 +- cwt/cose.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/cwt/__init__.py b/cwt/__init__.py index ce46970..526b97b 100644 --- a/cwt/__init__.py +++ b/cwt/__init__.py @@ -1,6 +1,6 @@ -from .cose_key import COSEKey from .claims import Claims, claims from .cose import COSE +from .cose_key import COSEKey from .cwt import CWT, decode, encode_and_encrypt, encode_and_mac, encode_and_sign from .exceptions import CWTError, DecodeError, EncodeError, VerifyError from .key_builder import KeyBuilder, cose_key diff --git a/cwt/cose.py b/cwt/cose.py index ade21e4..00536bd 100644 --- a/cwt/cose.py +++ b/cwt/cose.py @@ -3,7 +3,6 @@ from cbor2 import CBORTag, dumps, loads from .cose_key import COSEKey -from .exceptions import VerifyError class COSE: From 40dd42a7b6fc40832dde0ecf94cd8764ebf7da69 Mon Sep 17 00:00:00 2001 From: "Ajitomi, Daisuke" Date: Sun, 18 Apr 2021 21:43:38 +0900 Subject: [PATCH 7/9] Fix name of exception. --- cwt/key_types/ec2.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cwt/key_types/ec2.py b/cwt/key_types/ec2.py index dd1e1ca..82c36aa 100644 --- a/cwt/key_types/ec2.py +++ b/cwt/key_types/ec2.py @@ -119,7 +119,7 @@ def verify(self, msg: bytes, sig: bytes): else: der_sig = self._os_to_der(self._public_key.curve.key_size, sig) self._public_key.verify(der_sig, msg, ec.ECDSA(self._hash_alg())) - except cryptography.exceptions.Verify as err: + except cryptography.exceptions.InvalidSignature as err: raise VerifyError("Failed to verify.") from err except ValueError as err: raise VerifyError("Invalid signature.") from err From d30585e051333889b4ab9c43b6d437569faf418d Mon Sep 17 00:00:00 2001 From: "Ajitomi, Daisuke" Date: Sun, 18 Apr 2021 21:44:25 +0900 Subject: [PATCH 8/9] Add tests for HMAC. --- tests/test_cwt.py | 73 +++++++++++++++++++++++++++++++++++++++ tests/test_key_builder.py | 48 +++++++++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 tests/test_cwt.py create mode 100644 tests/test_key_builder.py diff --git a/tests/test_cwt.py b/tests/test_cwt.py new file mode 100644 index 0000000..580c5d4 --- /dev/null +++ b/tests/test_cwt.py @@ -0,0 +1,73 @@ +# pylint: disable=R0201, R0904, W0621 +# R0201: Method could be a function +# R0904: Too many public methods +# W0621: Redefined outer name + +""" +Tests for CWT. +""" +# import cbor2 +import pytest + +# from secrets import token_bytes + +from cwt import CWT, claims, cose_key, VerifyError + +# from .utils import key_path + + +class TestCWT: + """ + Tests for CWT. + """ + + def test_cwt_constructor(self): + """""" + c = CWT() + assert isinstance(c, CWT) + + def test_cwt_encode_and_mac_with_default_alg(self): + """""" + c = CWT() + key = cose_key.from_symmetric_key("mysecretpassword") + token = c.encode_and_mac( + {1: "https://as.example", 2: "someone", 7: b"123"}, key + ) + decoded = c.decode(token, key) + assert 1 in decoded and decoded[1] == "https://as.example" + assert 2 in decoded and decoded[2] == "someone" + assert 2 in decoded and decoded[7] == b"123" + + @pytest.mark.parametrize( + "alg", + [ + "HMAC 256/64", + "HMAC 256/256", + "HMAC 384/384", + "HMAC 512/512", + ], + ) + def test_cwt_encode_and_mac_with_valid_alg(self, alg): + """""" + c = CWT() + key = cose_key.from_symmetric_key("mysecretpassword", alg=alg) + token = c.encode_and_mac( + {1: "https://as.example", 2: "someone", 7: b"123"}, key + ) + decoded = c.decode(token, key) + assert 1 in decoded and decoded[1] == "https://as.example" + assert 2 in decoded and decoded[2] == "someone" + assert 2 in decoded and decoded[7] == b"123" + + def test_cwt_decode_with_invalid_mac_key(self): + """""" + c = CWT() + key = cose_key.from_symmetric_key("mysecretpassword") + token = c.encode_and_mac( + {1: "https://as.example", 2: "someone", 7: b"123"}, key + ) + wrong_key = cose_key.from_symmetric_key("xxxxxxxxxx") + with pytest.raises(VerifyError) as err: + res = c.decode(token, wrong_key) + pytest.fail("decode should be fail: res=%s" % vars(res)) + assert "Failed to compare digest" in str(err.value) diff --git a/tests/test_key_builder.py b/tests/test_key_builder.py new file mode 100644 index 0000000..048529a --- /dev/null +++ b/tests/test_key_builder.py @@ -0,0 +1,48 @@ +# pylint: disable=R0201, R0904, W0621 +# R0201: Method could be a function +# R0904: Too many public methods +# W0621: Redefined outer name + +""" +Tests for KeyBuilder. +""" +import pytest + +# from secrets import token_bytes + +from cwt import COSEKey, KeyBuilder +# from .utils import key_path + + +class TestKeyBuilder: + """ + Tests for KeyBuilder. + """ + + def test_key_builder_constructor(self): + """""" + c = KeyBuilder() + assert isinstance(c, KeyBuilder) + + @pytest.mark.parametrize( + "alg", + [ + "HMAC 256/64", + "HMAC 256/256", + "HMAC 384/384", + "HMAC 512/512", + ], + ) + def test_cwt_encode_and_mac_with_valid_alg(self, alg): + """""" + kb = KeyBuilder() + k = kb.from_symmetric_key("mysecretpassword", alg=alg) + assert isinstance(k, COSEKey) + + def test_key_builder_from_symmetric_key_with_invalid_alg(self): + """""" + kb = KeyBuilder() + with pytest.raises(ValueError) as err: + res = kb.from_symmetric_key("mysecretpassword", alg="xxx") + pytest.fail("from_symmetric_key should be fail: res=%s" % vars(res)) + assert "Unsupported or unknown alg" in str(err.value) From f7cfc4a5b2f865732ad519cf0cb7e6657febe42e Mon Sep 17 00:00:00 2001 From: "Ajitomi, Daisuke" Date: Sun, 18 Apr 2021 21:50:56 +0900 Subject: [PATCH 9/9] Update version to v2.1.0. --- CHANGES.rst | 10 ++++++++++ cwt/__init__.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/CHANGES.rst b/CHANGES.rst index e902e45..14af807 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,16 @@ Changes Unreleased ---------- +Version 0.2.1 +------------- + +Released 2021-04-18 + +- Add VerifyError. `#11 `__ +- Fix HMAC alg names. `#11 `__ +- Make COSEKey public. `#11 `__ +- Add tests for HMAC. `#11 `__ + Version 0.2.0 ------------- diff --git a/cwt/__init__.py b/cwt/__init__.py index 526b97b..e818d8c 100644 --- a/cwt/__init__.py +++ b/cwt/__init__.py @@ -5,7 +5,7 @@ from .exceptions import CWTError, DecodeError, EncodeError, VerifyError from .key_builder import KeyBuilder, cose_key -__version__ = "0.1.1" +__version__ = "0.2.1" __title__ = "cwt" __description__ = "A Python implementation of CWT/COSE" __url__ = "https://python-cwt.readthedocs.io"