Skip to content

Commit 8709e86

Browse files
committed
fix: check PKCS#8 publicKey against private key
1 parent 7f64587 commit 8709e86

10 files changed

Lines changed: 383 additions & 5 deletions

File tree

‎ChangeLog.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
dimensions is rejected with `CACHE_MATCH_ERROR` instead of being copied in.
4040
A saved cache from an older release cannot be restored by this one.
4141
* **BREAKING (RFC 5958)**: The `publicKey` field of PKCS#8 v2 private keys (`OneAsymmetricKey`) for Ed25519, Ed448, X25519, ML-DSA and Falcon is a BIT STRING and now carries its unused-bits byte, so keys written by wolfSSL load in ring and other conformant parsers, and wolfSSL reads theirs. X25519 key values are still stored in reversed byte order, a separate bug, so X25519 PKCS#8 keys do not yet interoperate. Keys written by earlier wolfSSL still decode, but earlier wolfSSL cannot read keys written by this release. To write keys that every version reads, use `wc_Ed25519PrivateKeyToDer` and the other `*PrivateKeyToDer` functions, which omit `publicKey`. `WC_MLDSA_44/65/87_BOTH_KEY_DER_SIZE` and `CURVE25519_MAX_KEY_TO_DER_SZ` each grow by one byte, so applications that size buffers from them must be recompiled. A non-empty `publicKey` that matches neither form is now rejected with `ASN_PARSE_E`; an empty one is still treated as absent. by @MarkAtwood (PR 11674)
42+
* `wc_Falcon_PrivateKeyDecode()` now rejects a PKCS#8 key whose public key, in `publicKey` or in the legacy concatenated `privateKey`, does not match the private key, returning `PUBLIC_KEY_E` and leaving the key unset; verify-only and crypto-callback-only builds check only that both halves are present. by @MarkAtwood (issue 11671)
4243

4344
## Post-Quantum Cryptography (PQC)
4445

@@ -48,6 +49,7 @@
4849
* Made `wc_falcon_check_key()` constant time, and cut Falcon key generation's peak heap by about 20% with a constant-time inversion of f. by @Frauschi
4950
* Added `--enable-falcon=smallest-mem`, a Falcon signer working in 20*n bytes (12KB peak heap at Falcon-512, 22KB at Falcon-1024). by @Frauschi
5051
* Added ML-KEM (FIPS 203) key OIDs, SubjectPublicKeyInfo and PKCS#8 encoding, and X.509 certificate support, including issuing an ML-KEM certificate with `wc_MakeCert_ex`. A key initialised with the new `WC_ML_KEM_TYPE_UNSET` takes its parameter set from the DER being decoded. by @Frauschi
52+
* `wc_MlDsaKey_PrivateKeyDecode` now returns `PUBLIC_KEY_E` for an expanded ML-DSA private key whose public key does not match the `tr = H(pk)` value the private key embeds (not checked in `WOLF_CRYPTO_CB_ONLY_MLDSA` builds). Such keys previously loaded and then produced signatures that failed to verify. by @MarkAtwood
5153

5254
## Bug Fixes
5355

