Skip to content

Cryptographic structures

A COSE signature or MAC never covers the payload on its own. It covers a structure that also binds the protected header and the message type, so a tag computed for a COSE_Mac0 cannot be replayed on a COSE_Mac, and a signature made for one signer of a COSE_Sign cannot be lifted into a COSE_Sign1. An encryption likewise authenticates a structure rather than the bare protected header. These structures are what this library builds; casting one to string yields the CBOR bytes to hand to the algorithm.

RFC 9052 Class Context Fields after the context
§4.4 Sig_structure Cose\Signature\Signature1 "Signature1" body_protected, external_aad, payload
§4.4 Sig_structure Cose\Signature\Signature "Signature" body_protected, sign_protected, external_aad, payload
§6.3 MAC_structure Cose\Mac\Mac0Structure "MAC0" protected, external_aad, payload
§6.3 MAC_structure Cose\Mac\MacStructure "MAC" protected, external_aad, payload
§5.3 Enc_structure Cose\Encryption\Encrypt0Structure "Encrypt0" protected, external_aad
§5.3 Enc_structure Cose\Encryption\EncryptStructure "Encrypt" protected, external_aad
§5.3 Enc_structure RecipientStructure::forEncryptRecipient() "Enc_Recipient" protected, external_aad
§5.3 Enc_structure RecipientStructure::forMacRecipient() "Mac_Recipient" protected, external_aad
§5.3 Enc_structure RecipientStructure::forNestedRecipient() "Rec_Recipient" protected, external_aad
RFC 9338 §3.3 Countersign_structure Cose\Signature\Countersign::full() "CounterSignature" / "CounterSignatureV2" body_protected, sign_protected, external_aad, payload, ? other_fields
RFC 9338 §3.3 Countersign_structure Cose\Signature\Countersign::abbreviated() "CounterSignature0" / "CounterSignature0V2" body_protected, external_aad, payload, ? other_fields
use Cose\Mac\Mac0Structure;
use Cose\Signature\Signature1;

// Signing and verifying a COSE_Sign1
$toBeSigned = Signature1::create($protectedHeaderAsBytes, $payload);
$signature = $algorithm->sign((string) $toBeSigned, $privateKey);
$isValid = $algorithm->verify((string) $toBeSigned, $publicKey, $signature);

// Authenticating a COSE_Mac0: the same shape, a different context string
$toBeMaced = Mac0Structure::create($protectedHeaderAsBytes, $payload);
$macTag = $macAlgorithm->hash((string) $toBeMaced, $symmetricKey);

Warning

Cose\Algorithm\Mac\Mac::hash() and verify() authenticate exactly the bytes they are given. Passing getPayload()->getValue() straight to them produces a tag bound to no header, no algorithm and no message type, which no other implementation will accept.

The protected header is passed as bytes

The protected header is passed as the byte string the message carries, not as a map: the structure has to embed it verbatim, or the signature no longer verifies. HeaderMapHelper::encodeProtected() produces those bytes from a map, applying the RFC 9052 §3 rules on the way out: an empty map becomes h'' rather than h'a0', and the labels are checked (§1.5, §9).

There is one exception, and the structures apply it themselves. §3 lets a sender write an empty protected bucket either as the zero-length byte string h'' or as an empty map wrapped in a byte string, h'a0', and requires recipients to accept both; §§4.4, 5.3 and 6.3 then define the protected field of every structure with "If there are no protected attributes, a zero-length byte string is used". A message carrying h'a0' is therefore verified over h'', the bytes its sender computed, whichever form travels on the wire. CoseStructure::emptyOrSerializedMap() is that rule, and only h'a0' is affected: a non-empty bucket is never re-encoded.

Every structure takes the optional external_aad as its last argument, see External additional authenticated data. The Countersign_structure is computed over a finalized message rather than by it; see Countersignatures.