Skip to content

Commit

Permalink
Merge pull request #11 from dajiaji/add-test-for-mac
Browse files Browse the repository at this point in the history
Add test for MAC.
  • Loading branch information
dajiaji authored Apr 18, 2021
2 parents 875687c + f7cfc4a commit c33e2b5
Show file tree
Hide file tree
Showing 14 changed files with 169 additions and 41 deletions.
10 changes: 10 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ Changes
Unreleased
----------

Version 0.2.1
-------------

Released 2021-04-18

- Add VerifyError. `#11 <https://github.com/dajiaji/python-cwt/pull/11>`__
- Fix HMAC alg names. `#11 <https://github.com/dajiaji/python-cwt/pull/11>`__
- Make COSEKey public. `#11 <https://github.com/dajiaji/python-cwt/pull/11>`__
- Add tests for HMAC. `#11 <https://github.com/dajiaji/python-cwt/pull/11>`__

Version 0.2.0
-------------

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"}
Expand Down
10 changes: 6 additions & 4 deletions cwt/__init__.py
Original file line number Diff line number Diff line change
@@ -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"
Expand All @@ -21,12 +22,13 @@
"decode",
"CWT",
"COSE",
"KeyBuilder",
"Claims",
"COSEKey",
"KeyBuilder",
"cose_key",
"claims",
"CWTError",
"EncodeError",
"DecodeError",
"InvalidSignatureError",
"VerifyError",
]
8 changes: 4 additions & 4 deletions cwt/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 2 additions & 9 deletions cwt/cose.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
from cbor2 import CBORTag, dumps, loads

from .cose_key import COSEKey
from .exceptions import InvalidSignatureError


class COSE:
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions cwt/cwt.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
4 changes: 2 additions & 2 deletions cwt/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion cwt/key_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
8 changes: 4 additions & 4 deletions cwt/key_types/ec2.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
)

from ..cose_key import COSEKey
from ..exceptions import InvalidSignatureError
from ..exceptions import VerifyError
from ..utils import i2osp, os2ip


Expand Down Expand Up @@ -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):
""""""
Expand All @@ -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:
""""""
Expand Down
13 changes: 6 additions & 7 deletions cwt/key_types/okp.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
)

from ..cose_key import COSEKey
from ..exceptions import InvalidSignatureError
from ..exceptions import EncodeError, VerifyError


class OKPKey(COSEKey):
Expand Down Expand Up @@ -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
15 changes: 9 additions & 6 deletions cwt/key_types/symmetric.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from cryptography.hazmat.primitives.ciphers.aead import AESCCM

from ..cose_key import COSEKey
from ..exceptions import VerifyError


class SymmetricKey(COSEKey):
Expand Down Expand Up @@ -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:
Expand All @@ -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):
Expand Down
2 changes: 1 addition & 1 deletion docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
73 changes: 73 additions & 0 deletions tests/test_cwt.py
Original file line number Diff line number Diff line change
@@ -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)
48 changes: 48 additions & 0 deletions tests/test_key_builder.py
Original file line number Diff line number Diff line change
@@ -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)

0 comments on commit c33e2b5

Please sign in to comment.