dajiaji/python-cwt

Sign/MAC/Encryption failure on empty-map protected header

kentakayama opened this issue · 0 comments

Section 3 of RFC 9052 says

Senders SHOULD encode a zero-length map as a zero-length byte string rather than as a zero-length map (encoded as h'a0'). The zero-length byte string encoding is preferred, because it is both shorter and the version used in the serialization structures for cryptographic computation. Recipients MUST accept both a zero-length byte string and a zero-length map encoded in a byte string.

Though zero-length byte string protected header is "good manner" and python-cwt encode so, but it also says that recipients MUST accept both.

code

#!/usr/bin/env python3

from cwt import COSE, COSEKey
from cbor2 import loads, dumps

# The sender side:
priv_key = COSEKey.from_jwk(
    {
        "kty": "EC",
        "kid": "01",
        "crv": "P-256",
        "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
        "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4",
        "d": "V8kgd2ZBRuh2dgyVINBUqpPDr7BOMGcF22CQMIUHtNM",
    }
)
sender = COSE.new(kid_auto_inclusion=True)
encoded = sender.encode(payload=b"Hello world!", key=priv_key, protected={}, unprotected={"alg": "ES256"})

print("1. Generate COSE_Sign1(ES256) binary")
print(encoded.hex().upper())
t = loads(encoded)
print(f"{t}\n")

print("2. Replace h'' (0x40) protected header to << {} >> (0x41A0)")
t.value[0] = bytes.fromhex("A0") # << {} >>
encoded = dumps(t)
print(encoded.hex().upper())
print(loads(encoded))

# The recipient side:
pub_key = COSEKey.from_jwk(
    {
        "kty": "EC",
        "kid": "01",
        "crv": "P-256",
        "x": "usWxHK2PmfnHKwXPS54m0kTcGJ90UiglWiGahtagnv8",
        "y": "IBOL-C3BttVivg-lSreASjpkttcsz-1rb7btKLv8EX4",
    }
)
recipient = COSE.new()
assert b"Hello world!" == recipient.decode(encoded, pub_key)

result

$ ./cose_sign1_zero_length_map.py 
1. Generate COSE_Sign1(ES256) binary
D28440A20126044230314C48656C6C6F20776F726C642158403B3B72EDDC332C91523D4389A51500139CB9605A3A29DF704C7E4984095DEBDD24DF1A0FD52308D0562E4946C7CD3254C203735CD568B92C6F83D569DA974942
CBORTag(18, [b'', {1: -7, 4: b'01'}, b'Hello world!', b';;r\xed\xdc3,\x91R=C\x89\xa5\x15\x00\x13\x9c\xb9`Z:)\xdfpL~I\x84\t]\xeb\xdd$\xdf\x1a\x0f\xd5#\x08\xd0V.IF\xc7\xcd2T\xc2\x03s\\\xd5h\xb9,o\x83\xd5i\xda\x97IB'])

2. Replace h'' (0x40) protected header to << {} >> (0x41A0)
D28441A0A20126044230314C48656C6C6F20776F726C642158403B3B72EDDC332C91523D4389A51500139CB9605A3A29DF704C7E4984095DEBDD24DF1A0FD52308D0562E4946C7CD3254C203735CD568B92C6F83D569DA974942
CBORTag(18, [b'\xa0', {1: -7, 4: b'01'}, b'Hello world!', b';;r\xed\xdc3,\x91R=C\x89\xa5\x15\x00\x13\x9c\xb9`Z:)\xdfpL~I\x84\t]\xeb\xdd$\xdf\x1a\x0f\xd5#\x08\xd0V.IF\xc7\xcd2T\xc2\x03s\\\xd5h\xb9,o\x83\xd5i\xda\x97IB'])
Traceback (most recent call last):
  File "cwt/algs/ec2.py", line 266, in verify
    self._public_key.verify(der_sig, msg, ec.ECDSA(self._hash_alg()))
  File "cryptography/hazmat/backends/openssl/ec.py", line 328, in verify
    _ecdsa_sig_verify(self._backend, self, signature, data)
  File "cryptography/hazmat/backends/openssl/ec.py", line 124, in _ecdsa_sig_verify
    raise InvalidSignature
cryptography.exceptions.InvalidSignature

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "./cose_sign1_zero_length_map.py", line 42, in <module>
    assert b"Hello world!" == recipient.decode(encoded, pub_key)
  File "cwt/cose.py", line 315, in decode
    _, _, res = self.decode_with_headers(data, keys, context, external_aad, detached_payload)
  File "cwt/cose.py", line 480, in decode_with_headers
    raise err
  File "cwt/cose.py", line 476, in decode_with_headers
    k.verify(to_be_signed, data.value[3])
  File "cwt/algs/ec2.py", line 268, in verify
    raise VerifyError("Failed to verify.") from err
cwt.exceptions.VerifyError: Failed to verify.