From 6a2ed8ec8f071083bb561d26d447a3dea1e9ddc0 Mon Sep 17 00:00:00 2001 From: Paul Adelsbach Date: Tue, 6 Oct 2026 09:35:03 -0700 Subject: [PATCH 1/2] Add crypto callback for wc_falcon_check_key --- doc/dox_comments/header_files/falcon.h | 20 ++-- wolfcrypt/src/falcon.c | 32 +++++- wolfcrypt/test/test.c | 137 ++++++++++++++++++++++++- 3 files changed, 177 insertions(+), 12 deletions(-) diff --git a/doc/dox_comments/header_files/falcon.h b/doc/dox_comments/header_files/falcon.h index 08618947220..0f8a4724021 100644 --- a/doc/dox_comments/header_files/falcon.h +++ b/doc/dox_comments/header_files/falcon.h @@ -423,16 +423,22 @@ int wc_falcon_export_key(falcon_key* key, byte* priv, word32 *privSz, /*! \ingroup Falcon - \brief Checks the consistency of a Falcon key. Requires both key halves to - be present. When the native signing core is compiled in, the stored public - key h is additionally verified against the private key by checking the - defining relation h*f == g (mod q); in verify-only or crypto-callback-only - builds only the presence of both halves is checked. + \brief Checks the consistency of a Falcon key. With WOLF_CRYPTO_CB, a key + with a device id (or any key with WOLF_CRYPTO_CB_FIND) is first passed to + the crypto callback, along with the public key when it is set. The device + checks the key it contains, so neither key needs to be set locally. + Otherwise, or when the callback returns CRYPTOCB_UNAVAILABLE, both the + public and private keys must be present. When the native signing core is + compiled in, the stored public key h is additionally verified against the + private key by checking the defining relation h*f == g (mod q); in + verify-only or crypto-callback-only builds only the presence of both keys + is checked. \return 0 on success. \return BAD_FUNC_ARG if key is NULL or the level is unset. - \return PUBLIC_KEY_E if either key half is missing, or if the public and - private keys are cryptographically inconsistent. + \return PUBLIC_KEY_E if the public or private key is missing, or if the + public and private keys are cryptographically inconsistent. + \return Other negative values returned by the crypto callback. \param [in] key Pointer to a falcon_key to check. diff --git a/wolfcrypt/src/falcon.c b/wolfcrypt/src/falcon.c index bfef4d274ab..ff766786b6b 100644 --- a/wolfcrypt/src/falcon.c +++ b/wolfcrypt/src/falcon.c @@ -11329,16 +11329,16 @@ int wc_falcon_export_key(falcon_key* key, byte* priv, word32 *privSz, * * key [in] Falcon private/public key. * returns BAD_FUNC_ARG when key is NULL or the level is unset, - * PUBLIC_KEY_E when either half is not set, or when the stored public + * PUBLIC_KEY_E when either key is not set, or when the stored public * key h does not satisfy the defining relation h = g/f (mod q) for the - * private (f, g), + * private (f, g), the crypto callback result for a device backed key, * 0 otherwise. * - * When the native signing core is compiled in, both halves are decoded and the + * When the native signing core is compiled in, both keys are decoded and the * relation h*f == g (mod q, mod X^n + 1) is verified in the NTT domain, so a * mismatched pair is detected cryptographically. In verify-only or * callback-only builds (no private-key codec available) only the presence of - * both halves is checked. The pre-native implementation compared the stored + * both keys is checked. The pre-native implementation compared the stored * public key against a duplicate copy kept behind the private key, which was * always a copy of the same bytes and so could never detect a mismatch. */ int wc_falcon_check_key(falcon_key* key) @@ -11351,6 +11351,30 @@ int wc_falcon_check_key(falcon_key* key) return BAD_FUNC_ARG; } +#ifdef WOLF_CRYPTO_CB + /* Before prvKeySet check: a device backed key has no local private key. */ + #ifndef WOLF_CRYPTO_CB_FIND + if (key->devId != INVALID_DEVID) + #endif + { + const byte* pub = NULL; + word32 pubSz = 0; + int cbRet; + + /* Without a public key, the device checks the key it contains. */ + if (key->pubKeySet) { + pub = key->p; + pubSz = (word32)wc_falcon_pub_size(key); + } + + cbRet = wc_CryptoCb_PqcSignatureCheckPrivKey(key, + WC_PQC_SIG_TYPE_FALCON, pub, pubSz); + if (cbRet != WC_NO_ERR_TRACE(CRYPTOCB_UNAVAILABLE)) + return cbRet; + /* fall-through when unavailable */ + } +#endif /* WOLF_CRYPTO_CB */ + if (!key->pubKeySet || !key->prvKeySet) { return PUBLIC_KEY_E; } diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index f545745e18d..1b33dd69862 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -89216,6 +89216,10 @@ typedef struct { int mldsaSignHashCount; /* ML-DSA pre-hash sign invocations */ int mldsaVerifyHashCount; /* ML-DSA pre-hash verify invocations */ #endif +#ifdef HAVE_FALCON + int falconCheckRet; /* key check callback return */ + word32 falconCheckPubSz; /* public key length passed to callback */ +#endif } myCryptoDevCtx; #ifdef WOLF_CRYPTO_CB_ONLY_RSA @@ -91498,6 +91502,15 @@ static int myCryptoDevCb(int devIdArg, wc_CryptoInfo* info, void* ctx) myCtx->exampleVar++; } #endif /* HAVE_FALCON && !WOLF_CRYPTO_CB_ONLY_FALCON */ + #ifdef HAVE_FALCON + /* Software check runs in the caller when this is unavailable. */ + if (info->pk.type == WC_PK_TYPE_PQC_SIG_CHECK_PRIV_KEY && + info->pk.pqc_sig_check.type == WC_PQC_SIG_TYPE_FALCON) { + myCtx->falconCheckPubSz = info->pk.pqc_sig_check.pubKeySz; + ret = myCtx->falconCheckRet; + myCtx->exampleVar++; + } + #endif /* HAVE_FALCON */ #if defined(WOLFSSL_HAVE_MLDSA) && defined(WC_MLDSA_HAVE_NATIVE) #ifndef WOLFSSL_MLDSA_NO_MAKE_KEY if ((info->pk.type == WC_PK_TYPE_PQC_SIG_KEYGEN) && @@ -95344,6 +95357,10 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t cryptocb_test(void) myCtx.mldsaSignHashCount = 0; myCtx.mldsaVerifyHashCount = 0; #endif +#ifdef HAVE_FALCON + myCtx.falconCheckRet = WC_NO_ERR_TRACE(CRYPTOCB_UNAVAILABLE); + myCtx.falconCheckPubSz = 0; +#endif /* set devId to something other than INVALID_DEVID */ devId = 1; @@ -96271,7 +96288,125 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t cryptocb_test(void) ret = WC_TEST_RET_ENC_NC; myCtx.exampleVar = baseline; } -#endif +#endif /* HAVE_FALCON && !WOLF_CRYPTO_CB_ONLY_FALCON */ +#ifdef HAVE_FALCON + /* wc_falcon_check_key() dispatches to callback and returns its result */ + if (ret == 0) { + WC_DECLARE_VAR(key, falcon_key, 1, HEAP_HINT); + int key_inited = 0; + int baseline = myCtx.exampleVar; + int prev = 0; + int r; + + WC_ALLOC_VAR(key, falcon_key, 1, HEAP_HINT); + if (!WC_VAR_OK(key)) + ret = WC_TEST_RET_ENC_EC(MEMORY_E); + if (ret == 0) { + r = wc_falcon_init_ex(key, HEAP_HINT, devId); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + else + key_inited = 1; + } + if (ret == 0) { + #ifndef WOLFSSL_NO_FALCON_LEVEL1 + r = wc_falcon_set_level(key, FALCON_LEVEL1); + #else + r = wc_falcon_set_level(key, FALCON_LEVEL5); + #endif + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + } + /* Key only in the device, so no public key is passed */ + if (ret == 0) { + myCtx.falconCheckRet = 0; + myCtx.falconCheckPubSz = 1; + prev = myCtx.exampleVar; + r = wc_falcon_check_key(key); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + else if (myCtx.exampleVar == prev) + ret = WC_TEST_RET_ENC_NC; /* never reached the callback */ + else if (myCtx.falconCheckPubSz != 0) + ret = WC_TEST_RET_ENC_NC; + } + /* Bad arguments rejected before dispatch */ + if (ret == 0) { + byte savedLevel = key->level; + + if (wc_falcon_check_key(NULL) != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) + ret = WC_TEST_RET_ENC_NC; + key->level = 3; /* not a Falcon parameter set */ + if ((ret == 0) && + (wc_falcon_check_key(key) != WC_NO_ERR_TRACE(BAD_FUNC_ARG))) + ret = WC_TEST_RET_ENC_NC; + key->level = savedLevel; + } + /* Device error is returned as-is; software never returns WC_HW_E */ + if (ret == 0) { + myCtx.falconCheckRet = WC_NO_ERR_TRACE(WC_HW_E); + prev = myCtx.exampleVar; + r = wc_falcon_check_key(key); + if (r != WC_NO_ERR_TRACE(WC_HW_E)) + ret = WC_TEST_RET_ENC_NC; + else if (myCtx.exampleVar == prev) + ret = WC_TEST_RET_ENC_NC; + } + /* Unavailable falls back to software, which has no local key */ + if (ret == 0) { + myCtx.falconCheckRet = WC_NO_ERR_TRACE(CRYPTOCB_UNAVAILABLE); + prev = myCtx.exampleVar; + r = wc_falcon_check_key(key); + if (r != WC_NO_ERR_TRACE(PUBLIC_KEY_E)) + ret = WC_TEST_RET_ENC_NC; + else if (myCtx.exampleVar == prev) + ret = WC_TEST_RET_ENC_NC; + } + #if !defined(WOLFSSL_FALCON_VERIFY_ONLY) && \ + !defined(WOLF_CRYPTO_CB_ONLY_FALCON) + /* Local key: public key is passed, then software check on fallback */ + if (ret == 0) { + WC_DECLARE_VAR(falconRng, WC_RNG, 1, HEAP_HINT); + int rng_inited = 0; + + WC_ALLOC_VAR(falconRng, WC_RNG, 1, HEAP_HINT); + if (!WC_VAR_OK(falconRng)) + ret = WC_TEST_RET_ENC_EC(MEMORY_E); + if (ret == 0) { + r = wc_InitRng_ex(falconRng, HEAP_HINT, INVALID_DEVID); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + else + rng_inited = 1; + } + if (ret == 0) { + r = wc_falcon_make_key(key, falconRng); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + } + if (ret == 0) { + prev = myCtx.exampleVar; + r = wc_falcon_check_key(key); + if (r != 0) + ret = WC_TEST_RET_ENC_EC(r); + else if (myCtx.exampleVar == prev) + ret = WC_TEST_RET_ENC_NC; + else if (myCtx.falconCheckPubSz != + (word32)wc_falcon_pub_size(key)) + ret = WC_TEST_RET_ENC_NC; /* public key was not passed */ + } + if (rng_inited) + wc_FreeRng(falconRng); + WC_FREE_VAR(falconRng, HEAP_HINT); + } + #endif /* !WOLFSSL_FALCON_VERIFY_ONLY && !WOLF_CRYPTO_CB_ONLY_FALCON */ + myCtx.falconCheckRet = WC_NO_ERR_TRACE(CRYPTOCB_UNAVAILABLE); + if (key_inited) + wc_falcon_free(key); + WC_FREE_VAR(key, HEAP_HINT); + myCtx.exampleVar = baseline; + } +#endif /* HAVE_FALCON */ #if defined(WOLFSSL_HAVE_XMSS) && !defined(WOLFSSL_XMSS_VERIFY_ONLY) if (ret == 0) ret = xmss_test(); From 945415cffb6763c7e08b62e1a8265eebeefd8348 Mon Sep 17 00:00:00 2001 From: Paul Adelsbach Date: Thu, 8 Oct 2026 09:27:51 -0700 Subject: [PATCH 2/2] PR feedback: check level prior to callback, update docs --- doc/dox_comments/header_files/falcon.h | 3 ++- tests/api/test_falcon.c | 1 + wolfcrypt/src/falcon.c | 9 +++------ wolfcrypt/test/test.c | 13 +++++++++++++ 4 files changed, 19 insertions(+), 7 deletions(-) diff --git a/doc/dox_comments/header_files/falcon.h b/doc/dox_comments/header_files/falcon.h index 0f8a4724021..20bd93c442e 100644 --- a/doc/dox_comments/header_files/falcon.h +++ b/doc/dox_comments/header_files/falcon.h @@ -435,7 +435,8 @@ int wc_falcon_export_key(falcon_key* key, byte* priv, word32 *privSz, is checked. \return 0 on success. - \return BAD_FUNC_ARG if key is NULL or the level is unset. + \return BAD_FUNC_ARG if key is NULL, the level is unset, or the level was + changed without wc_falcon_set_level() to one the key buffers do not fit. \return PUBLIC_KEY_E if the public or private key is missing, or if the public and private keys are cryptographically inconsistent. \return Other negative values returned by the crypto callback. diff --git a/tests/api/test_falcon.c b/tests/api/test_falcon.c index 93f79e4c4ab..b545d847905 100644 --- a/tests/api/test_falcon.c +++ b/tests/api/test_falcon.c @@ -1199,6 +1199,7 @@ int test_wc_falcon_level_overwrite(void) WC_NO_ERR_TRACE(BAD_FUNC_ARG)); ExpectIntEQ(wc_Falcon_PrivateKeyToDer(&key, NULL, 0), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); + ExpectIntEQ(wc_falcon_check_key(&key), WC_NO_ERR_TRACE(BAD_FUNC_ARG)); #else (void)bufLen; #endif diff --git a/wolfcrypt/src/falcon.c b/wolfcrypt/src/falcon.c index ff766786b6b..e572dcb10dd 100644 --- a/wolfcrypt/src/falcon.c +++ b/wolfcrypt/src/falcon.c @@ -11328,7 +11328,8 @@ int wc_falcon_export_key(falcon_key* key, byte* priv, word32 *privSz, /* Check that the falcon key has a matching private/public key pair present. * * key [in] Falcon private/public key. - * returns BAD_FUNC_ARG when key is NULL or the level is unset, + * returns BAD_FUNC_ARG when key is NULL, the level is unset, or the level + * does not match the key buffers, * PUBLIC_KEY_E when either key is not set, or when the stored public * key h does not satisfy the defining relation h = g/f (mod q) for the * private (f, g), the crypto callback result for a device backed key, @@ -11343,11 +11344,7 @@ int wc_falcon_export_key(falcon_key* key, byte* priv, word32 *privSz, * always a copy of the same bytes and so could never detect a mismatch. */ int wc_falcon_check_key(falcon_key* key) { - if (key == NULL) { - return BAD_FUNC_ARG; - } - - if ((key->level != 1) && (key->level != 5)) { + if ((key == NULL) || !falcon_level_ok(key)) { return BAD_FUNC_ARG; } diff --git a/wolfcrypt/test/test.c b/wolfcrypt/test/test.c index 1b33dd69862..5a83753d594 100644 --- a/wolfcrypt/test/test.c +++ b/wolfcrypt/test/test.c @@ -96334,13 +96334,26 @@ WOLFSSL_TEST_SUBROUTINE wc_test_ret_t cryptocb_test(void) if (ret == 0) { byte savedLevel = key->level; + prev = myCtx.exampleVar; if (wc_falcon_check_key(NULL) != WC_NO_ERR_TRACE(BAD_FUNC_ARG)) ret = WC_TEST_RET_ENC_NC; key->level = 3; /* not a Falcon parameter set */ if ((ret == 0) && (wc_falcon_check_key(key) != WC_NO_ERR_TRACE(BAD_FUNC_ARG))) ret = WC_TEST_RET_ENC_NC; + #if defined(WOLFSSL_FALCON_DYNAMIC_KEYS) || \ + defined(WOLFSSL_NO_FALCON_LEVEL1) || \ + defined(WOLFSSL_NO_FALCON_LEVEL5) + /* Key buffers only fit the level given to wc_falcon_set_level */ + key->level = (savedLevel == FALCON_LEVEL1) ? FALCON_LEVEL5 : + FALCON_LEVEL1; + if ((ret == 0) && + (wc_falcon_check_key(key) != WC_NO_ERR_TRACE(BAD_FUNC_ARG))) + ret = WC_TEST_RET_ENC_NC; + #endif key->level = savedLevel; + if ((ret == 0) && (myCtx.exampleVar != prev)) + ret = WC_TEST_RET_ENC_NC; /* reached the callback */ } /* Device error is returned as-is; software never returns WC_HW_E */ if (ret == 0) {