diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index 94c666e..e38fbc6 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -96,6 +96,22 @@ jobs: make zeroize-test CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -I./include -isystem $WOLFSSL_DIR/include" \ LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" + - name: Run delegated signing test + run: | + export WOLFSSL_DIR=$HOME/wolfssl-install + export LD_LIBRARY_PATH=$WOLFSSL_DIR/lib + export DYLD_LIBRARY_PATH=$WOLFSSL_DIR/lib + make ext-sign-test CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -I./include -isystem $WOLFSSL_DIR/include" \ + LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" + + - name: Run delegated signing forced-failure test + run: | + export WOLFSSL_DIR=$HOME/wolfssl-install + export LD_LIBRARY_PATH=$WOLFSSL_DIR/lib + export DYLD_LIBRARY_PATH=$WOLFSSL_DIR/lib + make ext-sign-force-failure CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -I./include -isystem $WOLFSSL_DIR/include" \ + LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" + - name: Run tool round-trip test run: | export WOLFSSL_DIR=$HOME/wolfssl-install diff --git a/.github/workflows/examples.yml b/.github/workflows/examples.yml index 38c54b5..4b85a06 100644 --- a/.github/workflows/examples.yml +++ b/.github/workflows/examples.yml @@ -70,6 +70,13 @@ jobs: make demos CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -I./include -isystem $WOLFSSL_DIR/include" \ LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" + - name: Build and run delegated signing example + run: | + export WOLFSSL_DIR=$HOME/wolfssl-install + export LD_LIBRARY_PATH=$WOLFSSL_DIR/lib + make ext-sign-demo CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Os -Wall -Wextra -Wpedantic -Wshadow -Wconversion -I./include -isystem $WOLFSSL_DIR/include" \ + LDFLAGS="-L$WOLFSSL_DIR/lib -lwolfssl" + - name: Binary size audit run: | export WOLFSSL_DIR=$HOME/wolfssl-install diff --git a/.github/workflows/misra-2012.yml b/.github/workflows/misra-2012.yml index 3a93e7f..c9c21d0 100644 --- a/.github/workflows/misra-2012.yml +++ b/.github/workflows/misra-2012.yml @@ -164,6 +164,7 @@ jobs: -DWOLFCOSE_KEY_ENCODE \ -DWOLFCOSE_KEY_DECODE \ -DWOLFCOSE_FLOAT \ + -DWOLFCOSE_ENABLE_EXT_SIGN \ -I include -I src -I $HOME/wolfssl-install/include \ src/ include/ \ 2>&1 | tee misra-report-raw.txt || true diff --git a/.github/workflows/misra-2023.yml b/.github/workflows/misra-2023.yml index fc8f2b5..ec7e1fb 100644 --- a/.github/workflows/misra-2023.yml +++ b/.github/workflows/misra-2023.yml @@ -101,6 +101,7 @@ jobs: -DHAVE_AES_KEYWRAP -DWOLFSSL_AES_DIRECT \ -DHAVE_HKDF -DHAVE_AES_CBC \ -DWOLFCOSE_FLOAT \ + -DWOLFCOSE_ENABLE_EXT_SIGN \ -I./include -isystem $WOLFSSL_DIR/include" for f in src/*.c; do gcc $MISRA_FLAGS -c "$f" -o /dev/null 2>&1 | tee -a compiler-warnings.txt || true @@ -228,6 +229,7 @@ jobs: -DWOLFCOSE_CBOR_ENCODE -DWOLFCOSE_CBOR_DECODE \ -DWOLFCOSE_KEY_ENCODE -DWOLFCOSE_KEY_DECODE \ -DWOLFCOSE_FLOAT \ + -DWOLFCOSE_ENABLE_EXT_SIGN \ > clang-tidy-report.txt 2>&1 || true - name: clang-tidy summary diff --git a/.github/workflows/sanitizer.yml b/.github/workflows/sanitizer.yml index 14da4e8..59d89a9 100644 --- a/.github/workflows/sanitizer.yml +++ b/.github/workflows/sanitizer.yml @@ -77,6 +77,20 @@ jobs: make test CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Og -g -fsanitize=address -fno-omit-frame-pointer -I./include -isystem $WOLFSSL_DIR/include" \ LDFLAGS="-fsanitize=address -L$WOLFSSL_DIR/lib -lwolfssl" + - name: Run delegated signing tests (ASan) + run: | + export WOLFSSL_DIR=$HOME/wolfssl-asan + export LD_LIBRARY_PATH=$WOLFSSL_DIR/lib + make ext-sign-test CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Og -g -fsanitize=address -fno-omit-frame-pointer -I./include -isystem $WOLFSSL_DIR/include" \ + LDFLAGS="-fsanitize=address -L$WOLFSSL_DIR/lib -lwolfssl" + + - name: Run delegated signing forced-failure tests (ASan) + run: | + export WOLFSSL_DIR=$HOME/wolfssl-asan + export LD_LIBRARY_PATH=$WOLFSSL_DIR/lib + make ext-sign-force-failure CFLAGS="-std=c99 -DHAVE_ANONYMOUS_INLINE_AGGREGATES=1 -Og -g -fsanitize=address -fno-omit-frame-pointer -I./include -isystem $WOLFSSL_DIR/include" \ + LDFLAGS="-fsanitize=address -L$WOLFSSL_DIR/lib -lwolfssl" + - name: Forced-failure coverage (ASan) run: | export WOLFSSL_DIR=$HOME/wolfssl-asan diff --git a/.gitignore b/.gitignore index 5e9757d..4773a7a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,10 @@ # Build artifacts *.o +*.o.tmp *.a *.so *.su +*.dSYM/ # Binaries tests/test_wolfcose @@ -13,6 +15,7 @@ tests/interop/t_cose/QCBOR/ tests/interop/t_cose/t_cose/ examples/sign1_verify_lean examples/sign1_mldsa +examples/ext_sign_demo examples/sign1_verify_mldsa # Editor / OS diff --git a/Makefile b/Makefile index fbab864..27173ea 100644 --- a/Makefile +++ b/Makefile @@ -49,6 +49,7 @@ MAC_DEMO = examples/mac0_demo SIGN1_DEMO = examples/sign1_demo LEANV_DEMO = examples/sign1_verify_lean MLDSA_DEMO = examples/sign1_mldsa +EXTSIGN_DEMO = examples/ext_sign_demo MLDSAV_DEMO = examples/sign1_verify_mldsa # Comprehensive tests (CI) @@ -64,7 +65,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-sign-test ext-sign-demo ext-sign-force-failure 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 +91,12 @@ zeroize-test: -o $(TEST_BIN) $(SRC) $(TEST_SRC) $(LDFLAGS) ./$(TEST_BIN) +# --- Delegated signing seam test: exercises the ext-sign callback --- +ext-sign-test: + $(CC) $(CFLAGS) -DWOLFCOSE_ENABLE_EXT_SIGN \ + -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 @@ -110,6 +117,14 @@ coverage-force-failure: clean ./$(TEST_BIN) gcov src/*.c +# --- Forced-failure coverage of the delegated seam --- +# WOLF_FAIL_EXT_SIGN lives behind both WOLFCOSE_FORCE_FAILURE and +# WOLFCOSE_ENABLE_EXT_SIGN, so it is unreachable unless both are set. +ext-sign-force-failure: clean + $(CC) $(CFLAGS) -DWOLFCOSE_FORCE_FAILURE -DWOLFCOSE_ENABLE_EXT_SIGN \ + -o $(TEST_BIN) $(SRC) $(TEST_SRC) $(FORCE_FAIL_SRC) $(LDFLAGS) + ./$(TEST_BIN) + # --- CLI Tool (compiled out of core lib) --- tool: $(LIB_A) $(CC) $(CFLAGS) -DWOLFCOSE_BUILD_TOOL -o $(TOOL_BIN) $(TOOL_SRC) $(LIB_A) $(LDFLAGS) @@ -154,6 +169,15 @@ lean-verify: @echo "=== Running lean verify-only example ===" ./$(LEANV_DEMO) +# --- Delegated signing example (WOLFCOSE_ENABLE_EXT_SIGN) --- +# Built from sources with the opt-in macro, since the prebuilt library does +# not carry the seam. +ext-sign-demo: + $(CC) $(CFLAGS) -DWOLFCOSE_ENABLE_EXT_SIGN -o $(EXTSIGN_DEMO) \ + $(EXTSIGN_DEMO).c src/wolfcose.c src/wolfcose_cbor.c $(LDFLAGS) + @echo "=== Running delegated signing example ===" + ./$(EXTSIGN_DEMO) + # --- Post-quantum ML-DSA lean sign + verify (WOLFCOSE_LEAN_MLDSA) --- # Requires wolfSSL built with ML-DSA (./configure --enable-dilithium). mldsa-demo: @@ -232,10 +256,12 @@ C99_SRC = $(SRC) $(TEST_SRC) $(TOOL_SRC) $(DEMO_SRC) \ $(ENC_DEMO).c $(MAC_DEMO).c $(SIGN1_DEMO).c \ $(COMP_SIGN).c $(COMP_ENCRYPT).c $(COMP_MAC).c $(COMP_ERRORS).c \ $(SCEN_FIRMWARE).c $(SCEN_MULTIPARTY).c $(SCEN_IOTFLEET).c \ - $(SCEN_SENSOR).c $(SCEN_BROADCAST).c -# Default features plus the opt-in WOLFCOSE_FLOAT paths, so the gate judges -# every conditionally-compiled translation unit, not just the default subset. -C99_CONFIGS = "" "-DWOLFCOSE_FLOAT" + $(SCEN_SENSOR).c $(SCEN_BROADCAST).c $(EXTSIGN_DEMO).c +# Default features plus the opt-in paths (WOLFCOSE_FLOAT, delegated signing, +# and delegated signing without EdDSA), so the gate judges every +# conditionally-compiled translation unit, not just the default subset. +C99_CONFIGS = "" "-DWOLFCOSE_FLOAT" "-DWOLFCOSE_ENABLE_EXT_SIGN" \ + "-DWOLFCOSE_ENABLE_EXT_SIGN -DWOLFCOSE_NO_EDDSA -DWOLFCOSE_NO_ED448" c99-check: @for cfg in $(C99_CONFIGS); do \ @@ -249,8 +275,10 @@ c99-check: # --- Cleanup --- clean: rm -f $(OBJ) $(TEST_BIN) $(TOOL_BIN) $(DEMO_BIN) $(ENC_DEMO) $(MAC_DEMO) \ - $(SIGN1_DEMO) $(COMP_SIGN) $(COMP_ENCRYPT) $(COMP_MAC) $(COMP_ERRORS) \ + $(EXTSIGN_DEMO) $(SIGN1_DEMO) $(COMP_SIGN) $(COMP_ENCRYPT) $(COMP_MAC) $(COMP_ERRORS) \ $(SCEN_FIRMWARE) $(SCEN_MULTIPARTY) $(SCEN_IOTFLEET) $(SCEN_SENSOR) $(SCEN_BROADCAST) \ $(INTEROP_DIR)/*.o $(INTEROP_DIR)/*.su $(INTEROP_BIN) \ - $(LIB_A) $(LIB_SO) src/*.su tests/*.su examples/comprehensive/*.su examples/scenarios/*.su \ + $(LIB_A) $(LIB_SO) src/*.su tests/*.su examples/*.su examples/comprehensive/*.su examples/scenarios/*.su \ src/*.gcno src/*.gcda tests/*.gcno tests/*.gcda *.gcov + rm -rf tests/*.dSYM tools/*.dSYM examples/*.dSYM \ + examples/comprehensive/*.dSYM examples/scenarios/*.dSYM diff --git a/docs/Macros.md b/docs/Macros.md index 8cbdb09..ef5a4a8 100644 --- a/docs/Macros.md +++ b/docs/Macros.md @@ -120,6 +120,41 @@ Resolved internally as read-only `WOLFCOSE_KEY_WRAP`, `WOLFCOSE_ECDH`, and `WOLF --- +## Delegated Signing + +| Define | Description | Default | +|--------|-------------|---------| +| `WOLFCOSE_ENABLE_EXT_SIGN` | Opt in `wc_CoseKey_SetExtSigner()`, which delegates signing to a caller-supplied callback | off | + +Off in every build unless explicitly enabled, lean or not. Resolved internally as the read-only `WOLFCOSE_EXT_SIGN` gate. + +Intended for keys held outside wolfCOSE — an HSM, a secure element, or a TrustZone secure partition. + +What the callback receives depends on the algorithm, and getting this wrong produces a well-formed message no verifier accepts: + +| Algorithm | `tbs` holds | +|---|---| +| ES256/384/512 | the **digest** of the `Sig_structure` — sign with a sign-hash primitive (`psa_sign_hash`, `CKM_ECDSA`) and return fixed-width `r \|\| s` (RFC 9053 sec. 2.1), **not** a DER `SEQUENCE` | +| PS256/384/512 | the **digest** — sign with RSASSA-PSS, MGF1 over the same SHA-2 as the algorithm, salt length equal to the digest length (RFC 8230 sec. 2) | +| EdDSA, Ed448, ML-DSA | the **`Sig_structure` itself** — sign it with a sign-message primitive | + +It returns the raw COSE signature; wolfCOSE checks the returned length against the algorithm but performs no key operation itself. No RNG is needed. + +A delegated key needs no local *private* key, but it must declare enough for wolfCOSE to know the expected signature length: ES* and ML-DSA need nothing beyond `alg`; EdDSA needs `kty`/`crv`; PS* needs `kty` plus a local `RsaKey` attached via `wc_CoseKey_SetRsa()` for its modulus size. + +Pass a NULL callback to detach. Attaching local key material with `wc_CoseKey_SetEcc()` and friends detaches implicitly, so always call `wc_CoseKey_SetExtSigner()` last. `wc_CoseKey_Decode()` is rejected on a key that has a signer attached, rather than silently importing private material and signing locally with it. + +For a key that has no local wolfCrypt object at all, set `kty` (and `crv` for EdDSA) on the `WOLFCOSE_KEY` directly before attaching the signer — there is no setter for declaring a key type without attaching one. + +Two limits worth knowing before designing around this: + +- It does not remove the local algorithm. `WOLFCOSE_ENABLE_EXT_SIGN` still requires a signing operation, which requires at least one signature algorithm compiled in, so a build with no local signature primitive is rejected at compile time and `WOLFCOSE_LEAN_VERIFY`/`WOLFCOSE_LEAN_VERIFY_MLDSA` cannot be combined with it. Delegating ML-DSA likewise needs local ML-DSA compiled in, which raises the `WOLFCOSE_MAX_SCRATCH_SZ` default to 8192 bytes and enforces a 4096-byte minimum. +- Scratch must hold the `Sig_structure`, which embeds the payload, and delegated signing needs at least as much again for the signature: + - **ES\*, PS\*** pre-hash, so the signature reuses the `Sig_structure` space: `scratchSz >= max(Sig_structure, signature)`. Local ECDSA signs into a stack buffer and needs only the `Sig_structure`, so a small-payload ES256 case can need more scratch delegated than local. + - **EdDSA, Ed448, ML-DSA** sign the structure in place, so the signature goes after it: `scratchSz >= Sig_structure + signature`. Delegated Ed25519 needs 64 bytes more than the local path, Ed448 114. + +--- + ## CBOR Layer Gates | Define | Description | Default | diff --git a/examples/ext_sign_demo.c b/examples/ext_sign_demo.c new file mode 100644 index 0000000..17600c3 --- /dev/null +++ b/examples/ext_sign_demo.c @@ -0,0 +1,295 @@ +/* ext_sign_demo.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfCOSE. + * + * wolfCOSE is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfCOSE is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, see . + */ + +/* Delegated signing: the private key never enters wolfCOSE. + * + * Mirrors wolfBoot's hardware DICE attestation, where the key lives inside a + * secure element and the platform exposes only a sign-hash entry point. The + * secure_element_* functions below stand in for that boundary; everything + * above them is what an integrator writes. + */ + +#include +#include +#include +#include +#include + +#if !defined(WOLFCOSE_EXT_SIGN) +int main(void) +{ + printf("Build with -DWOLFCOSE_ENABLE_EXT_SIGN to run this example.\n"); + return 0; +} +#else + +/* ----- Below this line stands in for the secure element ----- */ + +static ecc_key se_key; +static WC_RNG se_rng; +static int se_ready; + +static int secure_element_init(void) +{ + int ret; + int rngUp = 0; + int keyUp = 0; + + ret = wc_InitRng(&se_rng); + if (ret == 0) { + rngUp = 1; + ret = wc_ecc_init(&se_key); + } + if (ret == 0) { + keyUp = 1; + ret = wc_ecc_make_key(&se_rng, 32, &se_key); + } + if (ret == 0) { + se_ready = 1; + } + else { + /* Unwind here: se_ready stays 0, so the free path below cannot. */ + if (keyUp != 0) { + (void)wc_ecc_free(&se_key); + } + if (rngUp != 0) { + (void)wc_FreeRng(&se_rng); + } + } + return ret; +} + +/* The platform's only signing entry point: digest in, raw R||S out. + * + * COSE wants fixed-width R||S (RFC 9053 sec. 2.1), not the DER SEQUENCE + * wc_ecc_sign_hash emits, so the conversion belongs on this side of the + * boundary. A real secure element usually returns R||S already. */ +static int secure_element_sign_hash(const uint8_t* hash, size_t hashLen, + uint8_t* sig, size_t* sigLen) +{ + uint8_t derSig[ECC_MAX_SIG_SIZE]; + word32 derLen = (word32)sizeof(derSig); + word32 rLen = 32u; + word32 sLen = 32u; + int ret; + + if ((se_ready == 0) || (*sigLen < 64u)) { + return -1; + } + + ret = wc_ecc_sign_hash(hash, (word32)hashLen, derSig, &derLen, + &se_rng, &se_key); + if (ret != 0) { + return -1; + } + + memset(sig, 0, 64); + ret = wc_ecc_sig_to_rs(derSig, derLen, sig, &rLen, &sig[32], &sLen); + if ((ret != 0) || (rLen > 32u) || (sLen > 32u)) { + return -1; + } + + /* wc_ecc_sig_to_rs left-aligns each half; COSE needs them right-aligned + * in a fixed 32-byte field. */ + memmove(&sig[32u - rLen], sig, rLen); + memset(sig, 0, 32u - rLen); + memmove(&sig[32u + (32u - sLen)], &sig[32], sLen); + memset(&sig[32], 0, 32u - sLen); + + *sigLen = 64u; + return 0; +} + +/* The only thing that legitimately crosses the boundary: the public point. */ +static int secure_element_export_public(uint8_t* qx, word32* qxLen, + uint8_t* qy, word32* qyLen) +{ + if (se_ready == 0) { + return -1; + } + return wc_ecc_export_public_raw(&se_key, qx, qxLen, qy, qyLen); +} + +static void secure_element_free(void) +{ + if (se_ready != 0) { + (void)wc_ecc_free(&se_key); + (void)wc_FreeRng(&se_rng); + se_ready = 0; + } +} + +/* ----- Above this line stands in for the secure element ----- */ + +/* Every pointer is freed only if its own init flag says it was created, so + * an early failure never frees an object that was never initialised. */ +static void demo_cleanup(WOLFCOSE_KEY* signKey, int signInited, + WOLFCOSE_KEY* verifyKey, int verifyInited, + ecc_key* pubKey, int pubInited) +{ + if ((signKey != NULL) && (signInited != 0)) { + wc_CoseKey_Free(signKey); + } + if ((verifyKey != NULL) && (verifyInited != 0)) { + wc_CoseKey_Free(verifyKey); + } + if ((pubKey != NULL) && (pubInited != 0)) { + (void)wc_ecc_free(pubKey); + } + secure_element_free(); +} + +/* wolfCOSE pre-hashes the Sig_structure for ES256, so tbs is the 32-byte + * digest and must go to a sign-hash primitive, never a sign-message one. */ +static int demo_sign_cb(void* cbCtx, int32_t alg, + const uint8_t* tbs, size_t tbsLen, + uint8_t* sig, size_t sigSz, size_t* sigLen) +{ + size_t outLen = sigSz; + + (void)cbCtx; + + /* The length check wolfCOSE performs cannot separate ES256 from another + * 64-byte algorithm, so a real signer must pin what it was built for. */ + if (alg != WOLFCOSE_ALG_ES256) { + return -1; + } + + if (secure_element_sign_hash(tbs, tbsLen, sig, &outLen) != 0) { + return -1; + } + *sigLen = outLen; + return 0; +} + +int main(void) +{ + WOLFCOSE_KEY signKey; + WOLFCOSE_KEY verifyKey; + WOLFCOSE_HDR hdr; + ecc_key pubKey; + uint8_t qx[32]; + uint8_t qy[32]; + word32 qxLen = (word32)sizeof(qx); + word32 qyLen = (word32)sizeof(qy); + int pubInited = 0; + int signInited = 0; + int verifyInited = 0; + uint8_t payload[] = "DICE attestation claims"; + uint8_t kid[] = "se-attest-1"; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + uint8_t msg[512]; + size_t msgLen = 0; + const uint8_t* gotPayload = NULL; + size_t gotPayloadLen = 0; + int ret; + + printf("=== wolfCOSE delegated signing (external key) ===\n"); + + ret = secure_element_init(); + if (ret != 0) { + printf("FAIL: secure element init: %d\n", ret); + return 1; + } + + ret = wc_CoseKey_Init(&signKey); + if (ret == 0) { + signInited = 1; + /* ES256 derives its signature length from alg alone, so the key + * declares only what the algorithm needs. No private key is set. */ + signKey.kty = WOLFCOSE_KTY_EC2; + signKey.crv = WOLFCOSE_CRV_P256; + ret = wc_CoseKey_SetExtSigner(&signKey, demo_sign_cb, NULL); + } + if (ret != 0) { + printf("FAIL: attach external signer: %d\n", ret); + demo_cleanup(&signKey, signInited, NULL, 0, &pubKey, pubInited); + return 1; + } + printf(" signer attached, local private key held: %s\n", + (signKey.hasPrivate == 0u) ? "no" : "yes"); + + /* rng is NULL: the external signer owns its own randomness. */ + ret = wc_CoseSign1_Sign(&signKey, WOLFCOSE_ALG_ES256, + kid, sizeof(kid) - 1, + payload, sizeof(payload) - 1, + NULL, 0, NULL, 0, + scratch, sizeof(scratch), + msg, sizeof(msg), &msgLen, NULL); + if (ret != 0) { + printf("FAIL: delegated sign: %d\n", ret); + demo_cleanup(&signKey, signInited, NULL, 0, &pubKey, pubInited); + return 1; + } + printf(" signed via callback, COSE_Sign1 is %zu bytes\n", msgLen); + + /* Verification takes only the public point out of the secure element and + * rebuilds a separate key from it, so no private scalar reaches wolfCOSE. + * Handing it &se_key would be shorter and would defeat the whole point. */ + ret = wc_ecc_init(&pubKey); + if (ret == 0) { + pubInited = 1; + ret = secure_element_export_public(qx, &qxLen, qy, &qyLen); + if (ret != 0) { + printf("FAIL: export public key: %d\n", ret); + } + } + if (ret == 0) { + ret = wc_ecc_import_unsigned(&pubKey, qx, qy, NULL, ECC_SECP256R1); + if (ret != 0) { + printf("FAIL: import public key: %d\n", ret); + } + } + if (ret == 0) { + ret = wc_CoseKey_Init(&verifyKey); + verifyInited = (ret == 0) ? 1 : 0; + } + if (ret == 0) { + ret = wc_CoseKey_SetEcc(&verifyKey, WOLFCOSE_CRV_P256, &pubKey); + } + if (ret == 0) { + printf(" verify key holds a private scalar: %s\n", + (verifyKey.hasPrivate == 0u) ? "no" : "yes"); + ret = wc_CoseSign1_Verify(&verifyKey, msg, msgLen, NULL, 0, NULL, 0, + scratch, sizeof(scratch), &hdr, + &gotPayload, &gotPayloadLen); + } + if (ret != 0) { + printf("FAIL: verify chain: %d\n", ret); + demo_cleanup(&signKey, signInited, &verifyKey, verifyInited, + &pubKey, pubInited); + return 1; + } + + if ((gotPayloadLen != sizeof(payload) - 1) || (gotPayload == NULL) || + (memcmp(gotPayload, payload, gotPayloadLen) != 0)) { + printf("FAIL: payload mismatch\n"); + demo_cleanup(&signKey, signInited, &verifyKey, verifyInited, + &pubKey, pubInited); + return 1; + } + printf(" verified, payload round-tripped intact\n"); + + demo_cleanup(&signKey, signInited, &verifyKey, verifyInited, + &pubKey, pubInited); + printf("PASS: delegated signing demo\n"); + return 0; +} +#endif /* WOLFCOSE_EXT_SIGN */ diff --git a/include/wolfcose/settings.h b/include/wolfcose/settings.h index be40b95..78119ef 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 needs a signing op, which needs at least one local signature algorithm; the LEAN_VERIFY profiles are incompatible" +#endif + #ifdef __cplusplus } #endif diff --git a/include/wolfcose/wolfcose.h b/include/wolfcose/wolfcose.h index 06f09e6..5688c06 100644 --- a/include/wolfcose/wolfcose.h +++ b/include/wolfcose/wolfcose.h @@ -313,6 +313,26 @@ typedef struct WOLFCOSE_HDR { /** \brief Flag indicating payload is detached (RFC 9052 Section 2) */ #define WOLFCOSE_HDR_FLAG_DETACHED 0x01u +/** + * \brief Caller-supplied signature callback (RFC 9052 Section 4.4). + * + * Return the signature exactly as it appears in the message; for ECDSA that is + * fixed-width r||s (RFC 9053 Section 2.1), not DER. wolfCOSE converts nothing + * but does check the length. See docs/Macros.md. + * + * \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); + /** * \brief COSE key structure. Pointers to caller-owned wolfCrypt key structs. * @@ -353,8 +373,21 @@ 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 wolfCOSE holds private key material. + * Independent of signCb: a delegated key may also + * carry a local key, which wc_CoseKey_Encode then + * declines to serialise. */ uint8_t attachedType; /**< WOLFCOSE_ATT_*, set by wc_CoseKey_Set*() */ + /* Appended last and present regardless of WOLFCOSE_ENABLE_EXT_SIGN, so + * that macro alone cannot make a library and an application disagree + * about sizeof(WOLFCOSE_KEY). Fields above still sit behind wolfSSL + * feature macros, which must match between the two. */ + WOLFCOSE_SIGN_CB signCb; /**< NULL: sign locally with wolfCrypt. + * wc_CoseKey_Init() is mandatory: this is + * called if non-NULL, so an uninitialised + * key is an indirect call through stack + * garbage. */ + void* signCtx; /**< Opaque, passed to signCb untouched */ } WOLFCOSE_KEY; /** @@ -642,6 +675,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. + * + * Call after any wc_CoseKey_Set*(), which detach the signer. Leaves kty/crv, + * the key union and hasPrivate alone, but wc_CoseKey_Encode() will not + * serialise a private key while a signer is attached. Permits a NULL rng. + * + * Which kty/crv each algorithm still needs: see docs/Macros.md. + * + * \param key COSE key (must be initialized). + * \param cb Signature callback, or NULL to detach. + * \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 fcf0b58..97b7e94 100644 --- a/src/wolfcose.c +++ b/src/wolfcose.c @@ -1164,6 +1164,12 @@ int wc_CoseKey_SetEcc(WOLFCOSE_KEY* key, int32_t crv, ecc_key* eccKey) key->alg = WOLFCOSE_ALG_UNSET; key->key.ecc = eccKey; key->attachedType = WOLFCOSE_ATT_ECC; +#if defined(WOLFCOSE_EXT_SIGN) + /* Attaching local material replaces a delegated signer; keeping it + * would silently sign with the previous external signer. */ + key->signCb = NULL; + key->signCtx = NULL; +#endif /* Check if private key is present */ key->hasPrivate = ((wc_ecc_size(eccKey) > 0) && (eccKey->type == ECC_PRIVATEKEY)) ? 1u : 0u; @@ -1187,6 +1193,12 @@ int wc_CoseKey_SetEd25519(WOLFCOSE_KEY* key, ed25519_key* edKey) key->alg = WOLFCOSE_ALG_UNSET; key->key.ed25519 = edKey; key->attachedType = WOLFCOSE_ATT_ED25519; +#if defined(WOLFCOSE_EXT_SIGN) + /* Attaching local material replaces a delegated signer; keeping it + * would silently sign with the previous external signer. */ + key->signCb = NULL; + key->signCtx = NULL; +#endif key->hasPrivate = (edKey->privKeySet != 0u) ? 1u : 0u; ret = WOLFCOSE_SUCCESS; } @@ -1208,6 +1220,12 @@ int wc_CoseKey_SetEd448(WOLFCOSE_KEY* key, ed448_key* edKey) key->alg = WOLFCOSE_ALG_UNSET; key->key.ed448 = edKey; key->attachedType = WOLFCOSE_ATT_ED448; +#if defined(WOLFCOSE_EXT_SIGN) + /* Attaching local material replaces a delegated signer; keeping it + * would silently sign with the previous external signer. */ + key->signCb = NULL; + key->signCtx = NULL; +#endif key->hasPrivate = (edKey->privKeySet != 0u) ? 1u : 0u; ret = WOLFCOSE_SUCCESS; } @@ -1241,6 +1259,12 @@ int wc_CoseKey_SetMlDsa_ex(WOLFCOSE_KEY* key, int32_t alg, key->crv = 0; key->key.mldsa = mlDsaKey; key->attachedType = WOLFCOSE_ATT_MLDSA; +#if defined(WOLFCOSE_EXT_SIGN) + /* Attaching local material replaces a delegated signer; keeping it + * would silently sign with the previous external signer. */ + key->signCb = NULL; + key->signCtx = NULL; +#endif key->mldsaSeed = seed; key->mldsaSeedLen = (seed != NULL) ? seedLen : (size_t)0; key->hasPrivate = (mlDsaKey->prvKeySet != 0u) ? 1u : 0u; @@ -1269,6 +1293,12 @@ int wc_CoseKey_SetRsa(WOLFCOSE_KEY* key, RsaKey* rsaKey) key->alg = WOLFCOSE_ALG_UNSET; key->key.rsa = rsaKey; key->attachedType = WOLFCOSE_ATT_RSA; +#if defined(WOLFCOSE_EXT_SIGN) + /* Attaching local material replaces a delegated signer; keeping it + * would silently sign with the previous external signer. */ + key->signCb = NULL; + key->signCtx = NULL; +#endif key->hasPrivate = ((wc_RsaEncryptSize(rsaKey) > 0) && (rsaKey->type == RSA_PRIVATE)) ? 1u : 0u; ret = WOLFCOSE_SUCCESS; @@ -1291,13 +1321,83 @@ int wc_CoseKey_SetSymmetric(WOLFCOSE_KEY* key, const uint8_t* data, key->key.symm.key = data; key->key.symm.keyLen = dataLen; key->attachedType = WOLFCOSE_ATT_SYMMETRIC; +#if defined(WOLFCOSE_EXT_SIGN) + /* Attaching local material replaces a delegated signer; keeping it + * would silently sign with the previous external signer. */ + key->signCb = NULL; + key->signCtx = NULL; +#endif key->hasPrivate = 1; ret = WOLFCOSE_SUCCESS; } return ret; } +#if defined(WOLFCOSE_SIGN1_SIGN) || defined(WOLFCOSE_SIGN_SIGN) +/* Signing needs either private key material in wolfCOSE or a delegated signer; + * hasPrivate alone conflates that with "the private key is here", which + * governs whether wc_CoseKey_Encode may serialise it. */ +static int wolfCose_KeyCanSign(const WOLFCOSE_KEY* key) +{ + int can = 0; + + if (key->hasPrivate == 1u) { + can = 1; + } +#if defined(WOLFCOSE_EXT_SIGN) + if (key->signCb != NULL) { + can = 1; + } +#endif + return can; +} +#endif /* WOLFCOSE_SIGN1_SIGN || WOLFCOSE_SIGN_SIGN */ + +#if defined(WOLFCOSE_EXT_SIGN) +int wc_CoseKey_SetExtSigner(WOLFCOSE_KEY* key, WOLFCOSE_SIGN_CB cb, + void* cbCtx) +{ + int ret; + + if (key == NULL) { + ret = WOLFCOSE_E_INVALID_ARG; + } + else { + /* hasPrivate is deliberately untouched: clearing it here would not + * be restored on detach, leaving attached local material unusable. */ + key->signCb = cb; + key->signCtx = (cb != NULL) ? cbCtx : NULL; + ret = WOLFCOSE_SUCCESS; + } + return ret; +} +#endif + #if defined(WOLFCOSE_KEY_ENCODE) + +#if defined(HAVE_ECC) || defined(WOLFCOSE_HAVE_RSA_PRIVATE_KEY) || \ + defined(WOLFCOSE_HAVE_MLDSA) || defined(WOLFCOSE_HAVE_EDDSA) || \ + defined(WOLFCOSE_HAVE_ED448) +/* A delegated key may still carry local material, but its private half + * belongs to the external signer and must not be serialised. Guarded to + * match its call sites, which are all per-algorithm. */ +static int wolfCose_KeyEmitsPrivate(const WOLFCOSE_KEY* key) +{ + int emits = 0; + + if (key->hasPrivate != 0u) { + emits = 1; + } +#if defined(WOLFCOSE_EXT_SIGN) + if (key->signCb != NULL) { + emits = 0; + } +#endif + return emits; +} + +#endif /* any asymmetric key type */ + static size_t wolfCose_KeyOptionalEntries(const WOLFCOSE_KEY* key) { size_t count = 0u; @@ -1440,7 +1540,7 @@ int wc_CoseKey_Encode(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz, /* Map: kty [, kid] [, alg], crv, x, y [, d]. Optional kid and * alg are emitted when set so the decode/encode roundtrip * preserves them. */ - mapEntries = (key->hasPrivate != 0u) ? (size_t)5 : (size_t)4; + mapEntries = (wolfCose_KeyEmitsPrivate(key) != 0) ? (size_t)5 : (size_t)4; mapEntries += wolfCose_KeyOptionalEntries(key); if (ret == WOLFCOSE_SUCCESS) { ret = wc_CBOR_EncodeMapStart(&ctx, mapEntries); @@ -1478,7 +1578,7 @@ int wc_CoseKey_Encode(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz, ret = wc_CBOR_EncodeBstr(&ctx, yBuf, (size_t)yLen); } /* -4: d (private key, optional) */ - if ((ret == WOLFCOSE_SUCCESS) && (key->hasPrivate != 0u)) { + if ((ret == WOLFCOSE_SUCCESS) && (wolfCose_KeyEmitsPrivate(key) != 0)) { uint8_t dBuf[66]; word32 dLen = (word32)sizeof(dBuf); INJECT_FAILURE(WOLF_FAIL_ECC_EXPORT_PRIVATE, -1, @@ -1524,7 +1624,7 @@ int wc_CoseKey_Encode(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz, } /* Private round-trip needs the CRT export; else public-only. */ #ifdef WOLFCOSE_HAVE_RSA_PRIVATE_KEY - if ((ret == WOLFCOSE_SUCCESS) && (key->hasPrivate != 0u)) { + if ((ret == WOLFCOSE_SUCCESS) && (wolfCose_KeyEmitsPrivate(key) != 0)) { rsaPriv = 1; #ifdef WOLF_CRYPTO_CB /* Device-backed keys have no local CRT to export. */ @@ -1723,7 +1823,7 @@ int wc_CoseKey_Encode(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz, /* Emit priv only when a valid 32-byte seed is attached; wolfCrypt * does not retain the seed, so a keypair without one (e.g. from * wc_MlDsaKey_MakeKey) is exported as a public-only AKP key. */ - if ((key->hasPrivate != 0u) && (key->mldsaSeed != NULL) && + if ((wolfCose_KeyEmitsPrivate(key) != 0) && (key->mldsaSeed != NULL) && (key->mldsaSeedLen == WOLFCOSE_MLDSA_SEED_SZ)) { emitPriv = 1; } @@ -1839,7 +1939,7 @@ int wc_CoseKey_Encode(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz, ret = WOLFCOSE_E_COSE_BAD_ALG; } - mapEntries = (key->hasPrivate != 0u) ? (size_t)4 : (size_t)3; + mapEntries = (wolfCose_KeyEmitsPrivate(key) != 0) ? (size_t)4 : (size_t)3; mapEntries += wolfCose_KeyOptionalEntries(key); if (ret == WOLFCOSE_SUCCESS) { ret = wc_CBOR_EncodeMapStart(&ctx, mapEntries); @@ -1870,7 +1970,7 @@ int wc_CoseKey_Encode(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz, ret = wc_CBOR_EncodeBstr(&ctx, pubBuf, (size_t)pubLen); } /* -4: d (private key, optional) */ - if ((ret == WOLFCOSE_SUCCESS) && (key->hasPrivate != 0u)) { + if ((ret == WOLFCOSE_SUCCESS) && (wolfCose_KeyEmitsPrivate(key) != 0)) { uint8_t privBuf[57]; /* Ed448 priv = 57 bytes */ word32 privLen = (word32)sizeof(privBuf); #ifdef WOLFCOSE_HAVE_EDDSA @@ -1920,7 +2020,16 @@ int wc_CoseKey_Encode(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz, /* {1: 4, -1: k_bytes} */ size_t mapEntries = 2u + wolfCose_KeyOptionalEntries(key); - ret = wc_CBOR_EncodeMapStart(&ctx, mapEntries); +#if defined(WOLFCOSE_EXT_SIGN) + /* k is the whole key, so there is no public-only form to fall + * back on the way the asymmetric types have. */ + if (key->signCb != NULL) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } +#endif + if (ret == WOLFCOSE_SUCCESS) { + ret = wc_CBOR_EncodeMapStart(&ctx, mapEntries); + } if (ret == WOLFCOSE_SUCCESS) { ret = wc_CBOR_EncodeUint(&ctx, (uint64_t)WOLFCOSE_KEY_LABEL_KTY); } @@ -1947,8 +2056,14 @@ int wc_CoseKey_Encode(WOLFCOSE_KEY* key, uint8_t* out, size_t outSz, } /* Cleanup: zero output buffer on error */ - if ((ret != WOLFCOSE_SUCCESS) && (out != NULL)) { - (void)wolfCose_ForceZero(out, outSz); + if (ret != WOLFCOSE_SUCCESS) { + if (out != NULL) { + (void)wolfCose_ForceZero(out, outSz); + } + /* out is zeroed above, so a stale length would describe nothing. */ + if (outLen != NULL) { + *outLen = 0; + } } return ret; @@ -2038,6 +2153,14 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz) if ((key == NULL) || (in == NULL) || (inSz == 0u)) { ret = WOLFCOSE_E_INVALID_ARG; } +#if defined(WOLFCOSE_EXT_SIGN) + else if (key->signCb != NULL) { + /* Decoding here would import private material into a key whose whole + * point is that wolfCOSE holds none, and silently sign locally with + * it. Detach first if the key is genuinely being re-purposed. */ + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } +#endif else { ctx.cbuf = in; ctx.bufSz = inSz; @@ -2508,7 +2631,9 @@ int wc_CoseKey_Decode(WOLFCOSE_KEY* key, const uint8_t* in, size_t inSz) #endif /* WOLFCOSE_KEY_DECODE */ /* ----- Internal: RSA-PSS hash-to-MGF mapping ----- */ -#ifdef WOLFCOSE_HAVE_RSAPSS +#if defined(WOLFCOSE_HAVE_RSAPSS) && \ + (defined(WOLFCOSE_SIGN1_SIGN) || defined(WOLFCOSE_SIGN1_VERIFY) || \ + defined(WOLFCOSE_SIGN_SIGN) || defined(WOLFCOSE_SIGN_VERIFY)) static int wolfCose_HashToMgf(enum wc_HashType hashType, int* mgf) { int ret = WOLFCOSE_SUCCESS; @@ -3497,31 +3622,12 @@ static int wolfCose_LenFitsWord32(size_t n) /* ----- COSE_Sign1 API ----- */ -#if defined(WOLFCOSE_SIGN1) - -/** - * Build the Sig_structure for COSE_Sign1 (wrapper for unified builder): - * ["Signature1", body_protected, external_aad, payload] - */ -static int wolfCose_BuildSigStructure(const uint8_t* protectedHdr, - size_t protectedLen, - const uint8_t* extAad, size_t extAadLen, - const uint8_t* payload, - size_t payloadLen, - uint8_t* scratch, size_t scratchSz, - size_t* structLen) -{ - /* Use unified builder with "Signature1" context, no sign_protected */ - return wolfCose_BuildToBeSignedMaced( - WOLFCOSE_CTX_SIGNATURE1, sizeof(WOLFCOSE_CTX_SIGNATURE1), - protectedHdr, protectedLen, - NULL, 0, /* no sign_protected for Sign1 */ - extAad, extAadLen, - payload, payloadLen, - scratch, scratchSz, structLen); -} - -#ifdef WOLFCOSE_HAVE_MLDSA +/* Used by both COSE_Sign1 and COSE_Sign, so kept outside the + * WOLFCOSE_SIGN1 region below, but narrowed to the signing and verifying + * operations that actually call them. */ +#if defined(WOLFCOSE_HAVE_MLDSA) && \ + (defined(WOLFCOSE_SIGN1_SIGN) || defined(WOLFCOSE_SIGN1_VERIFY) || \ + defined(WOLFCOSE_SIGN_SIGN) || defined(WOLFCOSE_SIGN_VERIFY)) /* Map an ML-DSA COSE algorithm to the FIPS 204 security level its key must * report, so a key of the wrong level cannot satisfy a higher-level alg. */ static int wolfCose_MlDsaAlgLevel(int32_t alg, byte* level) @@ -3567,6 +3673,289 @@ static int wolfCose_MlDsaCheckKey(const WOLFCOSE_KEY* key, int32_t alg) } #endif /* WOLFCOSE_HAVE_MLDSA */ +/* Delegated signing is reachable from COSE_Sign1 and COSE_Sign alike, + * so these live outside the WOLFCOSE_SIGN1 region below. */ +#if defined(WOLFCOSE_EXT_SIGN) +/* Reject algorithms this build lacks; report whether alg pre-hashes. */ +/* Exact signature length for this key and algorithm. wolfCose_SigSize() alone + * cannot serve the delegated path: it reports EdDSA's worst case rather than + * the key's curve, and has no RSA case at all, so a guard built on it silently + * skips both. Fails closed when the length cannot be determined. */ +static int wolfCose_ExtSignSigLen(const WOLFCOSE_KEY* key, int32_t alg, + size_t* expSigLen) +{ + int ret; + + (void)key; + + switch (alg) { +#if defined(WOLFCOSE_HAVE_EDDSA) || defined(WOLFCOSE_HAVE_ED448) + case WOLFCOSE_ALG_EDDSA: + if (key->kty != WOLFCOSE_KTY_OKP) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } +#ifdef WOLFCOSE_HAVE_EDDSA + else if (key->crv == WOLFCOSE_CRV_ED25519) { + *expSigLen = 64; + ret = WOLFCOSE_SUCCESS; + } +#endif +#ifdef WOLFCOSE_HAVE_ED448 + else if (key->crv == WOLFCOSE_CRV_ED448) { + *expSigLen = 114; + ret = WOLFCOSE_SUCCESS; + } +#endif + else { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + break; +#endif +#ifdef WOLFCOSE_HAVE_RSAPSS +#ifdef WOLFCOSE_HAVE_PS256 + case WOLFCOSE_ALG_PS256: +#endif +#ifdef WOLFCOSE_HAVE_PS384 + case WOLFCOSE_ALG_PS384: +#endif +#ifdef WOLFCOSE_HAVE_PS512 + case WOLFCOSE_ALG_PS512: +#endif + { + if ((key->kty != WOLFCOSE_KTY_RSA) || + (key->attachedType != WOLFCOSE_ATT_RSA) || + (key->key.rsa == NULL)) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else { + int modSz = wc_RsaEncryptSize(key->key.rsa); + + if (modSz <= 0) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else { + *expSigLen = (size_t)modSz; + ret = WOLFCOSE_SUCCESS; + } + } + } + break; +#endif + default: + ret = wolfCose_SigSize(alg, expSigLen); +#if defined(WOLFCOSE_HAVE_ECDSA) + /* ES* lengths come from alg alone, so a declared curve would + * otherwise be ignored here while the local path rejects it. + * crv 0 means the caller declared none, which stays legal. */ + if (ret == WOLFCOSE_SUCCESS) { + int32_t expectedCrv = 0; + if (alg == WOLFCOSE_ALG_ES256) { + expectedCrv = WOLFCOSE_CRV_P256; + } + else if (alg == WOLFCOSE_ALG_ES384) { + expectedCrv = WOLFCOSE_CRV_P384; + } + else if (alg == WOLFCOSE_ALG_ES512) { + expectedCrv = WOLFCOSE_CRV_P521; + } + else { + /* No action required */ + } + /* expectedCrv stays 0 for non-ECDSA, which this arm does not + * bind. A declared kty or crv is honoured for ES* the way the + * local path does; 0 means the caller declared none. */ + if (expectedCrv != 0) { + if ((key->kty != 0) && (key->kty != WOLFCOSE_KTY_EC2)) { + ret = WOLFCOSE_E_COSE_KEY_TYPE; + } + else if ((key->crv != 0) && (key->crv != expectedCrv)) { + ret = WOLFCOSE_E_COSE_BAD_ALG; + } + else { + /* No action required */ + } + } + } +#endif + break; + } + return ret; +} + +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]; + const uint8_t* tbs = sigStruct; + size_t tbsLen = sigStructLen; + size_t expSigLen = 0; + int preHashes = 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)) { + int digestSz = 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; + } + } + + /* Reject an undersized buffer before handing it to caller code, so a + * fixed-output callback cannot overrun it. Every algorithm the seam + * accepts has a determinable length, so an error here is fatal rather + * than a reason to skip the check. */ + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_ExtSignSigLen(key, alg, &expSigLen); + if ((ret == WOLFCOSE_SUCCESS) && (sigSz < expSigLen)) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } + } + + if (ret == WOLFCOSE_SUCCESS) { + int cbRet = 0; + + *sigLen = 0; + /* This buffer usually overlaps the Sig_structure, which embeds the + * payload. A callback that reports the full length but writes fewer + * bytes would otherwise publish that plaintext as signature bytes. */ + (void)wolfCose_ForceZero(sig, expSigLen); + /* Offer exactly the expected length, not the whole scratch, so a + * callback that pads to its capacity cannot reach the rest of it. + * INJECT_FAILURE assigns ret, not cbRet, hence the normalised code. */ + INJECT_FAILURE(WOLF_FAIL_EXT_SIGN, WOLFCOSE_E_CRYPTO, + cbRet = key->signCb(key->signCtx, alg, tbs, tbsLen, + sig, expSigLen, 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; + } + + /* Every supported algorithm emits a fixed length, so a mismatch means the + * callback is wrong regardless of which family it belongs to. */ + if ((ret == WOLFCOSE_SUCCESS) && (*sigLen != expSigLen)) { + ret = WOLFCOSE_E_CRYPTO; + } + + (void)wolfCose_ForceZero(hashBuf, sizeof(hashBuf)); + return ret; +} +#endif /* WOLFCOSE_EXT_SIGN */ + +#if defined(WOLFCOSE_SIGN1) + +/** + * Build the Sig_structure for COSE_Sign1 (wrapper for unified builder): + * ["Signature1", body_protected, external_aad, payload] + */ +static int wolfCose_BuildSigStructure(const uint8_t* protectedHdr, + size_t protectedLen, + const uint8_t* extAad, size_t extAadLen, + const uint8_t* payload, + size_t payloadLen, + uint8_t* scratch, size_t scratchSz, + size_t* structLen) +{ + /* Use unified builder with "Signature1" context, no sign_protected */ + return wolfCose_BuildToBeSignedMaced( + WOLFCOSE_CTX_SIGNATURE1, sizeof(WOLFCOSE_CTX_SIGNATURE1), + protectedHdr, protectedLen, + NULL, 0, /* no sign_protected for Sign1 */ + extAad, extAadLen, + payload, payloadLen, + scratch, scratchSz, structLen); +} + + + #if defined(WOLFCOSE_SIGN1_SIGN) int wc_CoseSign1_Sign(WOLFCOSE_KEY* key, int32_t alg, const uint8_t* kid, size_t kidLen, @@ -3603,10 +3992,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) || @@ -3630,7 +4031,7 @@ int wc_CoseSign1_Sign(WOLFCOSE_KEY* key, int32_t alg, ret = WOLFCOSE_E_INVALID_ARG; } - if ((ret == WOLFCOSE_SUCCESS) && (key->hasPrivate != 1u)) { + if ((ret == WOLFCOSE_SUCCESS) && (wolfCose_KeyCanSign(key) == 0)) { ret = WOLFCOSE_E_COSE_KEY_TYPE; } /* Honour the key->alg pin. */ @@ -3654,6 +4055,35 @@ 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; + size_t sigOff = 0; + int extPreHash = 0; + + /* A pre-hashing algorithm has its digest copied out before the + * callback runs, so the signature may reuse the whole scratch. + * EdDSA and ML-DSA sign the Sig_structure in place, so there the + * signature has to start past it. */ + ret = wolfCose_ExtSignAlg(alg, &extPreHash); + if (ret == WOLFCOSE_SUCCESS) { + sigOff = (extPreHash != 0) ? 0u : sigStructLen; + if (scratchSz <= sigOff) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_ExtSign(key, alg, scratch, sigStructLen, + &scratch[sigOff], + scratchSz - sigOff, &extSigLen); + } + if (ret == WOLFCOSE_SUCCESS) { + sigPtr = &scratch[sigOff]; + 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); @@ -3931,8 +4361,14 @@ int wc_CoseSign1_Sign(WOLFCOSE_KEY* key, int32_t alg, if (scratch != NULL) { (void)wolfCose_ForceZero(scratch, scratchSz); } - if ((ret != WOLFCOSE_SUCCESS) && (out != NULL)) { - (void)wolfCose_ForceZero(out, outSz); + if (ret != WOLFCOSE_SUCCESS) { + if (out != NULL) { + (void)wolfCose_ForceZero(out, outSz); + } + /* out is zeroed above, so a stale length would describe nothing. */ + if (outLen != NULL) { + *outLen = 0; + } } return ret; @@ -4376,9 +4812,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) || @@ -4398,7 +4853,8 @@ int wc_CoseSign_Sign(const WOLFCOSE_SIGNATURE* signers, size_t signerCount, /* Fail-fast key/alg checks before any hashing. */ for (i = 0; (ret == WOLFCOSE_SUCCESS) && (i < signerCount); i++) { - if ((signers[i].key == NULL) || (signers[i].key->hasPrivate != 1u)) { + if ((signers[i].key == NULL) || + (wolfCose_KeyCanSign(signers[i].key) == 0)) { ret = WOLFCOSE_E_COSE_KEY_TYPE; } else if (((signers[i].kid != NULL) && (signers[i].kidLen == 0u)) || @@ -4409,6 +4865,15 @@ int wc_CoseSign_Sign(const WOLFCOSE_SIGNATURE* signers, size_t signerCount, (signers[i].key->alg != signers[i].algId)) { ret = WOLFCOSE_E_COSE_BAD_ALG; } +#if defined(WOLFCOSE_EXT_SIGN) + /* A delegated signer holds no local wolfCrypt key, so the kty and crv + * bindings below describe something that is not here. Matches the + * order wc_CoseSign1_Sign uses, where the ext-sign branch precedes + * the per-algorithm ones. */ + else if (signers[i].key->signCb != NULL) { + /* No action required */ + } +#endif #ifdef WOLFCOSE_HAVE_ECDSA else if ((signers[i].algId == WOLFCOSE_ALG_ES256) || (signers[i].algId == WOLFCOSE_ALG_ES384) || @@ -4521,6 +4986,9 @@ int wc_CoseSign_Sign(const WOLFCOSE_SIGNATURE* signers, size_t signerCount, * macro (ML-DSA). ML-DSA signs the Sig_structure directly * without a pre-hash so the hash type lookup is skipped. */ if ((ret == WOLFCOSE_SUCCESS) && +#if defined(WOLFCOSE_EXT_SIGN) + (signer->key->signCb == NULL) && +#endif (signer->algId != WOLFCOSE_ALG_ML_DSA_44) && (signer->algId != WOLFCOSE_ALG_ML_DSA_65) && (signer->algId != WOLFCOSE_ALG_ML_DSA_87)) { @@ -4546,8 +5014,12 @@ int wc_CoseSign_Sign(const WOLFCOSE_SIGNATURE* signers, size_t signerCount, } /* Hash the Sig_structure for algorithms that pre-hash. EdDSA - * and ML-DSA sign the structure directly. */ + * and ML-DSA sign the structure directly, and a delegated signer + * does its own hashing inside wolfCose_ExtSign. */ if ((ret == WOLFCOSE_SUCCESS) && +#if defined(WOLFCOSE_EXT_SIGN) + (signer->key->signCb == NULL) && +#endif (signer->algId != WOLFCOSE_ALG_EDDSA) && (signer->algId != WOLFCOSE_ALG_ML_DSA_44) && (signer->algId != WOLFCOSE_ALG_ML_DSA_65) && @@ -4567,6 +5039,34 @@ 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; + size_t sigOff = 0; + int extPreHash = 0; + + /* Same placement rule as wc_CoseSign1_Sign: only an in-place + * Sig_structure signer needs the structure kept intact. */ + ret = wolfCose_ExtSignAlg(signer->algId, &extPreHash); + if (ret == WOLFCOSE_SUCCESS) { + sigOff = (extPreHash != 0) ? 0u : sigStructLen; + if (scratchSz <= sigOff) { + ret = WOLFCOSE_E_BUFFER_TOO_SMALL; + } + } + if (ret == WOLFCOSE_SUCCESS) { + ret = wolfCose_ExtSign(signer->key, signer->algId, + scratch, sigStructLen, + &scratch[sigOff], + scratchSz - sigOff, &extSigLen); + } + if (ret == WOLFCOSE_SUCCESS) { + sigPtr = &scratch[sigOff]; + sigSz = extSigLen; + } + } + else +#endif #ifdef WOLFCOSE_HAVE_ECDSA if ((ret == WOLFCOSE_SUCCESS) && ((signer->algId == WOLFCOSE_ALG_ES256) || @@ -4749,8 +5249,14 @@ int wc_CoseSign_Sign(const WOLFCOSE_SIGNATURE* signers, size_t signerCount, if (scratch != NULL) { (void)wolfCose_ForceZero(scratch, scratchSz); } - if ((ret != WOLFCOSE_SUCCESS) && (out != NULL)) { - (void)wolfCose_ForceZero(out, outSz); + if (ret != WOLFCOSE_SUCCESS) { + if (out != NULL) { + (void)wolfCose_ForceZero(out, outSz); + } + /* out is zeroed above, so a stale length would describe nothing. */ + if (outLen != NULL) { + *outLen = 0; + } } return ret; diff --git a/src/wolfcose_internal.h b/src/wolfcose_internal.h index 310f5ba..f6b9bad 100644 --- a/src/wolfcose_internal.h +++ b/src/wolfcose_internal.h @@ -299,6 +299,7 @@ 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 */ + /** * \brief Verify a raw r||s ECC signature. * Converts raw r||s -> DER then calls wc_ecc_verify_hash. @@ -317,6 +318,27 @@ WOLFCOSE_LOCAL int wolfCose_EccVerifyRaw(const uint8_t* sigBuf, size_t sigLen, ecc_key* eccKey, int* verified); #endif /* WOLFCOSE_HAVE_ECDSA */ +#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 */ + #ifdef __cplusplus } #endif 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 dda6190..0c6939e 100644 --- a/tests/test_cose.c +++ b/tests/test_cose.c @@ -529,6 +529,1052 @@ 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_HAVE_ES256) +typedef struct { + WC_RNG* rng; + ecc_key* key; + size_t coordSz; + int called; +} 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; + ctx->called++; + *sigLen = sigSz; + ret = wolfCose_EccSignRaw(tbs, tbsSz, sig, sigLen, ctx->coordSz, + ctx->rng, ctx->key); + return (ret == 0) ? 0 : -1; +} + + +/* Reports the full signature length but writes only part of it, the natural + * result of omitting the r||s right-alignment fixup. */ +static int test_ext_sign_cb_short(void* cbCtx, int32_t alg, + const uint8_t* tbs, size_t tbsLen, uint8_t* sig, size_t sigSz, + size_t* sigLen) +{ + (void)cbCtx; + (void)alg; + (void)tbs; + (void)tbsLen; + if (sigSz < 64u) { + return -1; + } + memset(sig, 0x11, 8); + *sigLen = 64u; + return 0; +} + +/* A signer that simply fails; wolfCOSE must surface that as an error. */ +static int test_ext_sign_cb_fail(void* cbCtx, int32_t alg, + const uint8_t* tbs, size_t tbsLen, uint8_t* sig, size_t sigSz, + size_t* sigLen) +{ + (void)cbCtx; + (void)alg; + (void)tbs; + (void)tbsLen; + (void)sig; + (void)sigSz; + (void)sigLen; + 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; + if (sigSz < 32u) { + return -1; + } + memset(sig, 0xAB, 32); + *sigLen = 32u; + return 0; +} + + + +/* Delegated signing: no private key on the COSE key; verify locally. */ +static void test_cose_sign1_ext_sign(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]\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; + ctx.called = 0; + + /* 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 the delegated signature locally with the public key. */ + if (ret == 0) { + ret = wc_CoseKey_Init(&verifyKey); + if (ret == 0) { + ret = wc_CoseKey_SetEcc(&verifyKey, WOLFCOSE_CRV_P256, &eccKey); + } + TEST_ASSERT(ret == 0, "verify key set ecc"); + } + 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-sign verify"); + TEST_ASSERT(decPayloadLen == sizeof(payload) - 1 && + (decPayload != NULL) && + memcmp(decPayload, payload, decPayloadLen) == 0, + "ext-sign payload match"); + TEST_ASSERT(hdr.alg == WOLFCOSE_ALG_ES256, "ext-sign hdr alg"); + } + + /* 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"); + } + + /* Forced-failure path: reachable only when both WOLFCOSE_FORCE_FAILURE + * and WOLFCOSE_ENABLE_EXT_SIGN are set (make ext-sign-force-failure). */ +#ifdef WOLFCOSE_FORCE_FAILURE + if (ret == 0) { + WOLFCOSE_KEY injKey; + uint8_t injOut[256]; + size_t injOutLen = 1u; + int injRet; + + (void)wc_CoseKey_Init(&injKey); + injKey.kty = WOLFCOSE_KTY_EC2; + injKey.crv = WOLFCOSE_CRV_P256; + (void)wc_CoseKey_SetExtSigner(&injKey, test_ext_sign_cb, &ctx); + wolfForceFailure_Set(WOLF_FAIL_EXT_SIGN); + injRet = wc_CoseSign1_Sign(&injKey, WOLFCOSE_ALG_ES256, + NULL, 0, payload, sizeof(payload) - 1, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + injOut, sizeof(injOut), &injOutLen, NULL); + TEST_ASSERT(injRet == WOLFCOSE_E_CRYPTO, + "ext-sign forced failure surfaces"); + TEST_ASSERT(injOutLen == 0u, + "ext-sign forced failure zeroes output length"); + wolfForceFailure_Clear(); + } +#endif + + /* A short-writing callback must not publish leftover Sig_structure + * bytes, which for a detached payload is the plaintext itself. */ + if (ret == 0) { + WOLFCOSE_KEY shortKey; + uint8_t shortOut[512]; + size_t shortOutLen = 0; + int shortRet; + size_t si; + int leaked = 0; + + (void)wc_CoseKey_Init(&shortKey); + shortKey.kty = WOLFCOSE_KTY_EC2; + shortKey.crv = WOLFCOSE_CRV_P256; + (void)wc_CoseKey_SetExtSigner(&shortKey, test_ext_sign_cb_short, NULL); + shortRet = wc_CoseSign1_Sign(&shortKey, WOLFCOSE_ALG_ES256, NULL, 0, + NULL, 0, payload, sizeof(payload) - 1, NULL, 0, + scratch, sizeof(scratch), shortOut, sizeof(shortOut), + &shortOutLen, NULL); + TEST_ASSERT(shortRet == WOLFCOSE_SUCCESS, "ext-sign short write signs"); + for (si = 0; (si + sizeof(payload) - 1) <= shortOutLen; si++) { + if (memcmp(&shortOut[si], payload, sizeof(payload) - 1) == 0) { + leaked = 1; + } + } + TEST_ASSERT(leaked == 0, + "ext-sign short write leaks no detached payload"); + } + + /* A callback that fails must surface as an error, not a bogus message. */ + if (ret == 0) { + WOLFCOSE_KEY failKey; + uint8_t failOut[256]; + size_t failOutLen = 1u; + int failRet; + + (void)wc_CoseKey_Init(&failKey); + (void)wc_CoseKey_SetExtSigner(&failKey, test_ext_sign_cb_fail, NULL); + failRet = wc_CoseSign1_Sign(&failKey, WOLFCOSE_ALG_ES256, + NULL, 0, payload, sizeof(payload) - 1, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + failOut, sizeof(failOut), &failOutLen, NULL); + TEST_ASSERT(failRet == WOLFCOSE_E_CRYPTO, + "ext-sign callback failure surfaces as crypto error"); + TEST_ASSERT(failOutLen == 0u, + "ext-sign callback failure zeroes output length"); + } + + /* A NULL callback detaches, and the detached key can no longer sign. */ + if (ret == 0) { + WOLFCOSE_KEY tmp; + uint8_t tmpOut[256]; + size_t tmpOutLen = 0; + int tmpRet; + + (void)wc_CoseKey_Init(&tmp); + TEST_ASSERT(wc_CoseKey_SetExtSigner(&tmp, test_ext_sign_cb, &ctx) == + WOLFCOSE_SUCCESS, "ext-sign attach"); + TEST_ASSERT(wc_CoseKey_SetExtSigner(&tmp, NULL, NULL) == + WOLFCOSE_SUCCESS, "ext-sign detach accepted"); + /* rng is supplied so the NULL-rng precheck cannot mask the result: + * the detach itself must be what refuses the signature. */ + tmpRet = wc_CoseSign1_Sign(&tmp, WOLFCOSE_ALG_ES256, + NULL, 0, payload, sizeof(payload) - 1, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + tmpOut, sizeof(tmpOut), &tmpOutLen, &rng); + TEST_ASSERT(tmpRet == WOLFCOSE_E_COSE_KEY_TYPE, + "ext-sign detached key cannot sign"); + } + + /* Attaching a local key drops a previously installed delegated signer. + * The callback signs with the same ecc_key, so a successful sign proves + * nothing; only the invocation count distinguishes the two paths. */ + if (ret == 0) { + WOLFCOSE_KEY reKey; + uint8_t reOut[256]; + size_t reOutLen = 0; + int reRet; + int calledBefore; + + (void)wc_CoseKey_Init(&reKey); + TEST_ASSERT(wc_CoseKey_SetExtSigner(&reKey, test_ext_sign_cb, &ctx) == + WOLFCOSE_SUCCESS, "ext-sign attach before SetEcc"); + reRet = wc_CoseKey_SetEcc(&reKey, WOLFCOSE_CRV_P256, &eccKey); + TEST_ASSERT(reRet == WOLFCOSE_SUCCESS, "ext-sign SetEcc over signer"); + calledBefore = ctx.called; + reRet = wc_CoseSign1_Sign(&reKey, WOLFCOSE_ALG_ES256, + NULL, 0, payload, sizeof(payload) - 1, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + reOut, sizeof(reOut), &reOutLen, &rng); + TEST_ASSERT(reRet == WOLFCOSE_SUCCESS, "ext-sign local key signs"); + TEST_ASSERT(reOutLen > 0u, "ext-sign local key produced output"); + TEST_ASSERT(ctx.called == calledBefore, + "ext-sign SetEcc detached the delegated signer"); + wc_CoseKey_Free(&reKey); + } + + /* Attaching a signer over a local private key must stop the private + * scalar being serialised, since the doc says the key is not here. */ +#if defined(WOLFCOSE_KEY_ENCODE) + if (ret == 0) { + WOLFCOSE_KEY privKey; + uint8_t privBuf[256]; + size_t privLen = 0; + int privRet; + + (void)wc_CoseKey_Init(&privKey); + privRet = wc_CoseKey_SetEcc(&privKey, WOLFCOSE_CRV_P256, &eccKey); + TEST_ASSERT(privRet == WOLFCOSE_SUCCESS, "ext-sign SetEcc private"); + TEST_ASSERT(privKey.hasPrivate == 1u, "ext-sign private key attached"); + TEST_ASSERT(wc_CoseKey_SetExtSigner(&privKey, test_ext_sign_cb, &ctx) + == WOLFCOSE_SUCCESS, "ext-sign attach over private key"); + privRet = wc_CoseKey_Encode(&privKey, privBuf, sizeof(privBuf), + &privLen); + TEST_ASSERT(privRet == WOLFCOSE_SUCCESS, "ext-sign encode delegated"); + /* An EC2 COSE_Key is a 4-entry map public (kty/crv/x/y) and a 5-entry + * map once the private d is emitted. Scanning for the label byte + * would false-positive on random coordinate bytes. */ + TEST_ASSERT(privLen > 0u && privBuf[0] == 0xA4u, + "ext-sign delegated encode omits private scalar"); + + /* Detach must be reversible: the local key was never destroyed, so + * both signing and private-key export come back. */ + TEST_ASSERT(wc_CoseKey_SetExtSigner(&privKey, NULL, NULL) == + WOLFCOSE_SUCCESS, "ext-sign detach over private key"); + privLen = 0; + privRet = wc_CoseKey_Encode(&privKey, privBuf, sizeof(privBuf), + &privLen); + TEST_ASSERT(privRet == WOLFCOSE_SUCCESS, "ext-sign encode after detach"); + TEST_ASSERT(privLen > 0u && privBuf[0] == 0xA5u, + "ext-sign detach restores private scalar export"); + privLen = 0; + privRet = wc_CoseSign1_Sign(&privKey, WOLFCOSE_ALG_ES256, + NULL, 0, payload, sizeof(payload) - 1, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + privBuf, sizeof(privBuf), &privLen, &rng); + TEST_ASSERT(privRet == WOLFCOSE_SUCCESS && privLen > 0u, + "ext-sign local signing works after detach"); + wc_CoseKey_Free(&privKey); + } +#endif + + /* Decoding into a signer-set key must fail closed rather than import + * private material and silently sign locally with it. */ +#if defined(WOLFCOSE_KEY_ENCODE) && defined(WOLFCOSE_KEY_DECODE) + if (ret == 0) { + WOLFCOSE_KEY encKey; + WOLFCOSE_KEY decKey; + uint8_t keyBuf[128]; + size_t keyLen = 0; + int encRet; + int decRet; + + (void)wc_CoseKey_Init(&encKey); + encRet = wc_CoseKey_SetEcc(&encKey, WOLFCOSE_CRV_P256, &eccKey); + if (encRet == 0) { + encRet = wc_CoseKey_Encode(&encKey, keyBuf, sizeof(keyBuf), + &keyLen); + } + TEST_ASSERT(encRet == 0, "encode key for decode test"); + if (encRet == 0) { + (void)wc_CoseKey_Init(&decKey); + encRet = wc_CoseKey_SetEcc(&decKey, WOLFCOSE_CRV_P256, &eccKey); + TEST_ASSERT(encRet == 0, "decode test key attach"); + encRet = wc_CoseKey_SetExtSigner(&decKey, test_ext_sign_cb, &ctx); + TEST_ASSERT(encRet == 0, "decode test signer attach"); + decRet = wc_CoseKey_Decode(&decKey, keyBuf, keyLen); + TEST_ASSERT(decRet == WOLFCOSE_E_COSE_KEY_TYPE, + "decode into delegated key rejected"); + TEST_ASSERT(decKey.signCb != NULL, + "rejected decode leaves signer attached"); + } + } +#endif + + /* An undersized signature buffer is rejected before the callback runs, so a + * fixed-output signer cannot overrun it. 56 bytes holds the Sig_structure + * but is smaller than the 64-byte ES256 signature. */ + if (ret == 0) { + WOLFCOSE_KEY smallKey; + uint8_t smallScratch[56]; + uint8_t smallOut[512]; + size_t smallOutLen = 0; + int smallRet; + + ctx.called = 0; + (void)wc_CoseKey_Init(&smallKey); + smallKey.kty = WOLFCOSE_KTY_EC2; + smallKey.crv = WOLFCOSE_CRV_P256; + (void)wc_CoseKey_SetExtSigner(&smallKey, test_ext_sign_cb, &ctx); + smallRet = wc_CoseSign1_Sign(&smallKey, WOLFCOSE_ALG_ES256, + NULL, 0, payload, sizeof(payload) - 1, + NULL, 0, NULL, 0, smallScratch, sizeof(smallScratch), + smallOut, sizeof(smallOut), &smallOutLen, NULL); + TEST_ASSERT(smallRet == WOLFCOSE_E_BUFFER_TOO_SMALL, + "ext-sign undersized buffer rejected"); + TEST_ASSERT(ctx.called == 0, + "ext-sign callback not called on undersized buffer"); + } + + if (eccInited != 0) { + (void)wc_ecc_free(&eccKey); + } + if (rngInited != 0) { + (void)wc_FreeRng(&rng); + } +} +#endif /* WOLFCOSE_EXT_SIGN && WOLFCOSE_HAVE_ES256 */ + +/* EdDSA-only, so guarded without ES256 to match its call site. */ +#if defined(WOLFCOSE_EXT_SIGN) && defined(WOLFCOSE_HAVE_EDDSA) +/* Records the capacity it was offered. The pre-fix guard skipped EdDSA and + * RSA-PSS entirely, so a fixed-output signer could be handed less room than + * its signature needs and overrun the caller's scratch. */ +static int g_ext_cap_called; + +static int test_ext_sign_cb_capture(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)sig; + (void)sigSz; + g_ext_cap_called++; + *sigLen = 0; + return -1; +} +/* An Ed25519 delegated signer must never be invoked with less than its fixed + * 64-byte output, or it overruns the scratch buffer it was handed. */ +static void test_cose_sign1_ext_sign_eddsa_capacity(void) +{ + WOLFCOSE_KEY signKey; + uint8_t payload[] = "Hello wolfCOSE!"; + uint8_t scratch[80]; + uint8_t out[512]; + size_t outLen = 0; + int ret; + + TEST_LOG(" [Sign1 ext-sign EdDSA capacity]\n"); + + g_ext_cap_called = 0; + + ret = wc_CoseKey_Init(&signKey); + TEST_ASSERT(ret == 0, "eddsa capacity key init"); + if (ret != 0) { + return; + } + signKey.kty = WOLFCOSE_KTY_OKP; + signKey.crv = WOLFCOSE_CRV_ED25519; + ret = wc_CoseKey_SetExtSigner(&signKey, test_ext_sign_cb_capture, NULL); + TEST_ASSERT(ret == 0, "eddsa capacity set signer"); + + if (ret == 0) { + /* scratch leaves well under 64 bytes once the Sig_structure is built */ + ret = wc_CoseSign1_Sign(&signKey, WOLFCOSE_ALG_EDDSA, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), out, sizeof(out), &outLen, NULL); + TEST_ASSERT(ret == WOLFCOSE_E_BUFFER_TOO_SMALL, + "eddsa undersized scratch is rejected"); + TEST_ASSERT(g_ext_cap_called == 0, + "eddsa signer not invoked with undersized buffer"); + } + + wc_CoseKey_Free(&signKey); +} +#endif /* WOLFCOSE_EXT_SIGN && WOLFCOSE_HAVE_EDDSA */ + +/* Delegated signing for the algorithm families beyond ES256. Each callback + * holds the key the way an HSM would and wolfCOSE never sees private + * material, mirroring wolfBoot's hal_dice_sign_hash() integration. */ +#if defined(WOLFCOSE_EXT_SIGN) + +#if defined(WOLFCOSE_HAVE_EDDSA) +static int test_ext_cb_ed25519(void* cbCtx, int32_t alg, + const uint8_t* tbs, size_t tbsSz, + uint8_t* sig, size_t sigSz, size_t* sigLen) +{ + ed25519_key* k = (ed25519_key*)cbCtx; + word32 len = (word32)sigSz; + int ret; + + (void)alg; + ret = wc_ed25519_sign_msg(tbs, (word32)tbsSz, sig, &len, k); + if (ret != 0) { + return -1; + } + *sigLen = (size_t)len; + return 0; +} + +static void test_cose_sign1_ext_sign_ed25519(void) +{ + WOLFCOSE_KEY signKey; + WOLFCOSE_KEY verifyKey; + ed25519_key edKey; + WC_RNG rng; + int ret = 0; + int rngInited = 0; + int edInited = 0; + uint8_t payload[] = "delegated ed25519"; + 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 Ed25519]\n"); + + ret = wc_InitRng(&rng); + TEST_ASSERT(ret == 0, "ed25519 ext rng init"); + if (ret == 0) { + rngInited = 1; + ret = wc_ed25519_init(&edKey); + TEST_ASSERT(ret == 0, "ed25519 ext key init"); + } + if (ret == 0) { + edInited = 1; + ret = wc_ed25519_make_key(&rng, ED25519_KEY_SIZE, &edKey); + TEST_ASSERT(ret == 0, "ed25519 ext keygen"); + } + + if (ret == 0) { + ret = wc_CoseKey_Init(&signKey); + signKey.kty = WOLFCOSE_KTY_OKP; + signKey.crv = WOLFCOSE_CRV_ED25519; + ret = (ret == 0) ? + wc_CoseKey_SetExtSigner(&signKey, test_ext_cb_ed25519, &edKey) : ret; + TEST_ASSERT(ret == 0, "ed25519 ext set signer"); + TEST_ASSERT(signKey.key.ed25519 == NULL, "ed25519 ext no local key"); + } + if (ret == 0) { + ret = wc_CoseSign1_Sign(&signKey, WOLFCOSE_ALG_EDDSA, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), out, sizeof(out), &outLen, NULL); + TEST_ASSERT(ret == 0 && outLen > 0, "ed25519 ext delegated sign"); + } + if (ret == 0) { + ret = wc_CoseKey_Init(&verifyKey); + ret = (ret == 0) ? wc_CoseKey_SetEd25519(&verifyKey, &edKey) : ret; + TEST_ASSERT(ret == 0, "ed25519 ext verify 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, "ed25519 ext verify"); + TEST_ASSERT(decPayloadLen == sizeof(payload) - 1 && + (decPayload != NULL) && + memcmp(decPayload, payload, decPayloadLen) == 0, + "ed25519 ext payload match"); + TEST_ASSERT(hdr.alg == WOLFCOSE_ALG_EDDSA, "ed25519 ext hdr alg"); + } + + if (edInited != 0) { + (void)wc_ed25519_free(&edKey); + } + if (rngInited != 0) { + (void)wc_FreeRng(&rng); + } +} +#endif /* WOLFCOSE_HAVE_EDDSA */ + +#if defined(WOLFCOSE_HAVE_RSAPSS) && defined(WOLFSSL_KEY_GEN) +typedef struct { + RsaKey* key; + WC_RNG* rng; + int called; +} test_ext_rsa_ctx; + +static int test_ext_cb_rsapss(void* cbCtx, int32_t alg, + const uint8_t* tbs, size_t tbsSz, + uint8_t* sig, size_t sigSz, size_t* sigLen) +{ + test_ext_rsa_ctx* c = (test_ext_rsa_ctx*)cbCtx; + int ret; + + (void)alg; + c->called++; + ret = wc_RsaPSS_Sign_ex(tbs, (word32)tbsSz, sig, (word32)sigSz, + WC_HASH_TYPE_SHA256, WC_MGF1SHA256, + (int)tbsSz, c->key, c->rng); + if (ret <= 0) { + return -1; + } + *sigLen = (size_t)ret; + return 0; +} + +static void test_cose_sign1_ext_sign_rsapss(void) +{ + WOLFCOSE_KEY signKey; + WOLFCOSE_KEY verifyKey; + static RsaKey rsaKey; + WC_RNG rng; + test_ext_rsa_ctx cbCtx; + int ret = 0; + int rngInited = 0; + int rsaInited = 0; + uint8_t payload[] = "delegated rsa-pss"; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + uint8_t out[1024]; + size_t outLen = 0; + const uint8_t* decPayload = NULL; + size_t decPayloadLen = 0; + WOLFCOSE_HDR hdr; + + TEST_LOG(" [Sign1 ext-sign RSA-PSS]\n"); + + ret = wc_InitRng(&rng); + TEST_ASSERT(ret == 0, "rsapss ext rng init"); + if (ret == 0) { + rngInited = 1; + ret = wc_InitRsaKey(&rsaKey, NULL); + TEST_ASSERT(ret == 0, "rsapss ext key init"); + } + if (ret == 0) { + rsaInited = 1; + ret = wc_MakeRsaKey(&rsaKey, 2048, WC_RSA_EXPONENT, &rng); + TEST_ASSERT(ret == 0, "rsapss ext keygen"); + } + if (ret == 0) { + ret = wc_RsaSetRNG(&rsaKey, &rng); + TEST_ASSERT(ret == 0, "rsapss ext set rng"); + } + + cbCtx.key = &rsaKey; + cbCtx.rng = &rng; + cbCtx.called = 0; + + /* SetRsa first: the modulus size gives the expected signature length, + * and SetExtSigner must come last or it would be detached. */ + if (ret == 0) { + ret = wc_CoseKey_Init(&signKey); + ret = (ret == 0) ? wc_CoseKey_SetRsa(&signKey, &rsaKey) : ret; + ret = (ret == 0) ? + wc_CoseKey_SetExtSigner(&signKey, test_ext_cb_rsapss, &cbCtx) : ret; + TEST_ASSERT(ret == 0, "rsapss ext set signer"); + } + if (ret == 0) { + ret = wc_CoseSign1_Sign(&signKey, WOLFCOSE_ALG_PS256, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), out, sizeof(out), &outLen, NULL); + TEST_ASSERT(ret == 0 && outLen > 0, "rsapss ext delegated sign"); + /* The local RSA key is attached for its modulus size, so success + * alone cannot tell the two paths apart. */ + TEST_ASSERT(cbCtx.called == 1, "rsapss ext callback produced the sig"); + } + if (ret == 0) { + ret = wc_CoseKey_Init(&verifyKey); + ret = (ret == 0) ? wc_CoseKey_SetRsa(&verifyKey, &rsaKey) : ret; + TEST_ASSERT(ret == 0, "rsapss ext verify 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, "rsapss ext verify"); + TEST_ASSERT(decPayloadLen == sizeof(payload) - 1 && + (decPayload != NULL) && + memcmp(decPayload, payload, decPayloadLen) == 0, + "rsapss ext payload match"); + } + + /* The RSA arm derives the length from the attached key, so each way of + * failing to supply one must be rejected rather than fall through to the + * algorithm-only default. */ + if (ret == 0) { + WOLFCOSE_KEY badKey; + uint8_t badOut[1024]; + size_t badOutLen = 0; + int badRet; + + /* attachedType says RSA and kty agrees, but no key object is + * attached, so only the NULL-union clause can reject this. */ + (void)wc_CoseKey_Init(&badKey); + badKey.kty = WOLFCOSE_KTY_RSA; + badKey.attachedType = WOLFCOSE_ATT_RSA; + badKey.key.rsa = NULL; + (void)wc_CoseKey_SetExtSigner(&badKey, test_ext_cb_rsapss, &cbCtx); + cbCtx.called = 0; + badRet = wc_CoseSign1_Sign(&badKey, WOLFCOSE_ALG_PS256, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), badOut, sizeof(badOut), &badOutLen, + NULL); + TEST_ASSERT(badRet == WOLFCOSE_E_COSE_KEY_TYPE, + "rsapss ext NULL key object rejected"); + TEST_ASSERT(cbCtx.called == 0, + "rsapss ext callback not run without a key"); + + /* A real RsaKey is attached, but attachedType disagrees, which is + * the clause that stops the union being read as the wrong type. */ + (void)wc_CoseKey_Init(&badKey); + badKey.kty = WOLFCOSE_KTY_RSA; + badKey.attachedType = WOLFCOSE_ATT_ECC; + badKey.key.rsa = (RsaKey*)&rsaKey; + (void)wc_CoseKey_SetExtSigner(&badKey, test_ext_cb_rsapss, &cbCtx); + cbCtx.called = 0; + badRet = wc_CoseSign1_Sign(&badKey, WOLFCOSE_ALG_PS256, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), badOut, sizeof(badOut), &badOutLen, + NULL); + TEST_ASSERT(badRet == WOLFCOSE_E_COSE_KEY_TYPE, + "rsapss ext attachedType mismatch rejected"); + TEST_ASSERT(cbCtx.called == 0, + "rsapss ext callback not run on type mismatch"); + + /* Consistent attachment, but the declared kty is not RSA. */ + (void)wc_CoseKey_Init(&badKey); + (void)wc_CoseKey_SetRsa(&badKey, &rsaKey); + badKey.kty = WOLFCOSE_KTY_EC2; + (void)wc_CoseKey_SetExtSigner(&badKey, test_ext_cb_rsapss, &cbCtx); + cbCtx.called = 0; + badRet = wc_CoseSign1_Sign(&badKey, WOLFCOSE_ALG_PS256, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), badOut, sizeof(badOut), &badOutLen, + NULL); + TEST_ASSERT(badRet == WOLFCOSE_E_COSE_KEY_TYPE, + "rsapss ext wrong kty rejected"); + TEST_ASSERT(cbCtx.called == 0, + "rsapss ext callback not run on wrong kty"); + } + + if (rsaInited != 0) { + (void)wc_FreeRsaKey(&rsaKey); + } + if (rngInited != 0) { + (void)wc_FreeRng(&rng); + } +} +#endif /* WOLFCOSE_HAVE_RSAPSS && WOLFSSL_KEY_GEN */ +#endif /* WOLFCOSE_EXT_SIGN */ + +#if defined(WOLFCOSE_EXT_SIGN) && defined(WOLFCOSE_HAVE_MLDSA) +typedef struct { + wc_MlDsaKey* key; + WC_RNG* rng; +} test_ext_mldsa_ctx; + +static int test_ext_cb_mldsa(void* cbCtx, int32_t alg, + const uint8_t* tbs, size_t tbsSz, + uint8_t* sig, size_t sigSz, size_t* sigLen) +{ + test_ext_mldsa_ctx* c = (test_ext_mldsa_ctx*)cbCtx; + word32 len = (word32)sigSz; + int ret; + + (void)alg; + /* Empty context string, matching the local ML-DSA branch. */ + ret = wc_MlDsaKey_SignCtx(c->key, NULL, 0, sig, &len, + tbs, (word32)tbsSz, c->rng); + if (ret != 0) { + return -1; + } + *sigLen = (size_t)len; + return 0; +} + +static void test_cose_sign1_ext_sign_mldsa(void) +{ + static wc_MlDsaKey dlKey; + WOLFCOSE_KEY signKey; + WOLFCOSE_KEY verifyKey; + WC_RNG rng; + test_ext_mldsa_ctx cbCtx; + int ret = 0; + int rngInited = 0; + int dlInited = 0; + uint8_t payload[] = "delegated ml-dsa"; + static uint8_t scratch[8192]; + static uint8_t out[8192]; + size_t outLen = 0; + const uint8_t* decPayload = NULL; + size_t decPayloadLen = 0; + WOLFCOSE_HDR hdr; + + TEST_LOG(" [Sign1 ext-sign ML-DSA]\n"); + + ret = wc_InitRng(&rng); + TEST_ASSERT(ret == 0, "mldsa ext rng init"); + if (ret == 0) { + rngInited = 1; + ret = wc_MlDsaKey_Init(&dlKey, NULL, INVALID_DEVID); + TEST_ASSERT(ret == 0, "mldsa ext key init"); + } + if (ret == 0) { + dlInited = 1; + ret = wc_MlDsaKey_SetParams(&dlKey, WC_ML_DSA_44); + TEST_ASSERT(ret == 0, "mldsa ext set params"); + } + if (ret == 0) { + ret = wc_MlDsaKey_MakeKey(&dlKey, &rng); + TEST_ASSERT(ret == 0, "mldsa ext keygen"); + } + + cbCtx.key = &dlKey; + cbCtx.rng = &rng; + + if (ret == 0) { + ret = wc_CoseKey_Init(&signKey); + signKey.kty = WOLFCOSE_KTY_AKP; + ret = (ret == 0) ? + wc_CoseKey_SetExtSigner(&signKey, test_ext_cb_mldsa, &cbCtx) : ret; + TEST_ASSERT(ret == 0, "mldsa ext set signer"); + } + if (ret == 0) { + ret = wc_CoseSign1_Sign(&signKey, WOLFCOSE_ALG_ML_DSA_44, NULL, 0, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), out, sizeof(out), &outLen, NULL); + TEST_ASSERT(ret == 0 && outLen > 0, "mldsa ext delegated sign"); + } + if (ret == 0) { + ret = wc_CoseKey_Init(&verifyKey); + ret = (ret == 0) ? + wc_CoseKey_SetMlDsa(&verifyKey, WOLFCOSE_ALG_ML_DSA_44, &dlKey) : ret; + TEST_ASSERT(ret == 0, "mldsa ext verify 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, "mldsa ext verify"); + TEST_ASSERT(decPayloadLen == sizeof(payload) - 1 && + (decPayload != NULL) && + memcmp(decPayload, payload, decPayloadLen) == 0, + "mldsa ext payload match"); + } + + if (dlInited != 0) { + (void)wc_MlDsaKey_Free(&dlKey); + } + if (rngInited != 0) { + (void)wc_FreeRng(&rng); + } +} +#endif /* WOLFCOSE_EXT_SIGN && WOLFCOSE_HAVE_MLDSA */ + +/* The delegated branch in wc_CoseSign_Sign had no coverage at all: a message + * mixing a local signer with a delegated one exercises both arms. */ +#if defined(WOLFCOSE_EXT_SIGN) && defined(WOLFCOSE_SIGN_SIGN) && \ + defined(WOLFCOSE_SIGN_VERIFY) && defined(WOLFCOSE_HAVE_ES256) +static void test_cose_sign_ext_sign_multi(void) +{ + WOLFCOSE_KEY localKey; + WOLFCOSE_KEY extKey; + WOLFCOSE_KEY verifyKey; + WOLFCOSE_SIGNATURE signers[2]; + ecc_key localEcc; + ecc_key extEcc; + WC_RNG rng; + test_ext_ctx ctx; + int ret = 0; + int rngInited = 0; + int localInited = 0; + int extInited = 0; + uint8_t payload[] = "multi-signer delegated"; + uint8_t kidA[] = "local"; + uint8_t kidB[] = "hsm"; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + uint8_t out[1024]; + size_t outLen = 0; + const uint8_t* decPayload = NULL; + size_t decPayloadLen = 0; + WOLFCOSE_HDR hdr; + + TEST_LOG(" [Sign multi-signer ext-sign]\n"); + + ret = wc_InitRng(&rng); + TEST_ASSERT(ret == 0, "multi ext rng init"); + if (ret == 0) { + rngInited = 1; + ret = wc_ecc_init(&localEcc); + TEST_ASSERT(ret == 0, "multi local ecc init"); + } + if (ret == 0) { + localInited = 1; + ret = wc_ecc_make_key(&rng, 32, &localEcc); + TEST_ASSERT(ret == 0, "multi local keygen"); + } + if (ret == 0) { + ret = wc_ecc_init(&extEcc); + TEST_ASSERT(ret == 0, "multi ext ecc init"); + } + if (ret == 0) { + extInited = 1; + ret = wc_ecc_make_key(&rng, 32, &extEcc); + TEST_ASSERT(ret == 0, "multi ext keygen"); + } + + ctx.rng = &rng; + ctx.key = &extEcc; + ctx.coordSz = 32u; + ctx.called = 0; + + if (ret == 0) { + ret = wc_CoseKey_Init(&localKey); + ret = (ret == 0) ? + wc_CoseKey_SetEcc(&localKey, WOLFCOSE_CRV_P256, &localEcc) : ret; + TEST_ASSERT(ret == 0, "multi local key"); + } + if (ret == 0) { + ret = wc_CoseKey_Init(&extKey); + extKey.kty = WOLFCOSE_KTY_EC2; + extKey.crv = WOLFCOSE_CRV_P256; + ret = (ret == 0) ? + wc_CoseKey_SetExtSigner(&extKey, test_ext_sign_cb, &ctx) : ret; + TEST_ASSERT(ret == 0, "multi ext signer"); + } + + signers[0].algId = WOLFCOSE_ALG_ES256; + signers[0].key = &localKey; + signers[0].kid = kidA; + signers[0].kidLen = sizeof(kidA) - 1; + signers[1].algId = WOLFCOSE_ALG_ES256; + signers[1].key = &extKey; + signers[1].kid = kidB; + signers[1].kidLen = sizeof(kidB) - 1; + + if (ret == 0) { + ret = wc_CoseSign_Sign(signers, 2, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), out, sizeof(out), &outLen, &rng); + TEST_ASSERT(ret == 0 && outLen > 0, "multi delegated sign"); + TEST_ASSERT(ctx.called == 1, "multi delegated callback invoked once"); + } + + /* Signer 0 is local, signer 1 came from the callback; both must verify. */ + if (ret == 0) { + ret = wc_CoseKey_Init(&verifyKey); + ret = (ret == 0) ? + wc_CoseKey_SetEcc(&verifyKey, WOLFCOSE_CRV_P256, &localEcc) : ret; + ret = (ret == 0) ? wc_CoseSign_Verify(&verifyKey, 0, out, outLen, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + &hdr, &decPayload, &decPayloadLen) : ret; + TEST_ASSERT(ret == 0, "multi verify local signer"); + } + if (ret == 0) { + ret = wc_CoseKey_Init(&verifyKey); + ret = (ret == 0) ? + wc_CoseKey_SetEcc(&verifyKey, WOLFCOSE_CRV_P256, &extEcc) : ret; + ret = (ret == 0) ? wc_CoseSign_Verify(&verifyKey, 1, out, outLen, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + &hdr, &decPayload, &decPayloadLen) : ret; + TEST_ASSERT(ret == 0, "multi verify delegated signer"); + TEST_ASSERT(decPayloadLen == sizeof(payload) - 1 && + (decPayload != NULL) && + memcmp(decPayload, payload, decPayloadLen) == 0, + "multi payload match"); + } + + if (localInited != 0) { + (void)wc_ecc_free(&localEcc); + } + if (extInited != 0) { + (void)wc_ecc_free(&extEcc); + } + if (rngInited != 0) { + (void)wc_FreeRng(&rng); + } +} +#endif /* EXT_SIGN && SIGN_SIGN && SIGN_VERIFY && HAVE_ES256 */ + +/* The multi-signer delegated branch places the signature past the + * Sig_structure only for algorithms that sign it in place. ES256 takes the + * other arm, so a delegated Ed25519 signer is what covers this one. */ +#if defined(WOLFCOSE_EXT_SIGN) && defined(WOLFCOSE_SIGN_SIGN) && \ + defined(WOLFCOSE_SIGN_VERIFY) && defined(WOLFCOSE_HAVE_EDDSA) +static void test_cose_sign_ext_sign_ed25519(void) +{ + WOLFCOSE_KEY signKey; + WOLFCOSE_KEY verifyKey; + WOLFCOSE_SIGNATURE signers[1]; + ed25519_key edKey; + WC_RNG rng; + int ret = 0; + int rngInited = 0; + int edInited = 0; + uint8_t payload[] = "multi delegated ed25519"; + uint8_t scratch[WOLFCOSE_MAX_SCRATCH_SZ]; + uint8_t out[1024]; + size_t outLen = 0; + const uint8_t* decPayload = NULL; + size_t decPayloadLen = 0; + WOLFCOSE_HDR hdr; + + TEST_LOG(" [Sign multi-signer ext-sign Ed25519]\n"); + + ret = wc_InitRng(&rng); + TEST_ASSERT(ret == 0, "multi ed ext rng init"); + if (ret == 0) { + rngInited = 1; + ret = wc_ed25519_init(&edKey); + TEST_ASSERT(ret == 0, "multi ed ext key init"); + } + if (ret == 0) { + edInited = 1; + ret = wc_ed25519_make_key(&rng, ED25519_KEY_SIZE, &edKey); + TEST_ASSERT(ret == 0, "multi ed ext keygen"); + } + if (ret == 0) { + ret = wc_CoseKey_Init(&signKey); + signKey.kty = WOLFCOSE_KTY_OKP; + signKey.crv = WOLFCOSE_CRV_ED25519; + ret = (ret == 0) ? + wc_CoseKey_SetExtSigner(&signKey, test_ext_cb_ed25519, &edKey) : ret; + TEST_ASSERT(ret == 0, "multi ed ext set signer"); + } + + signers[0].algId = WOLFCOSE_ALG_EDDSA; + signers[0].key = &signKey; + signers[0].kid = NULL; + signers[0].kidLen = 0; + + if (ret == 0) { + ret = wc_CoseSign_Sign(signers, 1, + payload, sizeof(payload) - 1, NULL, 0, NULL, 0, + scratch, sizeof(scratch), out, sizeof(out), &outLen, NULL); + TEST_ASSERT(ret == 0 && outLen > 0, "multi ed delegated sign"); + } + if (ret == 0) { + ret = wc_CoseKey_Init(&verifyKey); + ret = (ret == 0) ? wc_CoseKey_SetEd25519(&verifyKey, &edKey) : ret; + ret = (ret == 0) ? wc_CoseSign_Verify(&verifyKey, 0, out, outLen, + NULL, 0, NULL, 0, scratch, sizeof(scratch), + &hdr, &decPayload, &decPayloadLen) : ret; + TEST_ASSERT(ret == 0, "multi ed delegated verify"); + TEST_ASSERT(decPayloadLen == sizeof(payload) - 1 && + (decPayload != NULL) && + memcmp(decPayload, payload, decPayloadLen) == 0, + "multi ed payload match"); + } + + if (edInited != 0) { + (void)wc_ed25519_free(&edKey); + } + if (rngInited != 0) { + (void)wc_FreeRng(&rng); + } +} +#endif /* EXT_SIGN && SIGN_SIGN && SIGN_VERIFY && HAVE_EDDSA */ + #ifdef WOLFCOSE_HAVE_EDDSA static void test_cose_sign1_eddsa(void) { @@ -5444,7 +6490,8 @@ static int mutate_first_recipient_protected_alg(uint8_t* msg, size_t msgLen, { int ret = -1; WOLFCOSE_CBOR_CTX ctx; - uint64_t count = 0; + uint64_t tagVal = 0; + size_t count = 0; const uint8_t* protectedData = NULL; size_t protectedLen = 0; size_t protectedOffset; @@ -5455,7 +6502,7 @@ static int mutate_first_recipient_protected_alg(uint8_t* msg, size_t msgLen, if ((ctx.idx < ctx.bufSz) && (wc_CBOR_PeekType(&ctx) == WOLFCOSE_CBOR_TAG)) { - ret = wc_CBOR_DecodeTag(&ctx, &count); + ret = wc_CBOR_DecodeTag(&ctx, &tagVal); } else { ret = 0; @@ -17427,6 +18474,12 @@ 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) + test_cose_sign1_ext_sign(); +#if defined(WOLFCOSE_SIGN_SIGN) && defined(WOLFCOSE_SIGN_VERIFY) + test_cose_sign_ext_sign_multi(); +#endif +#endif #if defined(SIZE_MAX) && (SIZE_MAX > 0xFFFFFFFFUL) test_cose_sign1_word32_overflow_guard(); #endif @@ -17436,6 +18489,24 @@ int test_cose(void) #ifdef WOLFCOSE_HAVE_ES512 test_cose_sign1_ecc("ES512", WOLFCOSE_ALG_ES512, WOLFCOSE_CRV_P521, 66); #endif +#endif + + /* Delegated per-family tests: guarded by their own algorithm only, so + * they still run in profiles that build without ES256. */ +#if defined(WOLFCOSE_EXT_SIGN) +#if defined(WOLFCOSE_HAVE_EDDSA) + test_cose_sign1_ext_sign_eddsa_capacity(); + test_cose_sign1_ext_sign_ed25519(); +#if defined(WOLFCOSE_SIGN_SIGN) && defined(WOLFCOSE_SIGN_VERIFY) + test_cose_sign_ext_sign_ed25519(); +#endif +#endif +#if defined(WOLFCOSE_HAVE_RSAPSS) && defined(WOLFSSL_KEY_GEN) + test_cose_sign1_ext_sign_rsapss(); +#endif +#if defined(WOLFCOSE_HAVE_MLDSA) + test_cose_sign1_ext_sign_mldsa(); +#endif #endif #ifdef WOLFCOSE_HAVE_EDDSA test_cose_sign1_eddsa(); @@ -17592,14 +18663,12 @@ int test_cose(void) /* Phase 1: Algorithm Combination Tests */ TEST_LOG("\n--- Algorithm Combination Tests ---\n"); -#ifdef WOLFCOSE_HAVE_ES256 #ifdef WOLFCOSE_HAVE_ES384 test_cose_sign1_es384(); #endif #ifdef WOLFCOSE_HAVE_ES512 test_cose_sign1_es512(); #endif -#endif #ifdef WOLFCOSE_HAVE_AESGCM test_cose_encrypt0_a192gcm(); #endif diff --git a/tools/wolfcose_tool.c b/tools/wolfcose_tool.c index 6e35def..f58ad81 100644 --- a/tools/wolfcose_tool.c +++ b/tools/wolfcose_tool.c @@ -242,6 +242,11 @@ static int tool_keygen(int32_t alg, const char* algStr, const char* outPath) uint8_t keyBuf[WOLFCOSE_TOOL_MAX_KEY]; size_t keyLen = 0; +#if !defined(WOLFCOSE_HAVE_EDDSA) && !defined(WOLFCOSE_HAVE_ED448) + /* Only the Ed448-vs-Ed25519 disambiguation reads this. */ + (void)algStr; +#endif + ret = wc_InitRng(&rng); if (ret != 0) { fprintf(stderr, "RNG init failed: %d\n", ret); @@ -436,6 +441,12 @@ static int tool_sign(const char* keyPath, int32_t alg, const char* algStr, WOLFCOSE_KEY coseKey; WC_RNG rng; +#if !defined(WOLFCOSE_HAVE_EDDSA) && !defined(WOLFCOSE_HAVE_ED448) + /* Only the Ed448-vs-Ed25519 disambiguation reads this. */ + (void)algStr; +#endif + + ret = read_file(keyPath, keyBuf, sizeof(keyBuf), &keyLen); if (ret != 0) return ret;