Skip to content

Encoding of header parameters varies depending on order of keys in the dict #634

@achamayou

Description

@achamayou

I stumbled (quite badly!) across this while passing application-specific header parameters. I am trying to pass Python str values, and instead of being encoded as tstr, they come out of as bstr. A look at to_cose_header() explains why, they are being explicitly encoded that way. I want to propose a patch for this, so I started with trying a small repro:

        protected = {COSEHeaders.KID: b"01", COSEHeaders.ALG: COSEAlgs.ES256, "content type": "application/cwt"}

        ctx = COSE.new(alg_auto_inclusion=True)
        encoded4 = ctx.encode_and_sign(b"Hello world!", cose_key, protected=protected)
        phdr, _, payload = recipient.decode_with_headers(encoded4, pub_key)
        assert payload == b"Hello world!"

        assert phdr[COSEHeaders.ALG] == COSEAlgs.ES256
        assert phdr["content type"] == "application/cwt"

But that works. How?

This does not work:

        # Different order than 781, but surely equivalent?
        protected = {"content type": "application/cwt", COSEHeaders.KID: b"01", COSEHeaders.ALG: COSEAlgs.ES256}

        with pytest.raises(ValueError) as err:
            # Raises ValueError: Unsupported or unknown COSE header parameter: 4.
            # Very surprising!
            encoded5 = ctx.encode_and_sign(b"Hello world!", cose_key, protected=protected)

            phdr, _, payload = recipient.decode_with_headers(encoded5, pub_key)
            assert payload == b"Hello world!"

            assert phdr[COSEHeaders.ALG] == COSEAlgs.ES256
            assert phdr["content type"] == "application/cwt"

It turns out that to_cose_header() does this:

if len(data) == 0 or not isinstance(list(data.keys())[0], str):

If the first key in the keys of the header dict is not a string, the code assumes the dict has already been encoded, and lets it through unmodified. Huh. On the one hand, this is great, now I have a way to get my tstr through. On the other hand... yikes!

Repro available on: https://github.com/achamayou/python-cwt/tree/encoding_repro

pytest -k test_cose_usage_examples_cose_signature

I can think of a few solutions, most of which involve letting the user handle header pre-processing. Another one may be to add a specific type for the built-in header parameters, instead of using string mappings. That way the logic could be: int and str keys are let through unprocessed all the way to cbor2, but only keys of COSEHeaderKey type are mapped to int. Collisions probably throw an exception?

I will think about this over the weekend, but I wanted to write this up while it is fresh. I should also say that the library is lovely, and a pleasure to use so far!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions