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/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/__init__.py b/cwt/__init__.py index f197d2a..e818d8c 100644 --- a/cwt/__init__.py +++ b/cwt/__init__.py @@ -1,10 +1,11 @@ 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, InvalidSignatureError +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" @@ -21,12 +22,13 @@ "decode", "CWT", "COSE", - "KeyBuilder", "Claims", + "COSEKey", + "KeyBuilder", "cose_key", "claims", "CWTError", "EncodeError", "DecodeError", - "InvalidSignatureError", + "VerifyError", ] 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/cose.py b/cwt/cose.py index 1ada677..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 InvalidSignatureError class COSE: @@ -112,10 +111,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 InvalidSignatureError: - raise + key.verify(msg, data.value[3]) return loads(data.value[2]) # MAC @@ -128,10 +124,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 InvalidSignatureError: - raise + key.verify(msg, data.value[3]) return loads(data.value[2]) # Signature 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: 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_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/ec2.py b/cwt/key_types/ec2.py index cfdf29a..82c36aa 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): """""" @@ -120,9 +120,9 @@ def verify(self, msg: bytes, sig: bytes): 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 + 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..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 InvalidSignatureError +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.InvalidSignature as err: - raise InvalidSignatureError("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 diff --git a/cwt/key_types/symmetric.py b/cwt/key_types/symmetric.py index be1bbaa..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): @@ -47,16 +48,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: @@ -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): 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, 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)