Message Types - aidangarske/wolfCOSE GitHub Wiki

COSE Message Types

wolfCOSE implements the complete RFC 9052 message set — all six COSE structures, including the multi-actor variants (COSE_Sign, COSE_Encrypt, COSE_Mac) that most embedded C COSE libraries omit.

Message RFC 9052 Tag API
COSE_Sign1 Sec. 4.2 18 wc_CoseSign1_Sign / wc_CoseSign1_Verify
COSE_Sign Sec. 4.1 98 wc_CoseSign_Sign / wc_CoseSign_Verify
COSE_Encrypt0 Sec. 5.2 16 wc_CoseEncrypt0_Encrypt / wc_CoseEncrypt0_Decrypt
COSE_Encrypt Sec. 5.1 96 wc_CoseEncrypt_Encrypt / wc_CoseEncrypt_Decrypt
COSE_Mac0 Sec. 6.2 17 wc_CoseMac0_Create / wc_CoseMac0_Verify
COSE_Mac Sec. 6.1 97 wc_CoseMac_Create / wc_CoseMac_Verify
COSE_Key / COSE_KeySet Sec. 7 (none) wc_CoseKey_Encode / wc_CoseKey_Decode

Every message can be built attached or detached, with optional external AAD, and every algorithm in Algorithms is available to every message type that accepts it.

COSE_Sign1 — single-signer signature (RFC 9052 Sec. 4.2)

COSE_Sign1 is the most common COSE message: one signer, one signature.

WOLFCOSE_KEY key;
uint8_t out[1024];
size_t outLen;
WC_RNG rng;

wc_InitRng(&rng);
wc_CoseKey_Init(&key);
wc_CoseKey_SetEcc(&key, WOLFCOSE_CRV_P256, &eccPriv);

int ret = wc_CoseSign1_Sign(&key, WOLFCOSE_ALG_ES256,
                            payload, payloadLen,
                            NULL, 0,            /* no detached payload */
                            NULL, 0,            /* no external AAD */
                            scratch, sizeof(scratch),
                            out, sizeof(out), &outLen,
                            &rng);

COSE_Sign — multi-signer signature (RFC 9052 Sec. 4.1)

COSE_Sign carries N independent signatures over the same payload. Use it for dual-control approval, certificate chains where multiple parties attest, or hybrid classical/PQC signatures during migration. Each signer has its own algorithm and kid.

WOLFCOSE_SIGNATURE signers[2] = {
    { .algId = WOLFCOSE_ALG_ES256,
      .key   = &vendorKey,
      .kid   = (const uint8_t*)"vendor-2026", .kidLen = 11 },
    { .algId = WOLFCOSE_ALG_ML_DSA_65,        /* hybrid: classical + PQC */
      .key   = &oemKey,
      .kid   = (const uint8_t*)"oem-pqc-1",   .kidLen = 9 },
};

ret = wc_CoseSign_Sign(signers, 2,
                       firmware, firmwareLen,
                       NULL, 0,                   /* attached payload */
                       NULL, 0,                   /* no external AAD */
                       scratch, sizeof(scratch),
                       out, sizeof(out), &outLen,
                       &rng);

Verifiers select which signature to check by index:

ret = wc_CoseSign_Verify(&vendorPubKey, /*signerIndex=*/0,
                         in, inLen,
                         NULL, 0, NULL, 0,
                         scratch, sizeof(scratch),
                         &hdr, &payload, &payloadLen);

COSE_Encrypt0 — single-recipient AEAD (RFC 9052 Sec. 5.2)

Direct symmetric AEAD. The caller already has the content encryption key (CEK).

ret = wc_CoseEncrypt0_Encrypt(&aesKey, WOLFCOSE_ALG_A128GCM,
                              iv, sizeof(iv),
                              plaintext, plaintextLen,
                              NULL, 0, NULL, 0,
                              scratch, sizeof(scratch),
                              out, sizeof(out), &outLen,
                              &rng);

COSE_Encrypt — multi-recipient AEAD (RFC 9052 Sec. 5.1)

