Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# wolfSSL Release (unreleased)

## Post-Quantum Cryptography (PQC)

* 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

## Behavioral Changes

* **Behavioral change (`wc_PufReadSram` health tests the raw SRAM readout)**:
Expand Down
1 change: 1 addition & 0 deletions certs/include.am
Original file line number Diff line number Diff line change
Expand Up @@ -184,4 +184,5 @@ include certs/lms/include.am
include certs/rpk/include.am
include certs/acert/include.am
include certs/mldsa/include.am
include certs/mlkem/include.am
include certs/frodokem/include.am
34 changes: 34 additions & 0 deletions certs/mlkem/README.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
ML-KEM (FIPS 203) test key material for wolfSSL tests.

Per level N in {512, 768, 1024}:
mlkem<N>-cert.der ML-KEM end-entity certificate, DER
mlkem<N>-key.der matching private key, PKCS#8 DER (expandedKey form)

A KEM cannot sign, so these certificates cannot be self-signed. Each is
issued by the ML-DSA-87 test key in certs/mldsa, so certs/mldsa/mldsa87-cert.der
is the issuer and certs/mldsa/mldsa87-key.der signed them.

The certificates follow the CNSA 2.0 profile for a key establishment
certificate: the keyUsage extension is marked critical and asserts
keyEncipherment alone. The subjectPublicKeyInfo algorithm is
2.16.840.1.101.3.4.4.N with the parameters field absent, and the signature
algorithm is 2.16.840.1.101.3.4.3.19 (ML-DSA-87).

Regenerate them with certs/renewcerts.sh, which needs an OpenSSL 3.5+ binary
with the built-in ML-DSA and ML-KEM providers. The ML-DSA section runs first
and produces the issuer; the ML-KEM section then builds each certificate from
a throwaway request whose public key is replaced with the ML-KEM one
("x509 -req -force_pubkey"), because ML-KEM cannot sign a request either.

The private keys are written with -provparam ml-kem.output_formats=priv-only,
which is the RFC 9935 section 6 expandedKey shape. That form decodes without
expanding a seed, so the tests pass in WOLFSSL_MLKEM_NO_MAKE_KEY builds too;
the OpenSSL default (seed and expanded key together) would not.

The keys are freshly generated on every run, so the bytes differ each time;
only the structure is reproducible. The files committed here were produced by
wolfSSL itself before renewcerts.sh could make them, so a regenerated
certificate also carries an authorityKeyIdentifier that the committed ones do
not. Nothing in the test suite depends on that.

These are shared test credentials. Never use them in production.
12 changes: 12 additions & 0 deletions certs/mlkem/include.am
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# vim:ft=automake
# All paths should be given relative to the root
#

EXTRA_DIST += \
certs/mlkem/README.txt \
certs/mlkem/mlkem512-cert.der \
certs/mlkem/mlkem512-key.der \
certs/mlkem/mlkem768-cert.der \
certs/mlkem/mlkem768-key.der \
certs/mlkem/mlkem1024-cert.der \
certs/mlkem/mlkem1024-key.der
Binary file added certs/mlkem/mlkem1024-cert.der
Binary file not shown.
Binary file added certs/mlkem/mlkem1024-key.der
Binary file not shown.
Binary file added certs/mlkem/mlkem512-cert.der
Binary file not shown.
Binary file added certs/mlkem/mlkem512-key.der
Binary file not shown.
Binary file added certs/mlkem/mlkem768-cert.der
Binary file not shown.
Binary file added certs/mlkem/mlkem768-key.der
Binary file not shown.
108 changes: 103 additions & 5 deletions certs/renewcerts.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1256,9 +1256,9 @@ EOF
############################################################
# ML-DSA requires an OpenSSL 3.5+ binary with the built-in ML-DSA provider.
# Besides key/cert generation the block also produces the expanded-only
# PKCS#8 key.der (-provparam ml-dsa.output_formats=priv, a 3.5+ built-in
# construct) that the PKCS#7 tests decode without keygen-from-seed. The
# probe below requires both keygen and that conversion, so the common
# PKCS#8 key.der (-provparam ml-dsa.output_formats=priv-only, a 3.5+
# built-in construct) that the PKCS#7 tests decode without keygen-from-seed.
# The probe below requires both keygen and that conversion, so the common
# unsuitable binaries (oqsprovider or pre-3.5, which lack the expanded-only
# conversion) are rejected here and the block is skipped cleanly rather
# than aborting after writing a cert.der but no matching key.der.
Expand All @@ -1285,7 +1285,7 @@ EOF
if ! "$candidate" genpkey -algorithm "mldsa${probe_level}" \
-out "$probe_key" 2>/dev/null || \
! "$candidate" pkey -in "$probe_key" \
-provparam ml-dsa.output_formats=priv -outform DER \
-provparam ml-dsa.output_formats=priv-only -outform DER \
-out /dev/null 2>/dev/null; then
probe_ok=0
break
Expand Down Expand Up @@ -1328,7 +1328,7 @@ EOF
# builds too; the seed-and-expanded default would not. The probe
# above already verified this binary supports the conversion.
"$OPENSSL3" pkey -in "mldsa/mldsa${level}-key.pem" \
-provparam ml-dsa.output_formats=priv -outform DER \
-provparam ml-dsa.output_formats=priv-only -outform DER \
-out "mldsa/mldsa${level}-key.der"
check_result $? "ML-DSA-${level} key DER conversion"

Expand Down Expand Up @@ -1399,6 +1399,104 @@ EOF
echo "---------------------------------------------------------------------"
fi

############################################################
#### ML-KEM (FIPS 203) key establishment certificates ###
############################################################
# ML-KEM is a KEM, so it cannot sign anything, including a certificate
# request or its own certificate. Each end-entity certificate here is
# issued by the ML-DSA-87 certificate produced above, per RFC 9935 and the
# CNSA 2.0 PKIX profile, using a throwaway request that only carries the
# subject name - "x509 -req -force_pubkey" replaces its public key with the
# ML-KEM one before signing. That request is signed by the ML-DSA-87 CA key
# itself, so no extra key material is needed.
#
# The private keys are written in the priv-only PKCS#8 shape (RFC 9935
# section 6 expandedKey), which decodes without keygen-from-seed and so
# works in WOLFSSL_MLKEM_NO_MAKE_KEY builds too. The seed-priv default
# would not.
#
# This needs the same OpenSSL 3.5+ binary as the ML-DSA block, plus its
# built-in ML-KEM provider, so it is probed separately: a binary with
# ML-DSA but no ML-KEM still produces the ML-DSA material above.
if [ -n "$OPENSSL3" ] && [ -f mldsa/mldsa87-cert.pem ]; then
mlkem_probe_key="$(mktemp)"
mlkem_ok=1
for probe_level in 512 768 1024; do
if ! "$OPENSSL3" genpkey -algorithm "ML-KEM-${probe_level}" \
-out "$mlkem_probe_key" 2>/dev/null || \
! "$OPENSSL3" pkey -in "$mlkem_probe_key" \
-provparam ml-kem.output_formats=priv-only -outform DER \
-out /dev/null 2>/dev/null; then
mlkem_ok=0
break
fi
done
rm -f "$mlkem_probe_key"
else
mlkem_ok=0
fi

if [ "$mlkem_ok" -eq 1 ]; then
echo "Generating ML-KEM certificates using: $OPENSSL3"
echo ""
mkdir -p mlkem

# CNSA 2.0 key establishment certificate: keyUsage critical, asserting
# keyEncipherment and nothing else (RFC 9935 section 5).
cat > mlkem/mlkem.ext <<EOF
subjectKeyIdentifier = hash
keyUsage = critical, keyEncipherment
EOF

for level in 512 768 1024; do
echo "Generating ML-KEM-${level} key and certificate..."

"$OPENSSL3" genpkey -algorithm "ML-KEM-${level}" \
-out "mlkem/mlkem${level}-key.pem"
check_result $? "ML-KEM-${level} key generation"

"$OPENSSL3" pkey -in "mlkem/mlkem${level}-key.pem" -pubout \
-out "mlkem/mlkem${level}-pub.pem"
check_result $? "ML-KEM-${level} public key extraction"

