From aed5bd4efd0ed149181ea31448b91864e62f8321 Mon Sep 17 00:00:00 2001 From: aidan garske Date: Thu, 16 Jul 2026 18:33:35 -0700 Subject: [PATCH] Add WOLFCOSE_ENABLE_EXT_SIGN seam for delegated signature callbacks --- include/wolfcose/settings.h | 18 ++++ include/wolfcose/wolfcose.h | 56 +++++++++++- src/wolfcose.c | 169 ++++++++++++++++++++++++++++++++++++ src/wolfcose_internal.h | 21 +++++ tests/force_failure.h | 3 + 5 files changed, 266 insertions(+), 1 deletion(-) diff --git a/include/wolfcose/settings.h b/include/wolfcose/settings.h index be40b95..358884e 100644 --- a/include/wolfcose/settings.h +++ b/include/wolfcose/settings.h @@ -104,6 +104,19 @@ extern "C" { #endif #endif /* WOLFCOSE_LEAN_MLDSA */ +/* ----- Integration seams ----- + * + * Opt-in in every build, including a full one. These are not algorithms, so + * there is no "wolfSSL provides the primitive" clause to auto-enable them and a + * default build is byte-identical to one built without them. + * ----- */ + +/* External/delegated signing: the caller supplies the signature over the + * to-be-signed bytes, so no private key material enters wolfCOSE — extension. */ +#if defined(WOLFCOSE_ENABLE_EXT_SIGN) + #define WOLFCOSE_EXT_SIGN +#endif + /* ----- Signature algorithms ----- */ /* ES256 — core (on whenever wolfSSL has ECC) */ @@ -503,6 +516,11 @@ extern "C" { #error "wolfCOSE: ML-DSA enabled but WOLFCOSE_MAX_SCRATCH_SZ too small" #endif +#if defined(WOLFCOSE_EXT_SIGN) && !defined(WOLFCOSE_SIGN1_SIGN) && \ + !defined(WOLFCOSE_SIGN_SIGN) + #error "WOLFCOSE_ENABLE_EXT_SIGN requires an enabled signing operation" +#endif + #ifdef __cplusplus } #endif diff --git a/include/wolfcose/wolfcose.h b/include/wolfcose/wolfcose.h index b6855e5..c8fc9bf 100644 --- a/include/wolfcose/wolfcose.h +++ b/include/wolfcose/wolfcose.h @@ -303,6 +303,33 @@ typedef struct WOLFCOSE_HDR { /** \brief Flag indicating payload is detached (RFC 9052 Section 2) */ #define WOLFCOSE_HDR_FLAG_DETACHED 0x01u +#if defined(WOLFCOSE_EXT_SIGN) +/** + * \brief Caller-supplied signature callback (RFC 9052 Section 4.4). + * + * Receives what the algorithm's signature primitive consumes: the digest of the + * Sig_structure for pre-hashed algorithms (ES256/384/512, PS256/384/512), or the + * Sig_structure itself for EdDSA/Ed448/ML-DSA. + * + * Returns the COSE signature bytes exactly as they appear in the message: for + * ECDSA that is fixed-width r||s (RFC 9053 Section 2.1), which is what + * psa_sign_hash() already produces. wolfCOSE performs no conversion, but does + * check the returned length. + * + * \param cbCtx Opaque caller context, passed through untouched. + * \param alg WOLFCOSE_ALG_* being signed with. + * \param tbs To-be-signed bytes (digest, or Sig_structure for EdDSA). + * \param tbsSz Length of tbs. + * \param sig Output buffer for the signature. + * \param sigSz Capacity of sig. + * \param sigLen Output: bytes written to sig. + * \return 0 on success, non-zero to fail the operation. + */ +typedef int (*WOLFCOSE_SIGN_CB)(void* cbCtx, int32_t alg, + const uint8_t* tbs, size_t tbsSz, + uint8_t* sig, size_t sigSz, size_t* sigLen); +#endif /* WOLFCOSE_EXT_SIGN */ + /** * \brief COSE key structure. Pointers to caller-owned wolfCrypt key structs. * @@ -343,7 +370,15 @@ typedef struct WOLFCOSE_KEY { const uint8_t* mldsaSeed; /**< RFC 9964 ML-DSA private seed (32B), caller-owned */ size_t mldsaSeedLen; /**< ML-DSA private seed length */ #endif - uint8_t hasPrivate; /**< 1 if private key material present */ + uint8_t hasPrivate; /**< 1 if the key can produce signatures. With + * signCb set this means the external signer holds + * the private key, not wolfCOSE. */ +#if defined(WOLFCOSE_EXT_SIGN) + /* Appended at the end: absent unless the seam is enabled, so the struct + * layout is unchanged in a default build. */ + WOLFCOSE_SIGN_CB signCb; /**< NULL: sign locally with wolfCrypt */ + void* signCtx; /**< Opaque, passed to signCb untouched */ +#endif } WOLFCOSE_KEY; /** @@ -631,6 +666,25 @@ WOLFCOSE_API int wc_CoseKey_SetRsa(WOLFCOSE_KEY* key, RsaKey* rsaKey); WOLFCOSE_API int wc_CoseKey_SetSymmetric(WOLFCOSE_KEY* key, const uint8_t* data, size_t dataLen); +#if defined(WOLFCOSE_EXT_SIGN) +/** + * \brief Delegate signing to a caller-supplied callback. + * + * Does not touch kty/crv or the key union, so it composes with + * wc_CoseKey_SetEcc() and friends. Sets hasPrivate: for a delegated key that + * means the key can produce signatures, not that wolfCOSE holds the private + * material. With a callback set, wc_CoseSign1_Sign() accepts a NULL rng + * because the external signer owns its own randomness. + * + * \param key COSE key (must be initialized). + * \param cb Signature callback. Must not be NULL. + * \param cbCtx Opaque context passed to cb (may be NULL). + * \return WOLFCOSE_SUCCESS or negative error code. + */ +WOLFCOSE_API int wc_CoseKey_SetExtSigner(WOLFCOSE_KEY* key, + WOLFCOSE_SIGN_CB cb, void* cbCtx); +#endif + #if defined(WOLFCOSE_KEY_ENCODE) /** * \brief Encode a WOLFCOSE_KEY to CBOR COSE_Key map format. diff --git a/src/wolfcose.c b/src/wolfcose.c index 78f09c2..ac2a7a9 100644 --- a/src/wolfcose.c +++ b/src/wolfcose.c @@ -1291,6 +1291,25 @@ int wc_CoseKey_SetSymmetric(WOLFCOSE_KEY* key, const uint8_t* data, return ret; } +#if defined(WOLFCOSE_EXT_SIGN) +int wc_CoseKey_SetExtSigner(WOLFCOSE_KEY* key, WOLFCOSE_SIGN_CB cb, + void* cbCtx) +{ + int ret; + + if ((key == NULL) || (cb == NULL)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else { + key->signCb = cb; + key->signCtx = cbCtx; + key->hasPrivate = 1; + ret = WOLFCOSE_SUCCESS; + } + return ret; +} +#endif + #if defined(WOLFCOSE_KEY_ENCODE) static size_t wolfCose_KeyOptionalEntries(const WOLFCOSE_KEY* key) { @@ -3506,6 +3525,100 @@ static int wolfCose_MlDsaCheckKey(const WOLFCOSE_KEY* key, int32_t alg) } #endif /* WOLFCOSE_HAVE_MLDSA */ +#if defined(WOLFCOSE_EXT_SIGN) +/* EdDSA and ML-DSA primitives consume the Sig_structure whole; everything else + * signs a digest of it. Not derivable from wolfCose_AlgToHashType, which + * returns a stand-in hash for EdDSA. */ +static int wolfCose_AlgPreHashes(int32_t alg) +{ + int preHashes; + + switch (alg) { + case WOLFCOSE_ALG_EDDSA: + case WOLFCOSE_ALG_ML_DSA_44: + case WOLFCOSE_ALG_ML_DSA_65: + case WOLFCOSE_ALG_ML_DSA_87: + preHashes = 0; + break; + default: + preHashes = 1; + break; + } + return preHashes; +} + +int wolfCose_ExtSign(const WOLFCOSE_KEY* key, int32_t alg, + const uint8_t* sigStruct, size_t sigStructLen, + uint8_t* sig, size_t sigSz, size_t* sigLen) +{ + int ret = WOLFCOSE_SUCCESS; + enum wc_HashType hashType = WC_HASH_TYPE_NONE; + uint8_t hashBuf[WC_MAX_DIGEST_SIZE]; + int digestSz = 0; + const uint8_t* tbs = sigStruct; + size_t tbsLen = sigStructLen; + size_t coordSz = 0; + int cbRet = 0; + + if ((key == NULL) || (key->signCb == NULL) || (sigStruct == NULL) || + (sig == NULL) || (sigLen == NULL) || (sigSz == 0u)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + + if ((ret == WOLFCOSE_SUCCESS) && (wolfCose_AlgPreHashes(alg) != 0)) { + ret = wolfCose_AlgToHashType(alg, &hashType); + + if (ret == WOLFCOSE_SUCCESS) { + digestSz = wc_HashGetDigestSize(hashType); + if (digestSz <= 0) { + ret = WOLFCOSE_E_CRYPTO; + } + } + if (ret == WOLFCOSE_SUCCESS) { + INJECT_FAILURE(WOLF_FAIL_HASH, -1, + ret = wc_Hash(hashType, sigStruct, (word32)sigStructLen, + hashBuf, (word32)digestSz)); + if (ret != 0) { + ret = WOLFCOSE_E_CRYPTO; + } + } + if (ret == WOLFCOSE_SUCCESS) { + tbs = hashBuf; + tbsLen = (size_t)digestSz; + } + } + + if (ret == WOLFCOSE_SUCCESS) { + *sigLen = 0; + INJECT_FAILURE(WOLF_FAIL_EXT_SIGN, -1, + cbRet = key->signCb(key->signCtx, alg, tbs, tbsLen, + sig, sigSz, sigLen)); + if (cbRet != 0) { + ret = WOLFCOSE_E_CRYPTO; + } + } + + /* A callback is caller code: do not trust its length. */ + if ((ret == WOLFCOSE_SUCCESS) && + ((*sigLen == 0u) || (*sigLen > sigSz))) { + ret = WOLFCOSE_E_CRYPTO; + } + + /* ECDSA is fixed-width r||s (RFC 9053 Section 2.1); a short or long + * signature here would produce a malformed COSE message. */ + if ((ret == WOLFCOSE_SUCCESS) && (key->kty == WOLFCOSE_KTY_EC2)) { + ret = wolfCose_CrvKeySize(key->crv, &coordSz); + + if ((ret == WOLFCOSE_SUCCESS) && (*sigLen != (2u * coordSz))) { + ret = WOLFCOSE_E_CRYPTO; + } + } + + wolfCose_ForceZero(hashBuf, sizeof(hashBuf)); + return ret; +} +#endif /* WOLFCOSE_EXT_SIGN */ + #if defined(WOLFCOSE_SIGN1_SIGN) int wc_CoseSign1_Sign(WOLFCOSE_KEY* key, int32_t alg, const uint8_t* kid, size_t kidLen, @@ -3542,10 +3655,22 @@ int wc_CoseSign1_Sign(WOLFCOSE_KEY* key, int32_t alg, isDetached = 0u; } +#if defined(WOLFCOSE_EXT_SIGN) + if ((key == NULL) || (sigPayload == NULL) || (scratch == NULL) || + (out == NULL) || (outLen == NULL)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + /* An external signer owns its own randomness, so rng is required only when + * wolfCrypt does the signing. */ + if ((ret == WOLFCOSE_SUCCESS) && (key->signCb == NULL) && (rng == NULL)) { + ret = WOLFCOSE_E_INVALID_ARG; + } +#else if ((key == NULL) || (sigPayload == NULL) || (scratch == NULL) || (out == NULL) || (outLen == NULL) || (rng == NULL)) { ret = WOLFCOSE_E_INVALID_ARG; } +#endif #ifdef WOLFCOSE_CHECK_WORD32_LEN if ((ret == WOLFCOSE_SUCCESS) && ((wolfCose_LenFitsWord32(payloadLen) == 0) || @@ -3593,6 +3718,28 @@ int wc_CoseSign1_Sign(WOLFCOSE_KEY* key, int32_t alg, } /* Sign based on algorithm */ +#if defined(WOLFCOSE_EXT_SIGN) + if ((ret == WOLFCOSE_SUCCESS) && (key->signCb != NULL)) { + size_t extSigLen = 0; + + /* The signature lands after the Sig_structure in scratch, so no extra + * stack is needed and the capacity is whatever scratch has left rather + * than sigBuf's fixed 132. */ + if (scratchSz <= sigStructLen) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_ExtSign(key, alg, scratch, sigStructLen, + &scratch[sigStructLen], + scratchSz - sigStructLen, &extSigLen); + } + if (ret == WOLFCOSE_SUCCESS) { + sigPtr = &scratch[sigStructLen]; + sigSz = extSigLen; + } + } + else +#endif #if defined(WOLFCOSE_HAVE_EDDSA) || defined(WOLFCOSE_HAVE_ED448) if ((ret == WOLFCOSE_SUCCESS) && (alg == WOLFCOSE_ALG_EDDSA)) { word32 edSigLen = (word32)sizeof(sigBuf); @@ -4506,6 +4653,28 @@ int wc_CoseSign_Sign(const WOLFCOSE_SIGNATURE* signers, size_t signerCount, } /* Sign the hash */ +#if defined(WOLFCOSE_EXT_SIGN) + if ((ret == WOLFCOSE_SUCCESS) && (signer->key->signCb != NULL)) { + size_t extSigLen = 0; + + /* Signature goes after the Sig_structure in scratch; sigBuf is + * reserved for the local-signing branches. */ + if (scratchSz <= sigStructLen) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_ExtSign(signer->key, signer->algId, + scratch, sigStructLen, + &scratch[sigStructLen], + scratchSz - sigStructLen, &extSigLen); + } + if (ret == WOLFCOSE_SUCCESS) { + sigPtr = &scratch[sigStructLen]; + sigSz = extSigLen; + } + } + else +#endif #ifdef WOLFCOSE_HAVE_ECDSA if ((ret == WOLFCOSE_SUCCESS) && ((signer->algId == WOLFCOSE_ALG_ES256) || diff --git a/src/wolfcose_internal.h b/src/wolfcose_internal.h index 310f5ba..82fb441 100644 --- a/src/wolfcose_internal.h +++ b/src/wolfcose_internal.h @@ -299,6 +299,27 @@ WOLFCOSE_LOCAL int wolfCose_EccSignRaw(const uint8_t* hash, size_t hashLen, WC_RNG* rng, ecc_key* eccKey); #endif /* WOLFCOSE_SIGN1_SIGN || WOLFCOSE_SIGN_SIGN */ +#if defined(WOLFCOSE_EXT_SIGN) +/** + * \brief Produce a signature via the key's external signer callback. + * Pre-hashes the Sig_structure for algorithms whose primitive takes a + * digest, then validates the length the callback reports. + * \param key Key with signCb set. + * \param alg WOLFCOSE_ALG_* being signed with. + * \param sigStruct Encoded Sig_structure. + * \param sigStructLen Sig_structure length. + * \param sig Output buffer for the signature. + * \param sigSz Capacity of sig. + * \param sigLen Output: bytes written to sig. + * \return WOLFCOSE_SUCCESS or negative error code. + */ +WOLFCOSE_LOCAL int wolfCose_ExtSign(const WOLFCOSE_KEY* key, int32_t alg, + const uint8_t* sigStruct, + size_t sigStructLen, + uint8_t* sig, size_t sigSz, + size_t* sigLen); +#endif /* WOLFCOSE_EXT_SIGN */ + /** * \brief Verify a raw r||s ECC signature. * Converts raw r||s -> DER then calls wc_ecc_verify_hash. diff --git a/tests/force_failure.h b/tests/force_failure.h index 56c3e3d..2b86e36 100644 --- a/tests/force_failure.h +++ b/tests/force_failure.h @@ -98,6 +98,9 @@ typedef enum { /* Hash failures */ WOLF_FAIL_HASH, /* wc_Hash */ + /* External signer failures */ + WOLF_FAIL_EXT_SIGN, /* WOLFCOSE_SIGN_CB */ + WOLF_FAIL_COUNT } WolfForceFailure;