COSE_Encrypt produces one ciphertext addressable by any of N recipients. Each recipient entry describes how that recipient learns the CEK: pre-shared (Direct), wrapped under a recipient KEK (A128KW / A192KW / A256KW), or derived via ECDH.

WOLFCOSE_RECIPIENT recipients[3] = {
    { .algId = WOLFCOSE_ALG_DIRECT,
      .key   = &fleetSharedKey,
      .kid   = (const uint8_t*)"fleet-2026", .kidLen = 10 },
    { .algId = WOLFCOSE_ALG_A256KW,
      .key   = &device42Kek,
      .kid   = (const uint8_t*)"dev-42",     .kidLen = 6 },
    { .algId = WOLFCOSE_ALG_ECDH_ES_HKDF_256,
      .key   = &device43Pub,
      .kid   = (const uint8_t*)"dev-43",     .kidLen = 6 },
};

ret = wc_CoseEncrypt_Encrypt(recipients, 3,
                             WOLFCOSE_ALG_A128GCM,         /* content alg */
                             iv, sizeof(iv),
                             config, configLen,
                             NULL, 0, NULL, 0,
                             scratch, sizeof(scratch),
                             out, sizeof(out), &outLen,
                             &rng);

A receiving device decrypts by selecting its own recipient index:

ret = wc_CoseEncrypt_Decrypt(&myRecipient, /*recipientIndex=*/1,
                             in, inLen,
                             NULL, 0, NULL, 0,
                             scratch, sizeof(scratch),
                             &hdr, plaintext, sizeof(plaintext), &plaintextLen);

COSE_Mac0 — single-recipient MAC (RFC 9052 Sec. 6.2)

Direct MAC over a payload using a pre-shared key.

ret = wc_CoseMac0_Create(&hmacKey, WOLFCOSE_ALG_HMAC_256_256,
                         payload, payloadLen,
                         NULL, 0, NULL, 0,
                         scratch, sizeof(scratch),
                         out, sizeof(out), &outLen);

COSE_Mac — multi-recipient MAC (RFC 9052 Sec. 6.1)

COSE_Mac carries one MAC tag plus per-recipient envelopes describing how each recipient learns the MAC key. Useful for group broadcast scenarios where every subscriber needs to authenticate the same payload but holds a different KEK.

WOLFCOSE_RECIPIENT subscribers[N] = { /* one entry per group member */ };

ret = wc_CoseMac_Create(subscribers, N,
                        WOLFCOSE_ALG_HMAC_256_256,
                        payload, payloadLen,
                        NULL, 0, NULL, 0,
                        scratch, sizeof(scratch),
                        out, sizeof(out), &outLen);

Detached payloads

Every Sign*, Encrypt*, and Mac* API accepts a detached payload — the COSE message stores only the signature/MAC/ciphertext and a nil payload field, while the actual bytes live elsewhere. This is essential when signing large objects (firmware, OTA images) where you don't want to copy the payload through the COSE buffer. Pass the payload via the detachedPayload / detachedLen arguments instead of payload / payloadLen, and the same on verify.

Compile-time stripping

You only pay for the message types you use. Define the appropriate WOLFCOSE_NO_* macros to strip unused code paths:

-DWOLFCOSE_NO_SIGN     -DWOLFCOSE_NO_ENCRYPT  -DWOLFCOSE_NO_MAC   /* multi-actor only */
-DWOLFCOSE_NO_ENCRYPT0 -DWOLFCOSE_NO_MAC0                          /* sign-only build */
-DWOLFCOSE_NO_SIGN1_SIGN -DWOLFCOSE_NO_CBOR_ENCODE                 /* verify-only build */

A minimal Sign1-verify-only build is around 7.5 KB of .text, including the built-in CBOR engine.

See also

  • Algorithms — full algorithm list with COSE IDs and wolfCrypt guards
  • API Reference — function signatures, structures, and error codes
  • Macros — every WOLFCOSE_* and WOLFCOSE_NO_* compile-time toggle
  • examples/scenarios/multi_party_approval.c, iot_fleet_config.c, group_broadcast_mac.c show real multi-actor flows