From 59074a2c0c61946f3f1744f9ac25a0a571036290 Mon Sep 17 00:00:00 2001 From: Aidan Garske Date: Tue, 28 Jul 2026 14:58:49 -0700 Subject: [PATCH] Add wc_CoseSign1_Sign_ex with an untagged output option --- docs/API-Reference.md | 35 ++++++++++++++ include/wolfcose/wolfcose.h | 22 +++++++++ src/wolfcose.c | 92 +++++++++++++++++++++++++------------ tests/test_cose.c | 92 +++++++++++++++++++++++++++++++++++++ 4 files changed, 212 insertions(+), 29 deletions(-) diff --git a/docs/API-Reference.md b/docs/API-Reference.md index a88d8c8..cb37f71 100644 --- a/docs/API-Reference.md +++ b/docs/API-Reference.md @@ -374,6 +374,41 @@ Create a COSE_Sign1 message (single signer). --- +### wc_CoseSign1_Sign_ex + +```c +int wc_CoseSign1_Sign_ex( + WOLFCOSE_KEY* key, + int32_t alg, + const uint8_t* kid, size_t kidLen, + const uint8_t* payload, size_t payloadLen, + const uint8_t* detachedPayload, size_t detachedPayloadLen, + const uint8_t* extAad, size_t extAadLen, + uint8_t* scratch, size_t scratchSz, + uint8_t* out, size_t outSz, size_t* outLen, + WC_RNG* rng, uint32_t flags +); +``` + +As `wc_CoseSign1_Sign()`, plus output options. `wc_CoseSign1_Sign()` is this +function called with `flags = 0`. + +**Flags:** +| Flag | Effect | +|------|--------| +| `WOLFCOSE_SIGN1_UNTAGGED` | Omit the CBOR tag 18 prefix, so output starts with the 4-element array (`0x84`). Verification accepts either form. | + +The tag is not covered by the signature. `Sig_structure` is +`["Signature1", protected, external_aad, payload]` (RFC 9052 Section 4.4), so the +tagged and untagged forms of the same message carry an identical signature and +either can be converted to the other without detection. That is a property of +COSE, not of this flag, but a caller emitting untagged output should establish +the message type out of band. + +**Returns:** `WOLFCOSE_SUCCESS` or error code + +--- + ### wc_CoseSign1_Verify ```c diff --git a/include/wolfcose/wolfcose.h b/include/wolfcose/wolfcose.h index 06f09e6..d63b51d 100644 --- a/include/wolfcose/wolfcose.h +++ b/include/wolfcose/wolfcose.h @@ -122,6 +122,9 @@ extern "C" { /* ----- COSE constants (RFC 9052) ----- */ +/* Output options for wc_CoseSign1_Sign_ex() */ +#define WOLFCOSE_SIGN1_UNTAGGED 0x0001u /* Omit the tag 18 prefix */ + /* Tags (RFC 9052) */ #define WOLFCOSE_TAG_SIGN1 18u #define WOLFCOSE_TAG_ENCRYPT0 16u @@ -707,6 +710,25 @@ WOLFCOSE_API int wc_CoseSign1_Sign(WOLFCOSE_KEY* key, int32_t alg, uint8_t* scratch, size_t scratchSz, uint8_t* out, size_t outSz, size_t* outLen, WC_RNG* rng); + +/** + * \brief Sign a payload producing a COSE_Sign1 message, with output options. + * + * Identical to wc_CoseSign1_Sign() with the addition of \p flags, which is a + * bitmask of WOLFCOSE_SIGN1_* values. Passing 0 is equivalent to calling + * wc_CoseSign1_Sign(). + * + * \param flags Bitmask of WOLFCOSE_SIGN1_* output options. + * \return WOLFCOSE_SUCCESS or negative error code. + */ +WOLFCOSE_API int wc_CoseSign1_Sign_ex(WOLFCOSE_KEY* key, int32_t alg, + const uint8_t* kid, size_t kidLen, + const uint8_t* payload, size_t payloadLen, + const uint8_t* detachedPayload, size_t detachedLen, + const uint8_t* extAad, size_t extAadLen, + uint8_t* scratch, size_t scratchSz, + uint8_t* out, size_t outSz, size_t* outLen, + WC_RNG* rng, uint32_t flags); #endif /* WOLFCOSE_SIGN1_SIGN */ #if defined(WOLFCOSE_SIGN1_VERIFY) diff --git a/src/wolfcose.c b/src/wolfcose.c index fcf0b58..544e316 100644 --- a/src/wolfcose.c +++ b/src/wolfcose.c @@ -210,6 +210,32 @@ int wolfCose_AlgToHashType(int32_t alg, enum wc_HashType* hashType) return ret; } +/* Guarded to match its only call sites, which are all in Sign1, so a build + * without them does not carry an unused function. */ +#if (defined(WOLFCOSE_SIGN1_SIGN) || defined(WOLFCOSE_SIGN1_VERIFY)) && \ + defined(WOLFCOSE_HAVE_ECDSA) +/* Each ECDSA alg is bound to one curve. Shared so sign and verify cannot + * disagree about which pairings are legal. */ +static int wolfCose_EccAlgCrv(int32_t alg, int32_t* crv) +{ + int ret = WOLFCOSE_SUCCESS; + + if (alg == WOLFCOSE_ALG_ES256) { + *crv = WOLFCOSE_CRV_P256; + } + else if (alg == WOLFCOSE_ALG_ES384) { + *crv = WOLFCOSE_CRV_P384; + } + else if (alg == WOLFCOSE_ALG_ES512) { + *crv = WOLFCOSE_CRV_P521; + } + else { + ret = WOLFCOSE_E_COSE_BAD_ALG; + } + return ret; +} +#endif + WOLFCOSE_LOCAL int wolfCose_SigSize(int32_t alg, size_t* sigSz) { int ret = WOLFCOSE_SUCCESS; @@ -3576,6 +3602,21 @@ int wc_CoseSign1_Sign(WOLFCOSE_KEY* key, int32_t alg, uint8_t* scratch, size_t scratchSz, uint8_t* out, size_t outSz, size_t* outLen, WC_RNG* rng) +{ + return wc_CoseSign1_Sign_ex(key, alg, kid, kidLen, payload, payloadLen, + detachedPayload, detachedLen, extAad, + extAadLen, scratch, scratchSz, out, outSz, + outLen, rng, 0); +} + +int wc_CoseSign1_Sign_ex(WOLFCOSE_KEY* key, int32_t alg, + const uint8_t* kid, size_t kidLen, + const uint8_t* payload, size_t payloadLen, + const uint8_t* detachedPayload, size_t detachedLen, + const uint8_t* extAad, size_t extAadLen, + uint8_t* scratch, size_t scratchSz, + uint8_t* out, size_t outSz, size_t* outLen, + WC_RNG* rng, uint32_t flags) { int ret = WOLFCOSE_SUCCESS; uint8_t protectedBuf[WOLFCOSE_PROTECTED_HDR_MAX]; @@ -3603,10 +3644,18 @@ int wc_CoseSign1_Sign(WOLFCOSE_KEY* key, int32_t alg, isDetached = 0u; } - if ((key == NULL) || (sigPayload == NULL) || (scratch == NULL) || - (out == NULL) || (outLen == NULL) || (rng == NULL)) { + if ((key == NULL) || (sigPayload == NULL) || (out == NULL) || + (scratch == NULL) || (outLen == NULL) || (rng == NULL)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + /* Reject bits this build does not define rather than ignoring them, so a + * caller compiled against a newer header fails loudly. */ + else if ((flags & ~(uint32_t)WOLFCOSE_SIGN1_UNTAGGED) != 0u) { ret = WOLFCOSE_E_INVALID_ARG; } + else { + /* No action required */ + } #ifdef WOLFCOSE_CHECK_WORD32_LEN if ((ret == WOLFCOSE_SUCCESS) && ((wolfCose_LenFitsWord32(payloadLen) == 0) || @@ -3720,19 +3769,10 @@ int wc_CoseSign1_Sign(WOLFCOSE_KEY* key, int32_t alg, ret = WOLFCOSE_E_COSE_KEY_TYPE; } - /* Each ECDSA alg is bound to one curve. */ if (ret == WOLFCOSE_SUCCESS) { - int32_t expectedCrv; - if (alg == WOLFCOSE_ALG_ES256) { - expectedCrv = WOLFCOSE_CRV_P256; - } - else if (alg == WOLFCOSE_ALG_ES384) { - expectedCrv = WOLFCOSE_CRV_P384; - } - else { - expectedCrv = WOLFCOSE_CRV_P521; - } - if (key->crv != expectedCrv) { + int32_t expectedCrv = 0; + ret = wolfCose_EccAlgCrv(alg, &expectedCrv); + if ((ret == WOLFCOSE_SUCCESS) && (key->crv != expectedCrv)) { ret = WOLFCOSE_E_COSE_BAD_ALG; } } @@ -3879,11 +3919,13 @@ int wc_CoseSign1_Sign(WOLFCOSE_KEY* key, int32_t alg, * Tag(18) [protected_bstr, unprotected_map, payload_bstr, signature_bstr] */ outCtx.buf = out; + outCtx.cbuf = NULL; outCtx.bufSz = outSz; outCtx.idx = 0; /* Encode COSE_Sign1 output */ - if (ret == WOLFCOSE_SUCCESS) { + if ((ret == WOLFCOSE_SUCCESS) && + ((flags & WOLFCOSE_SIGN1_UNTAGGED) == 0u)) { ret = wc_CBOR_EncodeTag(&outCtx, WOLFCOSE_TAG_SIGN1); } @@ -3921,8 +3963,9 @@ int wc_CoseSign1_Sign(WOLFCOSE_KEY* key, int32_t alg, ret = wc_CBOR_EncodeBstr(&outCtx, sigPtr, sigSz); } - if ((ret == WOLFCOSE_SUCCESS) && (outLen != NULL)) { - *outLen = outCtx.idx; + if (outLen != NULL) { + /* Report 0 rather than leaving a stale length behind on failure. */ + *outLen = (ret == WOLFCOSE_SUCCESS) ? outCtx.idx : 0u; } /* Cleanup: always executed */ @@ -4141,19 +4184,10 @@ int wc_CoseSign1_Verify(const WOLFCOSE_KEY* key, if (key->kty != WOLFCOSE_KTY_EC2) { ret = WOLFCOSE_E_COSE_KEY_TYPE; } - /* Each ECDSA alg is bound to one curve. */ if (ret == WOLFCOSE_SUCCESS) { - int32_t expectedCrv; - if (alg == WOLFCOSE_ALG_ES256) { - expectedCrv = WOLFCOSE_CRV_P256; - } - else if (alg == WOLFCOSE_ALG_ES384) { - expectedCrv = WOLFCOSE_CRV_P384; - } - else { - expectedCrv = WOLFCOSE_CRV_P521; - } - if (key->crv != expectedCrv) { + int32_t expectedCrv = 0; + ret = wolfCose_EccAlgCrv(alg, &expectedCrv); + if ((ret == WOLFCOSE_SUCCESS) && (key->crv != expectedCrv)) { ret = WOLFCOSE_E_COSE_BAD_ALG; } } diff --git a/tests/test_cose.c b/tests/test_cose.c index dda6190..8eb42ba 100644 --- a/tests/test_cose.c +++ b/tests/test_cose.c @@ -17394,12 +17394,104 @@ static void test_sign_verify_bad_array_count(void) #endif /* ----- Entry point ----- */ +#if defined(WOLFCOSE_HAVE_ES256) && defined(WOLFCOSE_SIGN1_SIGN) +static void test_cose_sign1_untagged(void) +{ + static const uint8_t payload[] = "untagged payload"; + WC_RNG rng; + ecc_key eccKey; + WOLFCOSE_KEY signKey; + uint8_t scratch[512]; + uint8_t tagged[512]; + uint8_t untagged[512]; + size_t taggedLen = 0; + size_t untaggedLen = 0; + const uint8_t* decPayload = NULL; + size_t decPayloadLen = 0; + WOLFCOSE_HDR hdr; + int ret; + + TEST_LOG(" [Sign1 untagged output]\n"); + + ret = wc_InitRng(&rng); + TEST_ASSERT(ret == 0, "untagged rng init"); + if (ret != 0) { + return; + } + ret = wc_ecc_init(&eccKey); + TEST_ASSERT(ret == 0, "untagged ecc init"); + if (ret != 0) { + /* eccKey was never initialized, so it must not be freed below. */ + wc_FreeRng(&rng); + return; + } + ret = wc_ecc_make_key(&rng, 32, &eccKey); + TEST_ASSERT(ret == 0, "untagged ecc keygen"); + if (ret != 0) { + wc_ecc_free(&eccKey); + wc_FreeRng(&rng); + return; + } + ret = wc_CoseKey_Init(&signKey); + TEST_ASSERT(ret == 0, "untagged key init"); + if (ret != 0) { + wc_ecc_free(&eccKey); + wc_FreeRng(&rng); + return; + } + if (ret == 0) { + ret = wc_CoseKey_SetEcc(&signKey, WOLFCOSE_CRV_P256, &eccKey); + TEST_ASSERT(ret == 0, "untagged key set ecc"); + } + + if (ret == 0) { + ret = wc_CoseSign1_Sign_ex(&signKey, WOLFCOSE_ALG_ES256, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), + tagged, sizeof(tagged), &taggedLen, &rng, 0); + TEST_ASSERT(ret == 0, "flags=0 signs"); + TEST_ASSERT(taggedLen > 0u && tagged[0] == 0xD2u, + "flags=0 emits tag 18 (0xD2)"); + } + + if (ret == 0) { + ret = wc_CoseSign1_Sign_ex(&signKey, WOLFCOSE_ALG_ES256, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), + untagged, sizeof(untagged), &untaggedLen, &rng, + WOLFCOSE_SIGN1_UNTAGGED); + TEST_ASSERT(ret == 0, "untagged signs"); + TEST_ASSERT(untaggedLen > 0u && untagged[0] == 0x84u, + "untagged starts with 4-array (0x84)"); + TEST_ASSERT(untaggedLen == (taggedLen - 1u), + "untagged is exactly one byte shorter"); + } + + /* The verify path treats the tag as optional, so both forms round-trip. */ + if (ret == 0) { + ret = wc_CoseSign1_Verify(&signKey, untagged, untaggedLen, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + &hdr, &decPayload, &decPayloadLen); + TEST_ASSERT(ret == 0, "untagged verifies"); + TEST_ASSERT(decPayloadLen == (sizeof(payload) - 1), + "untagged payload length round-trips"); + } + + wc_CoseKey_Free(&signKey); + wc_ecc_free(&eccKey); + wc_FreeRng(&rng); +} +#endif /* WOLFCOSE_HAVE_ES256 */ + int test_cose(void) { g_failures = 0; /* Internal helper tests */ test_wolfcose_force_zero(); +#if defined(WOLFCOSE_HAVE_ES256) && defined(WOLFCOSE_SIGN1_SIGN) + test_cose_sign1_untagged(); +#endif /* Key tests */ test_cose_key_init();