Skip to content

Commit 293c540

Browse files
committed
Fix SLH-DSA DER encoding to match RFC 9909 / OpenSSL 3.5
RFC 9909 Section 6: The privateKey field in OneAsymmetricKey contains the raw key bytes (SK.seed || SK.prf || PK.seed || PK.root) directly in the OCTET STRING, without a nested OCTET STRING wrapper. This differs from Ed25519/Ed448 which wrap the key in an additional OCTET STRING inside the privateKey field. The previous implementation used DecodeAsymKey/SetAsymKeyDer which add the nested wrapping, making wolfssl-generated keys incompatible with OpenSSL 3.5+ and vice versa. PrivateKeyDecode: Parse PKCS#8 OneAsymmetricKey directly using GetSequence/GetMyVersion/GetAlgoId/GetOctetString, mapping the AlgorithmIdentifier OID to the correct SlhDsaParam. KeyToDer/PrivateKeyToDer: Build PKCS#8 directly using SetSequence/SetMyVersion/SetAlgoID/SetOctetString with a single (not nested) OCTET STRING for the raw key. PublicKeyDecode/PublicKeyToDer: Keep using DecodeAsymKeyPublic/ SetAsymKeyDerPublic since SubjectPublicKeyInfo uses BIT STRING (not OCTET STRING) and the public key format is the same. Also fix MIN_SLHDSAKEY_SZ: The key size check in ProcessBufferTryDecodeSlhDsa compares the public key length against minSlhDsaKeySz. The smallest public key is 32 bytes (SHAKE-128), so MIN_SLHDSAKEY_SZ must be 32 (not 64). Tested: OpenSSL 3.5.6-generated SLH-DSA-SHAKE-128f private keys now load successfully via wolfSSL_CTX_use_PrivateKey_file. https://claude.ai/code/session_019gqvW3ZMKGGyi6zCRNPDYV
1 parent 4f24d44 commit 293c540

2 files changed

Lines changed: 172 additions & 85 deletions

File tree

wolfcrypt/src/wc_slhdsa.c

Lines changed: 171 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -7352,132 +7352,164 @@ static int slhdsa_param_to_keytype(enum SlhDsaParam param)
73527352
}
73537353
}
73547354