‎doc/dox_comments/header_files-ja/wc_mldsa.h‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -694,13 +694,14 @@ int wc_MlDsaKey_ExportKey(wc_MlDsaKey* key, byte* priv, word32 *privSz,
694694
/*!
695695
\ingroup ML_DSA
696696
697-
\brief DER/ASN.1でエンコードされたバッファ(PKCS#8 OneAsymmetricKey)からML-DSA秘密鍵を解析します。パラメータセットはエンコード内のアルゴリズム識別子から推定されるため、事前に設定する必要はありません。成功時、*inOutIdxは消費したバイト数分進められます。
697+
\brief DER/ASN.1でエンコードされたバッファ(PKCS#8 OneAsymmetricKey)からML-DSA秘密鍵を解析します。パラメータセットはエンコード内のアルゴリズム識別子から推定されるため、事前に設定する必要はありません。成功時、*inOutIdxは消費したバイト数分進められます。秘密鍵が展開形式のみの場合、エンコードに含まれる公開鍵(publicKeyフィールド、または展開形式の秘密鍵の後に連結された形式)は秘密鍵内のtr = H(pk)の値と照合されます。WOLFSSL_ASN_TEMPLATEかつ鍵生成が有効なビルドでは、シードを含む秘密鍵はシードから鍵ペアが再導出され、エンコードされた公開鍵は無視されます。WOLF_CRYPTO_CB_ONLY_MLDSAが定義されている場合、この照合は行われません。
698698
699699
WOLFSSL_MLDSA_NO_ASN1が定義されていない場合にのみ利用できます。
700700
701701
\return 0 成功した場合に返されます。
702702
\return BAD_FUNC_ARG 必要なポインタのいずれかがNULLの場合に返されます。
703703
\return ASN_PARSE_E エンコードの形式が不正な場合に返されます。
704+
\return PUBLIC_KEY_E エンコードされた公開鍵が秘密鍵と一致しない場合に返されます。拒否された鍵ペアは鍵オブジェクトに読み込まれません。
704705
705706
\param [in,out] key 初期化済みのwc_MlDsaKeyへのポインタ。
706707
\param [in] input DERエンコードされた秘密鍵バイト列。

‎doc/dox_comments/header_files/falcon.h‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,9 +508,16 @@ int wc_falcon_sig_size(falcon_key* key);
508508
\ingroup Falcon
509509
510510
\brief Decodes a DER/ASN.1 (PKCS#8) Falcon private key into key. On return
511-
*inOutIdx is advanced past the consumed input.
511+
*inOutIdx is advanced past the consumed input. When the input carries a
512+
public key, either in the publicKey field or after the private key in the
513+
legacy concatenated privateKey layout, it is checked against the private
514+
key with wc_falcon_check_key(). The check is cryptographic only when
515+
native signing is compiled in; verify-only and crypto-callback-only builds
516+
check only that both halves are present. On a failed check the key is
517+
left with neither half set.
512518
513519
\return 0 on success.
520+
\return PUBLIC_KEY_E if the public key does not match the private key.
514521
\return ASN_PARSE_E or other negative error on a malformed input.
515522
516523
\param [in] input DER-encoded private key.
@@ -520,6 +527,7 @@ int wc_falcon_sig_size(falcon_key* key);
520527
521528
\sa wc_Falcon_PrivateKeyToDer
522529
\sa wc_Falcon_PublicKeyDecode
530+
\sa wc_falcon_check_key
523531
*/
524532
int wc_Falcon_PrivateKeyDecode(const byte* input, word32* inOutIdx,
525533
falcon_key* key, word32 inSz);

‎doc/dox_comments/header_files/wc_mldsa.h‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -876,13 +876,21 @@ int wc_MlDsaKey_ExportKey(wc_MlDsaKey* key, byte* priv, word32 *privSz,
876876
buffer (PKCS#8 OneAsymmetricKey). The parameter set is inferred
877877
from the algorithm identifier in the encoding, so it does NOT
878878
need to be set beforehand. On success *inOutIdx is advanced past
879-
the consumed bytes.
879+
the consumed bytes. When the private key is in expanded form only,
880+
a public key carried in the encoding, in the publicKey field or
881+
appended to the expanded private key, is checked against the
882+
tr = H(pk) value in the private key. In WOLFSSL_ASN_TEMPLATE builds
883+
with key generation, a private key holding a seed is re-derived from
884+
the seed and any encoded public key is ignored. The check is not made when WOLF_CRYPTO_CB_ONLY_MLDSA
885+
is defined.
880886
881887
Only available when WOLFSSL_MLDSA_NO_ASN1 is not defined.
882888
883889
\return 0 on success.
884890
\return BAD_FUNC_ARG if any required pointer is NULL.
885891
\return ASN_PARSE_E on malformed encoding.
892+
\return PUBLIC_KEY_E if the encoded public key does not match the
893+
private key. The rejected keys are not loaded.
886894
887895
\param [in,out] key Pointer to an initialized wc_MlDsaKey.
888896
\param [in] input DER-encoded private key bytes.

‎tests/api/test_falcon.c‎

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -648,6 +648,127 @@ int test_wc_falcon_der(void)
648648
return EXPECT_RESULT();
649649
}
650650

651+
#if defined(WC_FALCON_HAVE_NATIVE_SIGN) && \
652+
(!defined(WOLFSSL_NO_FALCON_LEVEL1) || !defined(WOLFSSL_NO_FALCON_LEVEL5))
653+
/*
654+
* fix is a liboqs-generated PKCS#8: SEQUENCE, version and AlgorithmIdentifier
655+
* (bytes 4..15), a privateKey OCTET STRING wrapping an OCTET STRING whose raw
656+
* key starts at 24, then a 4-byte [1] header and the raw public key, which
657+
* ends the DER. h is fixed by (f, g), so changing one public coefficient
658+
* yields a mismatched pair.
659+
*/
660+
static int falcon_pkcs8_mismatch_level(const byte* fix, word32 fixSz,
661+
byte level, word32 keySz, word32 pubSz)
662+
{
663+
EXPECT_DECLS;
664+
const word32 privIdx = 24;
665+
const word32 pubIdx = fixSz - pubSz;
666+
const word32 rawSz = keySz + pubSz;
667+
const word32 catSz = rawSz + 24;
668+
static const byte msg[] = { 0x61, 0x62, 0x63 };
669+
falcon_key key;
670+
WC_RNG rng;
671+
byte* der = NULL;
672+
word32 idx;
673+
word32 outLen;
674+
675+
ExpectIntEQ(privIdx + keySz + 4, pubIdx);
676+
ExpectIntEQ(fix[privIdx - 4], 0x04);
677+
ExpectIntEQ(fix[pubIdx - 4], 0x81);
678+
ExpectIntLE(catSz, fixSz);
679+
680+
XMEMSET(&rng, 0, sizeof(rng));
681+
ExpectIntEQ(wc_InitRng(&rng), 0);
682+
der = (byte*)XMALLOC(fixSz, NULL, DYNAMIC_TYPE_TMP_BUFFER);
683+
ExpectNotNull(der);
684+
685+
/* [1] publicKey form: a valid key is loaded first, then the same object
686+
* gets the mismatched DER. */
687+
XMEMSET(&key, 0, sizeof(key));
688+
ExpectIntEQ(wc_falcon_init(&key), 0);
689+
ExpectIntEQ(wc_falcon_set_level(&key, level), 0);
690+
idx = 0;
691+
ExpectIntEQ(wc_Falcon_PrivateKeyDecode(fix, &idx, &key, fixSz), 0);
692+
if (der != NULL) {
693+
XMEMCPY(der, fix, fixSz);
694+
/* Last coefficient goes 246 -> 247 (L1) or 6704 -> 6705 (L5), still
695+
* < q: only the h*f == g relation fails. */
696+
der[fixSz - 1] ^= 0x01;
697+
}
698+
idx = 0;
699+
ExpectIntEQ(wc_Falcon_PrivateKeyDecode(der, &idx, &key, fixSz),
700+
WC_NO_ERR_TRACE(PUBLIC_KEY_E));
701+
/* Neither the rejected pair nor the earlier key may remain usable. */
702+
outLen = fixSz;
703+
ExpectIntEQ(wc_falcon_sign_msg(msg, (word32)sizeof(msg), der, &outLen,
704+
&key, &rng), WC_NO_ERR_TRACE(BAD_FUNC_ARG));
705+
outLen = fixSz;
706+
ExpectIntEQ(wc_falcon_export_private_only(&key, der, &outLen),
707+
WC_NO_ERR_TRACE(BAD_FUNC_ARG));
708+
outLen = fixSz;
709+
ExpectIntEQ(wc_falcon_export_public(&key, der, &outLen),
710+
WC_NO_ERR_TRACE(BAD_FUNC_ARG));
711+
wc_falcon_free(&key);
712+
713+
/* Legacy form: privateKey holds concat(priv, pub) and there is no [1]. */
714+
if (der != NULL) {
715+
der[0] = 0x30;
716+
der[1] = 0x82;
717+
der[2] = (byte)((catSz - 4) >> 8);
718+
der[3] = (byte)(catSz - 4);
719+
XMEMCPY(der + 4, fix + 4, 12);
720+
der[16] = 0x04;
721+
der[17] = 0x82;
722+
der[18] = (byte)((rawSz + 4) >> 8);
723+
der[19] = (byte)(rawSz + 4);
724+
der[20] = 0x04;
725+
der[21] = 0x82;
726+
der[22] = (byte)(rawSz >> 8);
727+
der[23] = (byte)rawSz;
728+
XMEMCPY(der + 24, fix + privIdx, keySz);
729+
XMEMCPY(der + 24 + keySz, fix + pubIdx, pubSz);
730+
}
731+
XMEMSET(&key, 0, sizeof(key));
732+
ExpectIntEQ(wc_falcon_init(&key), 0);
733+
ExpectIntEQ(wc_falcon_set_level(&key, level), 0);
734+
idx = 0;
735+
ExpectIntEQ(wc_Falcon_PrivateKeyDecode(der, &idx, &key, catSz), 0);
736+
ExpectIntEQ(idx, catSz);
737+
if (der != NULL) {
738+
der[catSz - 1] ^= 0x01;
739+
}
740+
idx = 0;
741+
ExpectIntEQ(wc_Falcon_PrivateKeyDecode(der, &idx, &key, catSz),
742+
WC_NO_ERR_TRACE(PUBLIC_KEY_E));
743+
outLen = fixSz;
744+
ExpectIntEQ(wc_falcon_sign_msg(msg, (word32)sizeof(msg), der, &outLen,
745+
&key, &rng), WC_NO_ERR_TRACE(BAD_FUNC_ARG));
746+
wc_falcon_free(&key);
747+
748+
XFREE(der, NULL, DYNAMIC_TYPE_TMP_BUFFER);
749+
wc_FreeRng(&rng);
750+
return EXPECT_RESULT();
751+
}
752+
#endif
753+
754+
/* A PKCS#8 public key that does not match the private key must be rejected,
755+
* whether it arrives in [1] or in the legacy concat privateKey. */
756+
int test_wc_falcon_pkcs8_pub_mismatch(void)
757+
{
758+
EXPECT_DECLS;
759+
#if defined(WC_FALCON_HAVE_NATIVE_SIGN) && !defined(WOLFSSL_NO_FALCON_LEVEL1)
760+
ExpectIntEQ(falcon_pkcs8_mismatch_level(bench_falcon_level1_key,
761+
(word32)sizeof_bench_falcon_level1_key, FALCON_LEVEL1,
762+
FALCON_LEVEL1_KEY_SIZE, FALCON_LEVEL1_PUB_KEY_SIZE), TEST_SUCCESS);
763+
#endif
764+
#if defined(WC_FALCON_HAVE_NATIVE_SIGN) && !defined(WOLFSSL_NO_FALCON_LEVEL5)
765+
ExpectIntEQ(falcon_pkcs8_mismatch_level(bench_falcon_level5_key,
766+
(word32)sizeof_bench_falcon_level5_key, FALCON_LEVEL5,
767+
FALCON_LEVEL5_KEY_SIZE, FALCON_LEVEL5_PUB_KEY_SIZE), TEST_SUCCESS);
768+
#endif
769+
return EXPECT_RESULT();
770+
}
771+
651772
/*
652773
* Exhaustive argument sanitising for the always-present entry points. Runs in
653774
* every HAVE_FALCON build (including verify-only / crypto-cb-only); make_key is

‎tests/api/test_falcon.h‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ int test_wc_falcon_sign_vfy(void);
3030
int test_wc_falcon_import_export(void);
3131
int test_wc_falcon_check_key(void);
3232
int test_wc_falcon_der(void);
33+
int test_wc_falcon_pkcs8_pub_mismatch(void);
3334
int test_wc_falcon_error_paths(void);
3435
int test_wc_falcon_deterministic(void);
3536
int test_wc_falcon_key_reuse(void);
@@ -45,6 +46,7 @@ int test_falcon_cb_free(void);
4546
TEST_DECL_GROUP("falcon", test_wc_falcon_import_export), \
4647
TEST_DECL_GROUP("falcon", test_wc_falcon_check_key), \
4748
TEST_DECL_GROUP("falcon", test_wc_falcon_der), \
49+
TEST_DECL_GROUP("falcon", test_wc_falcon_pkcs8_pub_mismatch), \
4850
TEST_DECL_GROUP("falcon", test_wc_falcon_error_paths), \
4951
TEST_DECL_GROUP("falcon", test_wc_falcon_deterministic), \
5052
TEST_DECL_GROUP("falcon", test_wc_falcon_key_reuse), \

0 commit comments

Comments
 (0)