diff --git a/Makefile b/Makefile index fbab864..29e9f6f 100644 --- a/Makefile +++ b/Makefile @@ -64,7 +64,7 @@ SCEN_IOTFLEET = examples/scenarios/iot_fleet_config SCEN_SENSOR = examples/scenarios/sensor_attestation SCEN_BROADCAST = examples/scenarios/group_broadcast_mac -.PHONY: all shared test zeroize-test coverage tool tool-test cmdline-test demo demos lean-verify mldsa-demo mldsa-verify comprehensive scenarios interop-tcose c99-check clean +.PHONY: all shared test zeroize-test ext-callback-test coverage tool tool-test cmdline-test demo demos lean-verify mldsa-demo mldsa-verify comprehensive scenarios interop-tcose c99-check clean # --- Core library --- all: $(LIB_A) @@ -90,6 +90,12 @@ zeroize-test: -o $(TEST_BIN) $(SRC) $(TEST_SRC) $(LDFLAGS) ./$(TEST_BIN) +# --- Delegated sign/verify seam test: exercises the ext-sign + ext-verify callbacks --- +ext-callback-test: + $(CC) $(CFLAGS) -DWOLFCOSE_ENABLE_EXT_SIGN -DWOLFCOSE_ENABLE_EXT_VERIFY \ + -o $(TEST_BIN) $(SRC) $(TEST_SRC) $(LDFLAGS) + ./$(TEST_BIN) + # --- Coverage --- coverage: clean $(CC) $(CFLAGS) --coverage -fprofile-arcs -ftest-coverage -c src/wolfcose_cbor.c -o src/wolfcose_cbor.o diff --git a/include/wolfcose/settings.h b/include/wolfcose/settings.h index be40b95..3f05985 100644 --- a/include/wolfcose/settings.h +++ b/include/wolfcose/settings.h @@ -104,6 +104,24 @@ 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 + +/* Delegated verification: caller checks the signature; mirror of EXT_SIGN. */ +#if defined(WOLFCOSE_ENABLE_EXT_VERIFY) + #define WOLFCOSE_EXT_VERIFY +#endif + /* ----- Signature algorithms ----- */ /* ES256 — core (on whenever wolfSSL has ECC) */ @@ -503,6 +521,16 @@ 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 + +#if defined(WOLFCOSE_EXT_VERIFY) && !defined(WOLFCOSE_SIGN1_VERIFY) && \ + !defined(WOLFCOSE_SIGN_VERIFY) + #error "WOLFCOSE_ENABLE_EXT_VERIFY requires an enabled verify operation" +#endif + #ifdef __cplusplus } #endif diff --git a/include/wolfcose/wolfcose.h b/include/wolfcose/wolfcose.h index b6855e5..8da89e9 100644 --- a/include/wolfcose/wolfcose.h +++ b/include/wolfcose/wolfcose.h @@ -303,6 +303,53 @@ 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 */ + +#if defined(WOLFCOSE_EXT_VERIFY) +/** + * \brief Caller-supplied verification callback (RFC 9052 Section 4.4). + * + * Mirror of WOLFCOSE_SIGN_CB: wolfCOSE hands the caller the digest and r||s so + * no public-key operation runs in wolfCOSE, matching psa_verify_hash(). + * + * \param cbCtx Opaque caller context, passed through untouched. + * \param alg WOLFCOSE_ALG_* being verified. + * \param tbs To-be-verified digest (or Sig_structure for EdDSA). + * \param tbsSz Length of tbs. + * \param sig COSE signature bytes. + * \param sigSz Length of sig. + * \return 0 when the signature is valid, non-zero otherwise. + */ +typedef int (*WOLFCOSE_VERIFY_CB)(void* cbCtx, int32_t alg, + const uint8_t* tbs, size_t tbsSz, + const uint8_t* sig, size_t sigSz); +#endif /* WOLFCOSE_EXT_VERIFY */ + /** * \brief COSE key structure. Pointers to caller-owned wolfCrypt key structs. * @@ -343,7 +390,19 @@ 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 +#if defined(WOLFCOSE_EXT_VERIFY) + WOLFCOSE_VERIFY_CB verifyCb; /**< NULL: verify locally with wolfCrypt */ + void* verifyCtx; /**< Opaque, passed to verifyCb untouched */ +#endif } WOLFCOSE_KEY; /** @@ -631,6 +690,41 @@ 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_EXT_VERIFY) +/** + * \brief Delegate signature verification to a caller-supplied callback. + * + * Mirror of wc_CoseKey_SetExtSigner(): wolfCOSE parses and hashes, then hands + * the check to cb. The key union carries no ecc_key; kty and crv must match alg. + * + * \param key COSE key (must be initialized). + * \param cb Verification 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_SetExtVerifier(WOLFCOSE_KEY* key, + WOLFCOSE_VERIFY_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..6ebb8ce 100644 --- a/src/wolfcose.c +++ b/src/wolfcose.c @@ -1291,6 +1291,43 @@ 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_EXT_VERIFY) +int wc_CoseKey_SetExtVerifier(WOLFCOSE_KEY* key, WOLFCOSE_VERIFY_CB cb, + void* cbCtx) +{ + int ret; + + if ((key == NULL) || (cb == NULL)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else { + key->verifyCb = cb; + key->verifyCtx = cbCtx; + ret = WOLFCOSE_SUCCESS; + } + return ret; +} +#endif + #if defined(WOLFCOSE_KEY_ENCODE) static size_t wolfCose_KeyOptionalEntries(const WOLFCOSE_KEY* key) { @@ -3506,6 +3543,137 @@ static int wolfCose_MlDsaCheckKey(const WOLFCOSE_KEY* key, int32_t alg) } #endif /* WOLFCOSE_HAVE_MLDSA */ +#if defined(WOLFCOSE_EXT_SIGN) +/* Reject algorithms this build lacks; report whether alg pre-hashes. */ +static int wolfCose_ExtSignAlg(int32_t alg, int* preHashes) +{ + int ret = WOLFCOSE_SUCCESS; + + switch (alg) { +#if defined(WOLFCOSE_HAVE_ES256) + case WOLFCOSE_ALG_ES256: + *preHashes = 1; + break; +#endif +#if defined(WOLFCOSE_HAVE_ES384) + case WOLFCOSE_ALG_ES384: + *preHashes = 1; + break; +#endif +#if defined(WOLFCOSE_HAVE_ES512) + case WOLFCOSE_ALG_ES512: + *preHashes = 1; + break; +#endif +#if defined(WOLFCOSE_HAVE_PS256) + case WOLFCOSE_ALG_PS256: + *preHashes = 1; + break; +#endif +#if defined(WOLFCOSE_HAVE_PS384) + case WOLFCOSE_ALG_PS384: + *preHashes = 1; + break; +#endif +#if defined(WOLFCOSE_HAVE_PS512) + case WOLFCOSE_ALG_PS512: + *preHashes = 1; + break; +#endif +#if defined(WOLFCOSE_HAVE_EDDSA) || defined(WOLFCOSE_HAVE_ED448) + case WOLFCOSE_ALG_EDDSA: + *preHashes = 0; + break; +#endif +#if defined(WOLFCOSE_HAVE_MLDSA) + case WOLFCOSE_ALG_ML_DSA_44: + case WOLFCOSE_ALG_ML_DSA_65: + case WOLFCOSE_ALG_ML_DSA_87: + *preHashes = 0; + break; +#endif + default: + ret = WOLFCOSE_E_COSE_BAD_ALG; + break; + } + return ret; +} + +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 expSigLen = 0; + int preHashes = 0; + int cbRet = 0; + + if ((key == NULL) || (key->signCb == NULL) || (sigStruct == NULL) || + (sig == NULL) || (sigLen == NULL) || (sigSz == 0u)) { + ret = WOLFCOSE_E_INVALID_ARG; + } + + /* Reject algorithms this build does not support. */ + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_ExtSignAlg(alg, &preHashes); + } + + if ((ret == WOLFCOSE_SUCCESS) && (preHashes != 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; + } + + /* Validate fixed-length signatures by algorithm, not key type. */ + if ((ret == WOLFCOSE_SUCCESS) && (alg != WOLFCOSE_ALG_EDDSA) && + (wolfCose_SigSize(alg, &expSigLen) == WOLFCOSE_SUCCESS) && + (*sigLen != expSigLen)) { + 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 +3710,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 +3773,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); @@ -4117,9 +4319,20 @@ int wc_CoseSign1_Verify(const WOLFCOSE_KEY* key, ret = wolfCose_CrvKeySize(key->crv, &coordSz); } if (ret == WOLFCOSE_SUCCESS) { - ret = wolfCose_EccVerifyRaw(sigData, sigDataLen, - hashBuf, (size_t)digestSz, - coordSz, key->key.ecc, &verified); +#if defined(WOLFCOSE_EXT_VERIFY) + if (key->verifyCb != NULL) { + /* Delegate the public-key check; no wolfCrypt ECC here. */ + verified = (key->verifyCb(key->verifyCtx, alg, + hashBuf, (size_t)digestSz, + sigData, sigDataLen) == 0) ? 1 : 0; + } + else +#endif + { + ret = wolfCose_EccVerifyRaw(sigData, sigDataLen, + hashBuf, (size_t)digestSz, + coordSz, key->key.ecc, &verified); + } } if ((ret == WOLFCOSE_SUCCESS) && (verified != 1)) { ret = WOLFCOSE_E_COSE_SIG_FAIL; @@ -4315,9 +4528,28 @@ int wc_CoseSign_Sign(const WOLFCOSE_SIGNATURE* signers, size_t signerCount, } if ((signers == NULL) || (signerCount == 0u) || (sigPayload == NULL) || - (scratch == NULL) || (out == NULL) || (outLen == NULL) || (rng == NULL)) { + (scratch == NULL) || (out == NULL) || (outLen == NULL)) { ret = WOLFCOSE_E_INVALID_ARG; } + + /* rng is required only when a signer signs locally; a delegated signer owns + * its randomness. */ + if ((ret == WOLFCOSE_SUCCESS) && (rng == NULL)) { +#if defined(WOLFCOSE_EXT_SIGN) + size_t s; + int needRng = 0; + for (s = 0; (s < signerCount) && (needRng == 0); s++) { + if ((signers[s].key == NULL) || (signers[s].key->signCb == NULL)) { + needRng = 1; + } + } + if (needRng != 0) { + ret = WOLFCOSE_E_INVALID_ARG; + } +#else + ret = WOLFCOSE_E_INVALID_ARG; +#endif + } #ifdef WOLFCOSE_CHECK_WORD32_LEN if ((ret == WOLFCOSE_SUCCESS) && ((wolfCose_LenFitsWord32(payloadLen) == 0) || @@ -4506,6 +4738,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; diff --git a/tests/test_cose.c b/tests/test_cose.c index 1eeaea6..03c0a2a 100644 --- a/tests/test_cose.c +++ b/tests/test_cose.c @@ -527,6 +527,224 @@ static void test_cose_sign1_ecc(const char* label, int32_t alg, int32_t crv, } #endif /* WOLFCOSE_HAVE_ES256 */ +#if defined(WOLFCOSE_EXT_SIGN) && defined(WOLFCOSE_EXT_VERIFY) && \ + defined(WOLFCOSE_HAVE_ES256) +typedef struct { + WC_RNG* rng; + ecc_key* key; + size_t coordSz; +} test_ext_ctx; + +/* External signer: produce fixed-width r||s, mirroring psa_sign_hash(). */ +static int test_ext_sign_cb(void* cbCtx, int32_t alg, + const uint8_t* tbs, size_t tbsSz, + uint8_t* sig, size_t sigSz, size_t* sigLen) +{ + test_ext_ctx* ctx = (test_ext_ctx*)cbCtx; + int ret; + + (void)alg; + *sigLen = sigSz; + ret = wolfCose_EccSignRaw(tbs, tbsSz, sig, sigLen, ctx->coordSz, + ctx->rng, ctx->key); + return (ret == 0) ? 0 : -1; +} + +/* External verifier: check the digest and r||s, mirroring psa_verify_hash(). */ +static int test_ext_verify_cb(void* cbCtx, int32_t alg, + const uint8_t* tbs, size_t tbsSz, + const uint8_t* sig, size_t sigSz) +{ + test_ext_ctx* ctx = (test_ext_ctx*)cbCtx; + int verified = 0; + int ret; + + (void)alg; + ret = wolfCose_EccVerifyRaw(sig, sigSz, tbs, tbsSz, ctx->coordSz, + ctx->key, &verified); + return ((ret == 0) && (verified == 1)) ? 0 : -1; +} + +static int test_ext_verify_cb_reject(void* cbCtx, int32_t alg, + const uint8_t* tbs, size_t tbsSz, + const uint8_t* sig, size_t sigSz) +{ + (void)cbCtx; + (void)alg; + (void)tbs; + (void)tbsSz; + (void)sig; + (void)sigSz; + return -1; +} + +/* Emit a wrong-length ECDSA signature; wolfCOSE must reject it by algorithm. */ +static int test_ext_sign_cb_badlen(void* cbCtx, int32_t alg, + const uint8_t* tbs, size_t tbsSz, + uint8_t* sig, size_t sigSz, size_t* sigLen) +{ + (void)cbCtx; + (void)alg; + (void)tbs; + (void)tbsSz; + (void)sigSz; + memset(sig, 0xAB, 32); + *sigLen = 32u; + return 0; +} + +/* Delegated sign + verify: no private key or public-key op on the COSE key. */ +static void test_cose_sign1_ext_sign_verify(void) +{ + WOLFCOSE_KEY signKey; + WOLFCOSE_KEY verifyKey; + ecc_key eccKey; + WC_RNG rng; + test_ext_ctx ctx; + int ret = 0; + int rngInited = 0; + int eccInited = 0; + uint8_t payload[] = "Hello wolfCOSE!"; + uint8_t kid[] = "key-1"; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + uint8_t out[512]; + size_t outLen = 0; + const uint8_t* decPayload = NULL; + size_t decPayloadLen = 0; + WOLFCOSE_HDR hdr; + + TEST_LOG(" [Sign1 ext sign+verify]\n"); + + ret = wc_InitRng(&rng); + if (ret == 0) { + rngInited = 1; + } + else { + TEST_ASSERT(0, "rng init"); + } + + if (ret == 0) { + ret = wc_ecc_init(&eccKey); + if (ret == 0) { + eccInited = 1; + } + ret = (ret == 0) ? wc_ecc_make_key(&rng, 32, &eccKey) : ret; + if (ret != 0) { + TEST_ASSERT(0, "ecc keygen"); + } + } + + ctx.rng = &rng; + ctx.key = &eccKey; + ctx.coordSz = 32u; + + /* rng is NULL to the API: the external signer owns its randomness. */ + if (ret == 0) { + ret = wc_CoseKey_Init(&signKey); + if (ret == 0) { + signKey.kty = WOLFCOSE_KTY_EC2; + signKey.crv = WOLFCOSE_CRV_P256; + ret = wc_CoseKey_SetExtSigner(&signKey, test_ext_sign_cb, &ctx); + } + TEST_ASSERT(ret == 0, "ext-sign set signer"); + TEST_ASSERT(signKey.key.ecc == NULL, "ext-sign no local ecc key"); + } + if (ret == 0) { + ret = wc_CoseSign1_Sign(&signKey, WOLFCOSE_ALG_ES256, + kid, sizeof(kid) - 1, + payload, sizeof(payload) - 1, + NULL, 0, NULL, 0, + scratch, sizeof(scratch), + out, sizeof(out), &outLen, NULL); + TEST_ASSERT(ret == 0 && outLen > 0, "ext-sign delegated sign"); + } + + /* Verify through the delegated verifier: kty/crv only, plus the callback. */ + if (ret == 0) { + ret = wc_CoseKey_Init(&verifyKey); + if (ret == 0) { + verifyKey.kty = WOLFCOSE_KTY_EC2; + verifyKey.crv = WOLFCOSE_CRV_P256; + ret = wc_CoseKey_SetExtVerifier(&verifyKey, test_ext_verify_cb, + &ctx); + } + TEST_ASSERT(ret == 0, "ext-verify set verifier"); + TEST_ASSERT(verifyKey.key.ecc == NULL, "ext-verify no local ecc key"); + } + if (ret == 0) { + ret = wc_CoseSign1_Verify(&verifyKey, out, outLen, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + &hdr, &decPayload, &decPayloadLen); + TEST_ASSERT(ret == 0, "ext-verify delegated verify"); + TEST_ASSERT(decPayloadLen == sizeof(payload) - 1 && + memcmp(decPayload, payload, decPayloadLen) == 0, + "ext-verify payload match"); + TEST_ASSERT(hdr.alg == WOLFCOSE_ALG_ES256, "ext-verify hdr alg"); + } + + /* A rejecting verifier surfaces as a signature failure. */ + if (ret == 0) { + WOLFCOSE_KEY rejectKey; + int rejRet; + (void)wc_CoseKey_Init(&rejectKey); + rejectKey.kty = WOLFCOSE_KTY_EC2; + rejectKey.crv = WOLFCOSE_CRV_P256; + (void)wc_CoseKey_SetExtVerifier(&rejectKey, test_ext_verify_cb_reject, + NULL); + rejRet = wc_CoseSign1_Verify(&rejectKey, out, outLen, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + &hdr, &decPayload, &decPayloadLen); + TEST_ASSERT(rejRet == WOLFCOSE_E_COSE_SIG_FAIL, + "ext-verify reject -> sig fail"); + } + + /* Tampering the signature must fail through the delegated verifier. */ + if (ret == 0) { + uint8_t tampered[512]; + int tamperedRet; + memcpy(tampered, out, outLen); + tampered[outLen - 1u] ^= 0xFFu; + tamperedRet = wc_CoseSign1_Verify(&verifyKey, tampered, outLen, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + &hdr, &decPayload, &decPayloadLen); + TEST_ASSERT(tamperedRet != 0, "ext-verify tampered fails"); + } + + /* Wrong-length ECDSA sig is rejected by alg size, even with kty unset. */ + if (ret == 0) { + WOLFCOSE_KEY badKey; + uint8_t badOut[512]; + size_t badOutLen = 0; + int badRet; + (void)wc_CoseKey_Init(&badKey); + (void)wc_CoseKey_SetExtSigner(&badKey, test_ext_sign_cb_badlen, NULL); + badRet = wc_CoseSign1_Sign(&badKey, WOLFCOSE_ALG_ES256, + NULL, 0, payload, sizeof(payload) - 1, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + badOut, sizeof(badOut), &badOutLen, NULL); + TEST_ASSERT(badRet == WOLFCOSE_E_CRYPTO, + "ext-sign wrong length rejected by alg"); + } + + /* NULL callbacks are rejected at set time. */ + if (ret == 0) { + WOLFCOSE_KEY tmp; + (void)wc_CoseKey_Init(&tmp); + TEST_ASSERT(wc_CoseKey_SetExtSigner(&tmp, NULL, NULL) == + WOLFCOSE_E_INVALID_ARG, "ext-sign null cb rejected"); + TEST_ASSERT(wc_CoseKey_SetExtVerifier(&tmp, NULL, NULL) == + WOLFCOSE_E_INVALID_ARG, "ext-verify null cb rejected"); + } + + if (eccInited != 0) { + (void)wc_ecc_free(&eccKey); + } + if (rngInited != 0) { + (void)wc_FreeRng(&rng); + } +} +#endif /* WOLFCOSE_EXT_SIGN && WOLFCOSE_EXT_VERIFY && WOLFCOSE_HAVE_ES256 */ + #ifdef WOLFCOSE_HAVE_EDDSA static void test_cose_sign1_eddsa(void) { @@ -17175,6 +17393,9 @@ int test_cose(void) test_cose_sign1_ecc("ES256", WOLFCOSE_ALG_ES256, WOLFCOSE_CRV_P256, 32); test_cose_sign1_with_aad(); test_cose_sign1_detached(); +#if defined(WOLFCOSE_EXT_SIGN) && defined(WOLFCOSE_EXT_VERIFY) + test_cose_sign1_ext_sign_verify(); +#endif #if defined(SIZE_MAX) && (SIZE_MAX > 0xFFFFFFFFUL) test_cose_sign1_word32_overflow_guard(); #endif