7355-
/* Decode a DER-encoded SLH-DSA private key.
7355+
/* Map OID key type back to SlhDsaParam. */
7356+
static int slhdsa_keytype_to_param(int keytype)
7357+
{
7358+
switch (keytype) {
7359+
#ifndef WOLFSSL_SLHDSA_NO_128
7360+
#ifndef WOLFSSL_SLHDSA_NO_SMALL
7361+
case SLH_DSA_SHAKE_128Sk: return SLHDSA_SHAKE128S;
7362+
#endif
7363+
#ifndef WOLFSSL_SLHDSA_NO_FAST
7364+
case SLH_DSA_SHAKE_128Fk: return SLHDSA_SHAKE128F;
7365+
#endif
7366+
#endif
7367+
#ifndef WOLFSSL_SLHDSA_NO_192
7368+
#ifndef WOLFSSL_SLHDSA_NO_SMALL
7369+
case SLH_DSA_SHAKE_192Sk: return SLHDSA_SHAKE192S;
7370+
#endif
7371+
#ifndef WOLFSSL_SLHDSA_NO_FAST
7372+
case SLH_DSA_SHAKE_192Fk: return SLHDSA_SHAKE192F;
7373+
#endif
7374+
#endif
7375+
#ifndef WOLFSSL_SLHDSA_NO_256
7376+
#ifndef WOLFSSL_SLHDSA_NO_SMALL
7377+
case SLH_DSA_SHAKE_256Sk: return SLHDSA_SHAKE256S;
7378+
#endif
7379+
#ifndef WOLFSSL_SLHDSA_NO_FAST
7380+
case SLH_DSA_SHAKE_256Fk: return SLHDSA_SHAKE256F;
7381+
#endif
7382+
#endif
7383+
default:
7384+
return BAD_FUNC_ARG;
7385+
}
7386+
}
7387+
7388+
/* Find SlhDsaParameters entry for a given param enum. */
7389+
static const SlhDsaParameters* slhdsa_find_params(enum SlhDsaParam param)
7390+
{
7391+
int i;
7392+
for (i = 0; i < SLHDSA_PARAM_LEN; i++) {
7393+
if (SlhDsaParams[i].param == param) {
7394+
return &SlhDsaParams[i];
7395+
}
7396+
}
7397+
return NULL;
7398+
}
7399+
7400+
/* Decode a DER-encoded SLH-DSA private key (PKCS#8 / OneAsymmetricKey).
7401+
*
7402+
* RFC 9909 Section 6: The privateKey OCTET STRING contains the raw
7403+
* concatenation SK.seed || SK.prf || PK.seed || PK.root (4*n bytes)
7404+
* directly, without a nested OCTET STRING wrapper. This differs from
7405+
* Ed25519/Ed448 which wrap the key in an additional OCTET STRING.
73567406
*
7357-
* The parameter set is detected from the OID in the DER encoding. On
7358-
* success, key->params is updated to match the detected parameter set.
7359-
* The key must be initialized (via wc_SlhDsaKey_Init) before calling this
7360-
* function; the initial parameter is used only as a placeholder.
7407+
* The parameter set is detected from the AlgorithmIdentifier OID.
7408+
* On success, key->params is updated to match the detected parameter set.
73617409
*
73627410
* @param [in] input DER-encoded key data.
73637411
* @param [in, out] inOutIdx Index into input, updated on return.
73647412
* @param [in, out] key SLH-DSA key. Parameter set is auto-detected.
73657413
* @param [in] inSz Size of input in bytes.
73667414
* @return 0 on success.
73677415
* @return BAD_FUNC_ARG when input, inOutIdx, or key is NULL.
7368-
* @return ASN_PARSE_E when OID does not match any supported SLH-DSA param.
7416+
* @return ASN_PARSE_E when the DER cannot be parsed as an SLH-DSA key.
73697417
*/
73707418
int wc_SlhDsaKey_PrivateKeyDecode(const byte* input, word32* inOutIdx,
73717419
SlhDsaKey* key, word32 inSz)
73727420
{
7373-
int ret = ASN_PARSE_E;
7374-
byte privKey[WC_SLHDSA_MAX_PRIV_LEN];
7375-
byte pubKey[WC_SLHDSA_MAX_PUB_LEN];
7376-
word32 privKeyLen = 0;
7377-
word32 pubKeyLen = 0;
7378-
word32 savedIdx;
7379-
int i;
7380-
const SlhDsaParameters* matched = NULL;
7421+
int ret = 0;
7422+
int length;
7423+
int version;
7424+
word32 oid = 0;
7425+
int privSz;
7426+
int paramId;
7427+
const SlhDsaParameters* params;
73817428

73827429
if ((input == NULL) || (inOutIdx == NULL) || (key == NULL) || (inSz == 0)) {
73837430
return BAD_FUNC_ARG;
73847431
}
73857432

7386-
savedIdx = *inOutIdx;
7433+
/* Parse PKCS#8 OneAsymmetricKey wrapper:
7434+
* SEQUENCE { version, AlgorithmIdentifier { OID }, OCTET STRING { key } }
7435+
*/
7436+
if (GetSequence(input, inOutIdx, &length, inSz) < 0) {
7437+
return ASN_PARSE_E;
7438+
}
73877439

7388-
/* Try each compiled-in parameter set until one matches the OID in the
7389-
* DER encoding. DecodeAsymKey validates the OID against the requested
7390-
* keytype, so a mismatch returns an error without consuming input. */
7391-
for (i = 0; i < SLHDSA_PARAM_LEN; i++) {
7392-
int keytype = slhdsa_param_to_keytype(SlhDsaParams[i].param);
7393-
word32 idx = savedIdx;
7394-
word32 pkLen = (word32)sizeof(privKey);
7395-
word32 pbLen = (word32)sizeof(pubKey);
7440+
if (GetMyVersion(input, inOutIdx, &version, inSz) < 0) {
7441+
return ASN_PARSE_E;
7442+
}
7443+
if (version != 0) {
7444+
return ASN_PARSE_E;
7445+
}
73967446

7397-
if (keytype < 0) {
7398-
continue;
7399-
}
7447+
if (GetAlgoId(input, inOutIdx, &oid, oidKeyType, inSz) < 0) {
7448+
return ASN_PARSE_E;
7449+
}
74007450

7401-
ret = DecodeAsymKey(input, &idx, inSz, privKey, &pkLen,
7402-
pubKey, &pbLen, keytype);
7403-
if (ret == 0) {
7404-
/* OID matched - record detected parameter set. */
7405-
matched = &SlhDsaParams[i];
7406-
*inOutIdx = idx;
7407-
privKeyLen = pkLen;
7408-
pubKeyLen = pbLen;
7409-
break;
7410-
}
7451+
/* Map the OID to an SLH-DSA parameter set. */
7452+
paramId = slhdsa_keytype_to_param((int)oid);
7453+
if (paramId < 0) {
7454+
return ASN_PARSE_E;
7455+
}
7456+
params = slhdsa_find_params((enum SlhDsaParam)paramId);
7457+
if (params == NULL) {
7458+
return ASN_PARSE_E;
74117459
}
74127460

7413-
if (ret == 0 && matched != NULL) {
7414-
/* Point the key at the detected parameter set so subsequent import
7415-
* and size queries use the right values. */
7416-
key->params = matched;
7461+
/* RFC 9909: privateKey is a bare OCTET STRING containing the raw key
7462+
* (4*n bytes). No nested OCTET STRING wrapping. */
7463+
if (GetOctetString(input, inOutIdx, &privSz, inSz) < 0) {
7464+
return ASN_PARSE_E;
7465+
}
74177466

7418-
if (pubKeyLen == 0) {
7419-
ret = wc_SlhDsaKey_ImportPrivate(key, privKey, privKeyLen);
7420-
}
7421-
else {
7422-
/* Private key includes public key in SLH-DSA: reconstruct full
7423-
* key = sk_seed || sk_prf || pk_seed || pk_root */
7424-
int n = key->params->n;
7425-
if ((int)privKeyLen == n * 4) {
7426-
/* Full private key already includes public portion. */
7427-
ret = wc_SlhDsaKey_ImportPrivate(key, privKey, privKeyLen);
7428-
}
7429-
else if ((int)privKeyLen == n * 2 && (int)pubKeyLen == n * 2) {
7430-
/* sk_seed || sk_prf separate from pk_seed || pk_root. */
7431-
byte combined[WC_SLHDSA_MAX_PRIV_LEN];
7432-
XMEMCPY(combined, privKey, privKeyLen);
7433-
XMEMCPY(combined + privKeyLen, pubKey, pubKeyLen);
7434-
ret = wc_SlhDsaKey_ImportPrivate(key, combined,
7435-
privKeyLen + pubKeyLen);
7436-
ForceZero(combined, sizeof(combined));
7437-
}
7438-
else {
7439-
ret = BAD_FUNC_ARG;
7440-
}
7441-
}
7467+
if (privSz != params->n * 4) {
7468+
return ASN_PARSE_E;
7469+
}
7470+
7471+
/* Update the key's parameter set to the detected one. */
7472+
key->params = params;
7473+
7474+
/* Import the raw private key: SK.seed || SK.prf || PK.seed || PK.root */
7475+
ret = wc_SlhDsaKey_ImportPrivate(key, input + *inOutIdx, (word32)privSz);
7476+
if (ret == 0) {
7477+
*inOutIdx += (word32)privSz;
74427478
}
74437479

7444-
ForceZero(privKey, sizeof(privKey));
74457480
return ret;
74467481
}
74477482

7448-
/* Decode a DER-encoded SLH-DSA public key.
7483+
/* Decode a DER-encoded SLH-DSA public key (SubjectPublicKeyInfo).
74497484
*
7450-
* The parameter set is detected from the OID in the DER encoding. On
7451-
* success, key->params is updated to match the detected parameter set.
7485+
* The parameter set is detected from the AlgorithmIdentifier OID.
7486+
* On success, key->params is updated to match the detected parameter set.
74527487
*
74537488
* @param [in] input DER-encoded key data.
74547489
* @param [in, out] inOutIdx Index into input, updated on return.
74557490
* @param [in, out] key SLH-DSA key. Parameter set is auto-detected.
74567491
* @param [in] inSz Size of input in bytes.
74577492
* @return 0 on success.
74587493
* @return BAD_FUNC_ARG when input, inOutIdx, or key is NULL.
7459-
* @return ASN_PARSE_E when OID does not match any supported SLH-DSA param.
7494+
* @return ASN_PARSE_E when the DER cannot be parsed as an SLH-DSA key.
74607495
*/
74617496
int wc_SlhDsaKey_PublicKeyDecode(const byte* input, word32* inOutIdx,
74627497
SlhDsaKey* key, word32 inSz)
74637498
{
74647499
int ret = ASN_PARSE_E;
74657500
byte pubKey[WC_SLHDSA_MAX_PUB_LEN];
74667501
word32 pubKeyLen = 0;
7467-
word32 savedIdx;
74687502
int i;
74697503
const SlhDsaParameters* matched = NULL;
74707504

74717505
if ((input == NULL) || (inOutIdx == NULL) || (key == NULL) || (inSz == 0)) {
74727506
return BAD_FUNC_ARG;
74737507
}
74747508

7475-
savedIdx = *inOutIdx;
7476-
7477-
/* Try each compiled-in parameter set until one matches the OID. */
7509+
/* Try each compiled-in parameter set against the OID in the SPKI. */
74787510
for (i = 0; i < SLHDSA_PARAM_LEN; i++) {
74797511
int keytype = slhdsa_param_to_keytype(SlhDsaParams[i].param);
7480-
word32 idx = savedIdx;
7512+
word32 idx = *inOutIdx;
74817513
word32 pkLen = (word32)sizeof(pubKey);
74827514

74837515
if (keytype < 0) {
@@ -7538,18 +7570,27 @@ int wc_SlhDsaKey_PublicKeyToDer(SlhDsaKey* key, byte* output, word32 inLen,
75387570
return ret;
75397571
}
75407572

7541-
/* Encode an SLH-DSA key (private + public) to DER.
7573+
/* Encode an SLH-DSA private key to DER (PKCS#8 / OneAsymmetricKey).
7574+
*
7575+
* RFC 9909: The privateKey OCTET STRING contains the raw 4*n bytes
7576+
* (SK.seed || SK.prf || PK.seed || PK.root) directly, without a nested
7577+
* OCTET STRING wrapper. This differs from Ed25519/Ed448 which use a
7578+
* double OCTET STRING wrapping.
7579+
*
7580+
* Pass NULL for output to get the required buffer size.
75427581
*
75437582
* @param [in] key SLH-DSA key object.
7544-
* @param [out] output Buffer to put encoded data in.
7583+
* @param [out] output Buffer to put encoded data in (or NULL for size).
75457584
* @param [in] inLen Size of buffer in bytes.
75467585
* @return Size of encoded data in bytes on success.
75477586
* @return BAD_FUNC_ARG when key is NULL.
7587+
* @return BUFFER_E when output buffer is too small.
75487588
*/
75497589
int wc_SlhDsaKey_KeyToDer(SlhDsaKey* key, byte* output, word32 inLen)
75507590
{
75517591
int keytype;
75527592
int n;
7593+
word32 privSz, algoSz, verSz, seqSz, sz;
75537594

75547595
if ((key == NULL) || (key->params == NULL)) {
75557596
return BAD_FUNC_ARG;
@@ -7561,24 +7602,48 @@ int wc_SlhDsaKey_KeyToDer(SlhDsaKey* key, byte* output, word32 inLen)
75617602
}
75627603

75637604
n = key->params->n;
7564-
/* Private key portion: sk_seed || sk_prf (first 2*n bytes)
7565-
* Public key portion: pk_seed || pk_root (last 2*n bytes) */
7566-
return SetAsymKeyDer(key->sk, (word32)(n * 2), key->sk + n * 2,
7567-
(word32)(n * 2), output, inLen, keytype);
7605+
/* RFC 9909: bare OCTET STRING containing 4*n raw key bytes */
7606+
privSz = SetOctetString((word32)(n * 4), NULL) + (word32)(n * 4);
7607+
algoSz = SetAlgoID(keytype, NULL, oidKeyType, 0);
7608+
verSz = 3;
7609+
seqSz = SetSequence(verSz + algoSz + privSz, NULL);
7610+
sz = seqSz + verSz + algoSz + privSz;
7611+
7612+
if (output == NULL) {
7613+
return (int)sz;
7614+
}
7615+
if (sz > inLen) {
7616+
return BUFFER_E;
7617+
}
7618+
7619+
{
7620+
word32 idx = 0;
7621+
idx += SetSequence(verSz + algoSz + privSz, output + idx);
7622+
SetMyVersion(0, output + idx, FALSE);
7623+
idx += verSz;
7624+
idx += SetAlgoID(keytype, output + idx, oidKeyType, 0);
7625+
idx += SetOctetString((word32)(n * 4), output + idx);
7626+
XMEMCPY(output + idx, key->sk, (word32)(n * 4));
7627+
idx += (word32)(n * 4);
7628+
return (int)idx;
7629+
}
75687630
}
75697631

7570-
/* Encode an SLH-DSA private key only to DER.
7632+
/* Encode an SLH-DSA private key only (sk_seed || sk_prf) to DER.
7633+
* Same PKCS#8 format but with only the secret portion (2*n bytes).
75717634
*
75727635
* @param [in] key SLH-DSA key object.
7573-
* @param [out] output Buffer to put encoded data in.
7636+
* @param [out] output Buffer to put encoded data in (or NULL for size).
75747637
* @param [in] inLen Size of buffer in bytes.
75757638
* @return Size of encoded data in bytes on success.
75767639
* @return BAD_FUNC_ARG when key is NULL.
7640+
* @return BUFFER_E when output buffer is too small.
75777641
*/
75787642
int wc_SlhDsaKey_PrivateKeyToDer(SlhDsaKey* key, byte* output, word32 inLen)
75797643
{
75807644
int keytype;
75817645
int n;
7646+
word32 privSz, algoSz, verSz, seqSz, sz;
75827647

75837648
if ((key == NULL) || (key->params == NULL)) {
75847649
return BAD_FUNC_ARG;
@@ -7590,8 +7655,30 @@ int wc_SlhDsaKey_PrivateKeyToDer(SlhDsaKey* key, byte* output, word32 inLen)
75907655
}
75917656

75927657
n = key->params->n;
7593-
return SetAsymKeyDer(key->sk, (word32)(n * 2), NULL, 0, output, inLen,
7594-
keytype);
7658+
privSz = SetOctetString((word32)(n * 2), NULL) + (word32)(n * 2);
7659+
algoSz = SetAlgoID(keytype, NULL, oidKeyType, 0);
7660+
verSz = 3;
7661+
seqSz = SetSequence(verSz + algoSz + privSz, NULL);
7662+
sz = seqSz + verSz + algoSz + privSz;
7663+
7664+
if (output == NULL) {
7665+
return (int)sz;
7666+
}
7667+
if (sz > inLen) {
7668+
return BUFFER_E;
7669+
}
7670+
7671+
{
7672+
word32 idx = 0;
7673+
idx += SetSequence(verSz + algoSz + privSz, output + idx);
7674+
SetMyVersion(0, output + idx, FALSE);
7675+
idx += verSz;
7676+
idx += SetAlgoID(keytype, output + idx, oidKeyType, 0);
7677+
idx += SetOctetString((word32)(n * 2), output + idx);
7678+
XMEMCPY(output + idx, key->sk, (word32)(n * 2));
7679+
idx += (word32)(n * 2);
7680+
return (int)idx;
7681+
}
75957682
}
75967683
#endif /* WC_ENABLE_ASYM_KEY_EXPORT */
75977684

wolfssl/internal.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1931,7 +1931,7 @@ WOLFSSL_LOCAL int NamedGroupIsPqcHybrid(int group);
19311931
#endif
19321932
#ifdef WOLFSSL_HAVE_SLHDSA
19331933
#ifndef MIN_SLHDSAKEY_SZ
1934-
#define MIN_SLHDSAKEY_SZ 64 /* smallest SLH-DSA priv key (SHAKE-128) */
1934+
#define MIN_SLHDSAKEY_SZ 32 /* smallest SLH-DSA pub key (SHAKE-128) */
19351935
#endif
19361936
#endif
19371937

0 commit comments

Comments
 (0)