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;