# Carrier request. Its own public key is discarded below; only the
# subject name is kept.
"$OPENSSL3" req -new -key mldsa/mldsa87-key.pem \
-subj "/C=US/ST=Montana/L=Bozeman/O=wolfSSL/CN=ML-KEM-${level}" \
-out "mlkem/mlkem${level}.csr"
check_result $? "ML-KEM-${level} request"

"$OPENSSL3" x509 -req -in "mlkem/mlkem${level}.csr" \
-force_pubkey "mlkem/mlkem${level}-pub.pem" \
-CA mldsa/mldsa87-cert.pem -CAkey mldsa/mldsa87-key.pem \
-CAcreateserial -days 3650 -extfile mlkem/mlkem.ext \
-out "mlkem/mlkem${level}-cert.pem"
check_result $? "ML-KEM-${level} certificate generation"

"$OPENSSL3" x509 -in "mlkem/mlkem${level}-cert.pem" -outform DER \
-out "mlkem/mlkem${level}-cert.der"
check_result $? "ML-KEM-${level} DER conversion"

"$OPENSSL3" pkey -in "mlkem/mlkem${level}-key.pem" \
-provparam ml-kem.output_formats=priv-only -outform DER \
-out "mlkem/mlkem${level}-key.der"
check_result $? "ML-KEM-${level} key DER conversion"

# Only the DER files are kept under certs/mlkem; the PEM forms are
# intermediates.
rm -f "mlkem/mlkem${level}.csr" "mlkem/mlkem${level}-key.pem" \
"mlkem/mlkem${level}-pub.pem" "mlkem/mlkem${level}-cert.pem"

echo "End of ML-KEM-${level} section"
done

rm -f mlkem/mlkem.ext mldsa/mldsa87-cert.srl
echo "---------------------------------------------------------------------"
else
echo "Skipping ML-KEM cert generation (no OpenSSL 3.5+ built-in ML-KEM provider found)"
echo "---------------------------------------------------------------------"
fi

############################################################
#### FrodoKEM certificates (generated by wolfSSL) ###
############################################################
Expand Down
113 changes: 112 additions & 1 deletion doc/dox_comments/header_files/wc_mlkem.h
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,11 @@ int wc_MlKemKey_Delete(MlKemKey* key, MlKemKey** key_p);

