Skip to content

Commit 7cfc9e9

Browse files
authored
Merge pull request wolfSSL#10465 from Frauschi/slhdsa_pre_hash
SLH-DSA fixes
2 parents d7bdfd3 + bec6c0f commit 7cfc9e9

5 files changed

Lines changed: 256 additions & 28 deletions

File tree

ChangeLog.md

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,16 @@
1212
for SHAKE128, 64 bytes for SHAKE256 per FIPS 205 Section 10.2.2); otherwise
1313
`BAD_LENGTH_E` is returned. Migration: hash the message yourself before the
1414
call (callers using positional arguments are source-compatible; only the
15-
parameter names changed). The pre-existing `wc_SlhDsaKey_SignMsgDeterministic`
16-
and `wc_SlhDsaKey_SignMsgWithRandom` (FIPS 205 internal interface, M'
17-
supplied directly) are unaffected and gain stricter input validation
18-
matching the `*Hash*` family. `wc_SlhDsaKey_VerifyMsg` is unchanged. All
19-
three gain doxygen coverage.
15+
parameter names changed). Caveat: callers who today pass a raw message
16+
whose length happens to equal the digest size for the chosen `hashType`
17+
(e.g., signing a 32-byte handle/IV/seed with `WC_HASH_TYPE_SHA256`) will
18+
not trip `BAD_LENGTH_E`; the resulting signature is syntactically valid
19+
but is over the wrong bytes. The pre-existing
20+
`wc_SlhDsaKey_SignMsgDeterministic` and `wc_SlhDsaKey_SignMsgWithRandom`
21+
retain their M'-supplied-directly contract (FIPS 205 internal interface,
22+
Algorithm 19); their input validation is hardened with the same
23+
NULL/length/`MISSING_KEY` checks as the `*Hash*` family.
24+
`wc_SlhDsaKey_VerifyMsg` is unchanged. All three gain doxygen coverage.
2025

2126
* TLS 1.3: zero traffic key staging buffers in `SetKeysSide()` once a
2227
CryptoCB callback has imported the AES key into a Secure Element

doc/dox_comments/header_files/wc_slhdsa.h

