COSE Module
COSE Module
src/cose is the small embedded implementation of COSE_Sign1 used for PacketRF management traffic. It is intentionally narrow. The goal is not to implement all of RFC 9052; the goal is to implement one well-defined interoperable signed-message profile that fits the firmware and the host tooling.
Scope
The module builds the Sig_structure bytes that go into a COSE_Sign1, encodes the PacketRF COSE_Sign1 envelope, decodes it on receipt, calls into src/crypto to verify request signatures, and calls into src/crypto to sign response payloads. That is all.
It does not store keys, hold trust state, implement Ed25519 internals, route CoAP, or apply endpoint authorization rules. Those responsibilities live in src/crypto (keys, identity, Ed25519 primitives), src/coap (routing and request/response transport), and src/control (management payload schema and business logic).
Why COSE
PacketRF management payloads are CBOR. Once some endpoints needed signatures, there were two reasonable options: invent a private wrapper around CBOR, or use a standard signed CBOR container. PacketRF picked the second. COSE_Sign1 is a standard way to say "this payload is the application data, these header fields describe
how it was signed, this is the signature over a well-defined
structure". Inventing a local convention such as "put the signature
in one special CBOR key at the end" works until the moment it does not, and security boundaries are exactly the place to avoid that.
What is signed
PacketRF does not sign the full CoAP datagram as it appears on the wire — that would be a poor fit, because CoAP carries transport-level fields like message ID and token handling that are not part of the management payload. PacketRF signs the application payload through COSE_Sign1.
For COSE_Sign1, the signed bytes are a CBOR structure called Sig_structure. PacketRF builds it from:
- the context string
Signature1, - the protected header bytes,
- empty external associated data,
- the inner payload bytes.
So the management payload is authenticated, the protected headers are authenticated, and the transport envelope is not part of the signature. That is the right boundary for this firmware.
The PacketRF profile
The implemented profile is deliberately narrow. Keeping the embedded parser simple and making host interoperability easy to reason about both want the same thing: a small, fixed shape.
- Message type:
COSE_Sign1. - Algorithm:
EdDSAwith Ed25519 (alg = -8). - Outer CoAP content format:
18(application/cose; cose-type="cose-sign1"). - Inner payload content type: a CoAP integer Content-Format in the protected headers. Currently
60(application/cbor). kid: required, encoded as bytes in the protected headers.- Unprotected header bucket: empty.
external_aad: empty.- Payload form: embedded payload, not detached payload.
This is one profile, not a claim of full COSE feature coverage.
Protected headers
The current implementation carries three protected headers: alg, content type, and kid. They are protected because they describe how the signature is interpreted (alg), what the inner payload means (content type), and which signing identity to verify against (kid). The unprotected header map is kept empty so that there is no ambiguity between "this header is part of the signed
authenticated message" and "this header is a hint set by the
transport".
What <tt>kid</tt> means
In PacketRF, kid is a compact identifier of the signing public key, not the public key itself. The host tool can read kid from a response, look it up in its local trust database, and verify the signature only if it already has the matching trusted public key. This is the same idea as SSH known-host behavior: first you learn a device key, then you decide whether to trust it, and after that signed responses can be verified against the local trust record.
Public API
The interesting entry points are in cose.hpp and cose.cpp:
cose_sign1_tbs_into(...)cose_sign1_encode_into(...)cose_sign1_decode_view(...)cose_sign1_verify_view(...)cose_sign1_sign_into(...)
The API is small on purpose. It serves the actual needs of the firmware and the host tests; it is not a generic COSE framework.
How a request and a response flow
For a request:
- the host tool builds a CBOR application payload,
- wraps it as
COSE_Sign1, - the request arrives at the firmware via
src/coap, - CoAP forwards the payload bytes into
src/control, src/control::ExchangeServicecallssrc/coseto decode and verify the envelope,- the verified inner payload continues to command dispatch.
For a response:
- an endpoint callback returns its application payload,
src/control::ExchangeServicedecides whether response signing is enabled,- if so, it calls
src/coseto wrap the payload, src/coseusesprf::to sign with the device key,crypto:: IKeyService - the transport sends the bytes back with the COSE content format.
Strict decoding
Signed management traffic is a security boundary, so the decoder enforces the PacketRF profile strictly. It rejects duplicate protected header labels, rejects protected labels outside the PacketRF allowlist, refuses to handle crit (it is intentionally out of scope for this profile), refuses non-empty unprotected header buckets, refuses a nil payload, and requires the expected authenticated alg, kid and current inner payload content type.
A permissive "best-effort" COSE subset would be a smaller change and a worse design. PacketRF prefers to fail clearly when an incoming envelope does not match the profile.
References
- RFC 9052: CBOR Object Signing and Encryption (COSE): Structures and Process, https:/
/ www.rfc-editor.org/ rfc/ rfc9052 - RFC 9053: CBOR Object Signing and Encryption (COSE): Initial Algorithms, https:/
/ www.rfc-editor.org/ rfc/ rfc9053