\param [in,out] key Pointer to the MlKemKey to initialize.
\param [in] type ML-KEM variant: WC_ML_KEM_512, WC_ML_KEM_768 or
WC_ML_KEM_1024.
WC_ML_KEM_1024. Pass WC_ML_KEM_TYPE_UNSET to set the object up without a
parameter set, leaving wc_MlKemKey_PublicKeyDecode or
wc_MlKemKey_PrivateKeyDecode to take it from the algorithm OID in the DER.
Every other operation needs a parameter set and fails with BAD_FUNC_ARG
until a decode has named one.
\param [in] heap Heap hint for dynamic memory allocation. May be
NULL.
\param [in] devId Device identifier for hardware crypto callbacks.
Expand Down Expand Up @@ -470,3 +474,110 @@ int wc_MlKemKey_EncodePrivateKey(MlKemKey* key, unsigned char* out,
*/
int wc_MlKemKey_EncodePublicKey(MlKemKey* key, unsigned char* out,
word32 len);

/*!
\ingroup ML_KEM

\brief Encodes the ML-KEM public key as a DER SubjectPublicKeyInfo.
Pass NULL for output to obtain the required length.

\return Length of the encoding in bytes on success.
\return BAD_FUNC_ARG if key is NULL or its type has no standardised OID.
\return MEMORY_E if dynamic memory allocation fails.

\param [in] key Pointer to an MlKemKey with a public key.
\param [out] output Buffer that receives the DER, or NULL for a length.
\param [in] len Length of output in bytes.
\param [in] withAlg Include the SubjectPublicKeyInfo wrapper when 1, or
emit only the raw public key bytes when 0.

\sa wc_MlKemKey_PublicKeyDecode
\sa wc_MlKemKey_PrivateKeyToDer
*/
int wc_MlKemKey_PublicKeyToDer(MlKemKey* key, byte* output, word32 len,
int withAlg);

/*!
\ingroup ML_KEM

\brief Encodes the ML-KEM private key as a DER PKCS#8 OneAsymmetricKey,
carrying the expanded decapsulation key. Pass NULL for output to obtain
the required length.

\return Length of the encoding in bytes on success.
\return BAD_FUNC_ARG if key is NULL or its type has no standardised OID.
\return MEMORY_E if dynamic memory allocation fails.

\param [in] key Pointer to an MlKemKey with a private key.
\param [out] output Buffer that receives the DER, or NULL for a length.
\param [in] len Length of output in bytes.

\sa wc_MlKemKey_PrivateKeyDecode
\sa wc_MlKemKey_PublicKeyToDer
*/
int wc_MlKemKey_PrivateKeyToDer(MlKemKey* key, byte* output, word32 len);

/*!
\ingroup ML_KEM

\brief Decodes a DER SubjectPublicKeyInfo into an ML-KEM public key. Takes
wrapped DER, where wc_MlKemKey_DecodePublicKey takes the raw encoded key.
A key initialized for a parameter set holds the DER to that same one.
A key initialized with WC_ML_KEM_TYPE_UNSET takes the parameter set from
the algorithm OID in the DER instead.

\return 0 on success.
\return BAD_FUNC_ARG if any required pointer is NULL.
\return ASN_PARSE_E if the DER is invalid or names another parameter set.
\return NOT_COMPILED_IN if the DER names a parameter set this build lacks.

\param [in,out] key Pointer to an initialized MlKemKey.
\param [in] input Buffer holding the DER.
\param [in] inSz Length of input in bytes.
\param [in,out] inOutIdx On in, index into input; on out, index after.

\sa wc_MlKemKey_PublicKeyToDer
\sa wc_MlKemKey_Init
*/
int wc_MlKemKey_PublicKeyDecode(MlKemKey* key, const byte* input, word32 inSz,
word32* inOutIdx);

/*!
\ingroup ML_KEM

\brief Decodes a DER PKCS#8 OneAsymmetricKey into an ML-KEM private key.
Takes wrapped DER, where wc_MlKemKey_DecodePrivateKey takes the raw
encoded key.
All three RFC 9935 Section 6 CHOICE forms are accepted: the 64-byte seed
under an implicit [0], the expanded decapsulation key as an OCTET STRING,
and the SEQUENCE carrying both. A seed is expanded with
ML-KEM.KeyGen_internal(d,z). For the "both" form the expanded key is
regenerated from the seed and the two are compared, so a key whose halves
disagree is rejected as malformed, as RFC 9935 Section 8 requires. A key
initialized for a parameter set holds the DER to that same parameter set;
a key initialized with WC_ML_KEM_TYPE_UNSET takes it from the algorithm
OID in the DER instead.

A build defining WOLFSSL_MLKEM_NO_MAKE_KEY cannot expand a seed, and so
cannot perform the Section 8 comparison either. Such a build rejects every
key that carries a seed, the "both" form included, rather than accepting
its expanded half unchecked.

\return 0 on success.
\return BAD_FUNC_ARG if any required pointer is NULL.
\return MEMORY_E if dynamic memory allocation fails.
\return ASN_PARSE_E if the DER is invalid, names another parameter set,
carries a seed of the wrong length, or pairs a seed with an expanded key
that does not match it.
\return NOT_COMPILED_IN if the key carries a seed and the build defines
WOLFSSL_MLKEM_NO_MAKE_KEY.

\param [in,out] key Pointer to an initialized MlKemKey.
\param [in] input Buffer holding the DER.
\param [in] inSz Length of input in bytes.
\param [in,out] inOutIdx On in, index into input; on out, index after.

\sa wc_MlKemKey_PrivateKeyToDer
*/
int wc_MlKemKey_PrivateKeyDecode(MlKemKey* key, const byte* input, word32 inSz,
word32* inOutIdx);
Loading
Loading