Lines changed: 195 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -260,11 +260,15 @@ int wc_SlhDsaKey_Sign(SlhDsaKey* key, const byte* ctx,
260260
\ingroup SLH_DSA
261261
262262
\brief Verifies an SLH-DSA signature over a message using the external
263-
(pure) interface. This is FIPS 205 Algorithm 23. The message is wrapped
263+
(pure) interface. This is FIPS 205 Algorithm 24. The message is wrapped
264264
internally as M' = 0x00 || len(ctx) || ctx || M before verification.
265265
266266
\return 0 on success (signature valid).
267-
\return BAD_FUNC_ARG if key, msg, or sig is NULL.
267+
\return BAD_FUNC_ARG if key, msg, or sig is NULL, or ctx is NULL but
268+
ctxSz is greater than 0.
269+
\return BAD_LENGTH_E if sigSz does not match the parameter set's
270+
signature length.
271+
\return MISSING_KEY if the public key has not been set.
268272
\return SIG_VERIFY_E if the signature is invalid.
269273
270274
\param [in] key Pointer to a public SlhDsaKey.
@@ -397,6 +401,9 @@ int wc_SlhDsaKey_SignMsgWithRandom(SlhDsaKey* key,
397401
398402
\return 0 on success (signature valid).
399403
\return BAD_FUNC_ARG if key, mprime, or sig is NULL.
404+
\return BAD_LENGTH_E if sigSz does not match the parameter set's
405+
signature length.
406+
\return MISSING_KEY if the public key has not been set.
400407
\return SIG_VERIFY_E if the signature is invalid.
401408
402409
\param [in] key Pointer to a public SlhDsaKey.
@@ -513,7 +520,7 @@ int wc_SlhDsaKey_SignHashDeterministic(SlhDsaKey* key,
513520
*/
514521
int wc_SlhDsaKey_SignHashWithRandom(SlhDsaKey* key,
515522
const byte* ctx, byte ctxSz, const byte* hash, word32 hashSz,
516-
enum wc_HashType hashType, byte* sig, word32* sigSz, byte* addRnd);
523+
enum wc_HashType hashType, byte* sig, word32* sigSz, const byte* addRnd);
517524

518525
/*!
519526
\ingroup SLH_DSA
@@ -553,7 +560,7 @@ int wc_SlhDsaKey_SignHash(SlhDsaKey* key, const byte* ctx,
553560
\ingroup SLH_DSA
554561
555562
\brief Verifies an SLH-DSA signature using the external HashSLH-DSA
556-
interface (FIPS 205 Algorithm 24). The caller must hash the application
563+
interface (FIPS 205 Algorithm 25). The caller must hash the application
557564
message with hashType first and pass the digest as hash; this function
558565
does NOT hash its input.
559566
@@ -835,3 +842,187 @@ int wc_SlhDsaKey_PublicSizeFromParam(enum SlhDsaParam param);
835842
\sa wc_SlhDsaKey_SigSize
836843
*/
837844
int wc_SlhDsaKey_SigSizeFromParam(enum SlhDsaParam param);
845+
846+
/*!
847+
\ingroup SLH_DSA
848+
849+
\brief Decodes a DER-encoded SLH-DSA private key in the PKCS#8
850+
OneAsymmetricKey format defined by RFC 9909. The privateKey OCTET STRING
851+
contains the raw concatenation SK.seed || SK.prf || PK.seed || PK.root
852+
(4*n bytes) directly, without a nested OCTET STRING wrapper as used by
853+
Ed25519/Ed448. The SLH-DSA parameter set is detected from the
854+
AlgorithmIdentifier OID and key->params is updated to match. Available
855+
only when WOLFSSL_SLHDSA_VERIFY_ONLY is not defined.
856+
857+
On a failure that is detected before any write to key->sk
858+
(BAD_FUNC_ARG, header/OID parse errors, or wrong privateKey length), the
859+
key state is left untouched. On a failure detected after
860+
wc_SlhDsaKey_ImportPrivate has populated key->sk (a SHA-2 precompute
861+
error, or a trailing-field validation error), key->sk is scrubbed with
862+
ForceZero and the WC_SLHDSA_FLAG_PRIVATE/PUBLIC flags are cleared so
863+
flags can never claim valid bytes that were zeroed. In both rollback
864+
cases, key->params and inOutIdx are restored to their pre-call values.
865+
866+
\return 0 on success.
867+
\return BAD_FUNC_ARG if input, inOutIdx, or key is NULL, or inSz is 0.
868+
\return ASN_PARSE_E if the DER cannot be parsed as an SLH-DSA private
869+
key (malformed input, wrong key size, or trailing-field violation).
870+
\return NOT_COMPILED_IN if the OID names an SLH-DSA variant that is not
871+
built into this library.
872+
873+
\param [in] input DER-encoded key data.
874+
\param [in,out] inOutIdx On input, starting offset into input. On output,
875+
advanced past the parsed key (unchanged on failure).
876+
\param [in,out] key SLH-DSA key. Parameter set is auto-detected from the
877+
encoded OID.
878+
\param [in] inSz Total size of input in bytes.
879+
880+
\sa wc_SlhDsaKey_KeyToDer
881+
\sa wc_SlhDsaKey_PublicKeyDecode
882+
\sa wc_SlhDsaKey_ImportPrivate
883+
*/
884+
int wc_SlhDsaKey_PrivateKeyDecode(const byte* input, word32* inOutIdx,
885+
SlhDsaKey* key, word32 inSz);
886+
887+
/*!
888+
\ingroup SLH_DSA
889+
890+
\brief Decodes a DER-encoded SLH-DSA public key in the
891+
SubjectPublicKeyInfo (SPKI) format. The SLH-DSA parameter set is
892+
detected from the AlgorithmIdentifier OID and key->params is updated
893+
accordingly.
894+
895+
As a fast path, if key->params is already set the function first hands
896+
the entire window from inOutIdx to inSz to wc_SlhDsaKey_ImportPublic.
897+
ImportPublic's length check is the disambiguator: a window of exactly
898+
2*n bytes is accepted as a raw public key (PK.seed || PK.root) and
899+
consumed in full; any other length is rejected and the function falls
900+
through to SPKI parsing. SPKI input always carries enough
901+
AlgorithmIdentifier/BIT STRING overhead that it never collides with the
902+
2*n raw length, so it falls through cleanly. The caller does not need
903+
to pre-trim the window to 2*n.
904+
905+
On a failure detected before any write (BAD_FUNC_ARG or a malformed
906+
SPKI), the key state is left untouched. On a failure detected after
907+
ImportPublic has populated the public half of key->sk (a SHA-2
908+
precompute error), the public half sk[2*n .. 4*n] is scrubbed and
909+
WC_SLHDSA_FLAG_PUBLIC is cleared from the flags; the private half is
910+
left intact in case the caller imported it earlier. key->params and
911+
inOutIdx are restored to their pre-call values.
912+
913+
\return 0 on success.
914+
\return BAD_FUNC_ARG if input, inOutIdx, or key is NULL, or inSz is 0.
915+
\return ASN_PARSE_E if the DER cannot be parsed as an SLH-DSA public
916+
key.
917+
\return NOT_COMPILED_IN if the OID names an SLH-DSA variant that is not
918+
built into this library.
919+
920+
\param [in] input DER-encoded key data, or a raw 2*n public key when
921+
key->params is already set.
922+
\param [in,out] inOutIdx On input, starting offset into input. On output,
923+
advanced past the parsed key (unchanged on failure).
924+
\param [in,out] key SLH-DSA key. Parameter set is auto-detected from the
925+
encoded OID, or honored as-is in the raw fast path.
926+
\param [in] inSz Total size of input in bytes.
927+
928+
\sa wc_SlhDsaKey_PublicKeyToDer
929+
\sa wc_SlhDsaKey_PrivateKeyDecode
930+
\sa wc_SlhDsaKey_ImportPublic
931+
*/
932+
int wc_SlhDsaKey_PublicKeyDecode(const byte* input, word32* inOutIdx,
933+
SlhDsaKey* key, word32 inSz);
934+
935+
/*!
936+
\ingroup SLH_DSA
937+
938+
\brief Encodes an SLH-DSA private key to DER in the PKCS#8
939+
OneAsymmetricKey format defined by RFC 9909. The privateKey OCTET STRING
940+
contains the raw 4*n bytes (SK.seed || SK.prf || PK.seed || PK.root)
941+
directly, without the nested OCTET STRING wrapping used by Ed25519/Ed448.
942+
943+
Available only when WOLFSSL_SLHDSA_VERIFY_ONLY is not defined and
944+
WC_ENABLE_ASYM_KEY_EXPORT is set.
945+
946+
\return Size of the encoded DER in bytes on success. Pass NULL as output
947+
to query the required buffer size without writing.
948+
\return BAD_FUNC_ARG if key or key->params is NULL.
949+
\return MISSING_KEY if the private key has not been set.
950+
\return BUFFER_E if output is non-NULL and inLen is smaller than the
951+
required size.
952+
\return NOT_COMPILED_IN if key->params names an SLH-DSA variant whose
953+
parameter set is not built in.
954+
955+
\param [in] key SLH-DSA key with a populated private key.
956+
\param [out] output Buffer to receive the DER encoding, or NULL to query
957+
the required size.
958+
\param [in] inLen Size of output in bytes (ignored when output is NULL).
959+
960+
\sa wc_SlhDsaKey_PrivateKeyDecode
961+
\sa wc_SlhDsaKey_PrivateKeyToDer
962+
\sa wc_SlhDsaKey_PublicKeyToDer
963+
*/
964+
int wc_SlhDsaKey_KeyToDer(SlhDsaKey* key, byte* output, word32 inLen);
965+
966+
/*!
967+
\ingroup SLH_DSA
968+
969+
\brief Encodes an SLH-DSA private key to DER. RFC 9909 packs
970+
SK.seed || SK.prf || PK.seed || PK.root into a single OCTET STRING, so
971+
SLH-DSA has no distinct private-only encoding. This function is an
972+
intentional alias of wc_SlhDsaKey_KeyToDer, kept for API parity with
973+
Ed25519/Ed448 which do have a separate private form.
974+
975+
Available only when WOLFSSL_SLHDSA_VERIFY_ONLY is not defined and
976+
WC_ENABLE_ASYM_KEY_EXPORT is set.
977+
978+
Return codes are inherited unchanged from wc_SlhDsaKey_KeyToDer.
979+
980+
\return Size of the encoded DER in bytes on success. Pass NULL as output
981+
to query the required buffer size.
982+
\return BAD_FUNC_ARG if key or key->params is NULL.
983+
\return MISSING_KEY if the private key has not been set.
984+
\return BUFFER_E if output is non-NULL and inLen is smaller than the
985+
required size.
986+
\return NOT_COMPILED_IN if key->params names an SLH-DSA variant whose
987+
parameter set is not built in.
988+
989+
\param [in] key SLH-DSA key with a populated private key.
990+
\param [out] output Buffer to receive the DER encoding, or NULL to query
991+
the required size.
992+
\param [in] inLen Size of output in bytes (ignored when output is NULL).
993+
994+
\sa wc_SlhDsaKey_KeyToDer
995+
\sa wc_SlhDsaKey_PrivateKeyDecode
996+
*/
997+
int wc_SlhDsaKey_PrivateKeyToDer(SlhDsaKey* key, byte* output, word32 inLen);
998+
999+
/*!
1000+
\ingroup SLH_DSA
1001+
1002+
\brief Encodes an SLH-DSA public key to DER. When withAlg is non-zero
1003+
the output is a full SubjectPublicKeyInfo structure (AlgorithmIdentifier
1004+
plus BIT STRING). When withAlg is zero the output contains the raw
1005+
public key bytes without the SPKI wrapping.
1006+
1007+
Available only when WC_ENABLE_ASYM_KEY_EXPORT is set.
1008+
1009+
\return Size of the encoded DER in bytes on success. Pass NULL as output
1010+
to query the required buffer size.
1011+
\return BAD_FUNC_ARG if key or key->params is NULL.
1012+
\return BUFFER_E if output is non-NULL and inLen is smaller than the
1013+
required size.
1014+
\return NOT_COMPILED_IN if key->params names an SLH-DSA variant whose
1015+
parameter set is not built in.
1016+
1017+
\param [in] key SLH-DSA key with a populated public key.
1018+
\param [out] output Buffer to receive the DER encoding, or NULL to query
1019+
the required size.
1020+
\param [in] inLen Size of output in bytes (ignored when output is NULL).
1021+
\param [in] withAlg Non-zero to emit SubjectPublicKeyInfo (with
1022+
AlgorithmIdentifier); zero to emit the raw public key only.
1023+
1024+
\sa wc_SlhDsaKey_PublicKeyDecode
1025+
\sa wc_SlhDsaKey_KeyToDer
1026+
*/
1027+
int wc_SlhDsaKey_PublicKeyToDer(SlhDsaKey* key, byte* output, word32 inLen,
1028+
int withAlg);

tests/api/test_slhdsa.c

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1151,16 +1151,36 @@ int test_wc_slhdsa_sign_hash(void)
11511151
WC_NO_ERR_TRACE(BAD_LENGTH_E));
11521152
#endif
11531153

1154+
#ifdef WOLFSSL_SHAKE128
1155+
/* SHAKE128 PHM is fixed at 256 bits per FIPS 205 Section 10.2.2. */
1156+
sigLen = WC_SLHDSA_MAX_SIG_LEN;
1157+
ExpectIntEQ(wc_SlhDsaKey_SignHash(&key, ctx, sizeof(ctx), hash, 32,
1158+
WC_HASH_TYPE_SHAKE128, sig, &sigLen, &rng), 0);
1159+
ExpectIntEQ(wc_SlhDsaKey_VerifyHash(&key, ctx, sizeof(ctx), hash, 32,
1160+
WC_HASH_TYPE_SHAKE128, sig, sigLen), 0);
1161+
/* SignHash and VerifyHash both reject mismatched digest length. */
1162+
ExpectIntEQ(wc_SlhDsaKey_SignHash(&key, ctx, sizeof(ctx), hash, 64,
1163+
WC_HASH_TYPE_SHAKE128, sig, &sigLen, &rng),
1164+
WC_NO_ERR_TRACE(BAD_LENGTH_E));
1165+
ExpectIntEQ(wc_SlhDsaKey_VerifyHash(&key, ctx, sizeof(ctx), hash, 64,
1166+
WC_HASH_TYPE_SHAKE128, sig, sigLen),
1167+
WC_NO_ERR_TRACE(BAD_LENGTH_E));
1168+
#endif
1169+
11541170
#ifdef WOLFSSL_SHAKE256
11551171
/* SHAKE256 PHM is fixed at 512 bits per FIPS 205 Section 10.2.2. */
11561172
sigLen = WC_SLHDSA_MAX_SIG_LEN;
11571173
ExpectIntEQ(wc_SlhDsaKey_SignHash(&key, ctx, sizeof(ctx), hash, 64,
11581174
WC_HASH_TYPE_SHAKE256, sig, &sigLen, &rng), 0);
11591175
ExpectIntEQ(wc_SlhDsaKey_VerifyHash(&key, ctx, sizeof(ctx), hash, 64,
11601176
WC_HASH_TYPE_SHAKE256, sig, sigLen), 0);
1177+
/* SignHash and VerifyHash both reject mismatched digest length. */
11611178
ExpectIntEQ(wc_SlhDsaKey_SignHash(&key, ctx, sizeof(ctx), hash, 32,
11621179
WC_HASH_TYPE_SHAKE256, sig, &sigLen, &rng),
11631180
WC_NO_ERR_TRACE(BAD_LENGTH_E));
1181+
ExpectIntEQ(wc_SlhDsaKey_VerifyHash(&key, ctx, sizeof(ctx), hash, 32,
1182+
WC_HASH_TYPE_SHAKE256, sig, sigLen),
1183+
WC_NO_ERR_TRACE(BAD_LENGTH_E));
11641184
#endif
11651185

11661186
wc_SlhDsaKey_Free(&key);

0 commit comments

Comments
 (0)