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
14 changes: 6 additions & 8 deletions .github/workflows/nginx-pqc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,22 +63,20 @@ jobs:
else
REFS=$(printf '%s\n%s\n' "master" "$LATEST")
fi
# PQC needs the wc_MlDsaKey_* seed/message API that lands after
# v5.9.1-stable, so the floor is v5.9.2-stable. master is always
# eligible; a -stable ref only runs once it is past the floor. A
# v5.9.2-stable is the first release with the complete PQC interfaces
# required by wolfProvider. master is always eligible. A
# pre-floor ref (e.g. v5.8.4-stable from Wave 2) is dropped -- it
# would just fail the --enable-pqc gate.
PQC_FLOOR="v5.9.1-stable"
PQC_FLOOR="v5.9.2-stable"
ELIGIBLE=()
for ref in $REFS; do
if [ "$ref" = "master" ]; then
ELIGIBLE+=("$ref")
elif [ "$ref" != "$PQC_FLOOR" ] && \
[ "$(printf '%s\n%s\n' "$PQC_FLOOR" "$ref" \
| sort -V | tail -n1)" = "$ref" ]; then
elif [ "$(printf '%s\n%s\n' "$PQC_FLOOR" "$ref" \
| sort -V | head -n1)" = "$PQC_FLOOR" ]; then
ELIGIBLE+=("$ref")
else
echo "::notice::Skipping pre-PQC-floor wolfSSL ref $ref (floor past $PQC_FLOOR)"
echo "::notice::Skipping pre-PQC-floor wolfSSL ref $ref (floor $PQC_FLOOR)"
fi
done
if [ ${#ELIGIBLE[@]} -eq 0 ]; then
Expand Down
55 changes: 34 additions & 21 deletions .github/workflows/wolfssl-pqc-kat.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: wolfSSL PQC KAT (OpenSSL vectors)

# Runs OpenSSL's own ML-KEM (FIPS 203) and ML-DSA (FIPS 204) EVP KAT vectors
# (NIST ACVP + Wycheproof, 2602 sub-tests) through wolfProvider using
# Runs OpenSSL's own ML-KEM (FIPS 203), ML-DSA (FIPS 204), and LMS
# (RFC 8554 / SP 800-208) EVP KAT vectors through wolfProvider using
# OpenSSL's own evp_test harness, unmodified. Closely mirrors the version
# matrix of wolfssl-versions-pqc.yml: a discover job resolves the latest
# wolfSSL -stable tag and the latest OpenSSL release, then the test job runs
Expand Down Expand Up @@ -62,9 +62,9 @@ jobs:
echo "Latest stable wolfSSL: $LATEST"
echo "Latest OpenSSL: $OSSL"
echo "openssl-tag=$OSSL" >> "$GITHUB_OUTPUT"
PQC_FLOOR="v5.9.1-stable"
PQC_FLOOR="v5.9.2-stable"
if [ "$(printf '%s\n%s\n' "$PQC_FLOOR" "$LATEST" \
| sort -V | tail -n1)" != "$PQC_FLOOR" ]; then
| sort -V | head -n1)" = "$PQC_FLOOR" ]; then
LATEST_PQC=true
else
LATEST_PQC=false
Expand All @@ -85,17 +85,25 @@ jobs:
"sfx":" [non-replace] [force-fail]"},
{"replace":false,"ff":"","sfx":" [non-replace]"} ]
| map({"name":($lbl+.sfx),
"wolfssl-ref":$ref,"pqc":true,"replace":.replace,
"wolfssl-ref":$ref,"pqc":true,"lms":true,
"replace":.replace,
"force_fail":.ff})
else
[ {"name":($lbl+" [build-only]"),
"wolfssl-ref":$ref,"pqc":false,"replace":false,
"wolfssl-ref":$ref,"pqc":false,"lms":false,
"replace":false,
"force_fail":""} ]
end;
{ include:
( rows("v5.8.0-stable"; false; "pre-PQC v5.8.0-stable")
+ rows($latest; $latest_pqc; ("latest stable " + $latest))
+ rows("master"; true; "master") )
+ rows("master"; true; "master")
+ [ {"name":"master [LMS only]",
"wolfssl-ref":"master","pqc":false,"lms":true,
"replace":false,"force_fail":""},
{"name":"v5.9.2-stable [LMS only]",
"wolfssl-ref":"v5.9.2-stable","pqc":false,"lms":true,
"replace":false,"force_fail":""} ] )
}')
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"

Expand Down Expand Up @@ -125,25 +133,29 @@ jobs:
id: deps
uses: ./.github/actions/oras-build-deps
with:
variant: pqckat${{ matrix.replace == true && '-rd' || '' }}
variant: pqckat${{ matrix.replace == true && '-rd' || '' }}${{ matrix.lms == true && '-lms' || '' }}
openssl_ref: ${{ needs.discover-versions.outputs.openssl-tag }}
wolfssl_ref: ${{ matrix.wolfssl-ref }}
extra_key: pqc${{ matrix.pqc }}
cache_openssl_source: ${{ matrix.pqc }}
extra_key: pqc${{ matrix.pqc }}-lms${{ matrix.lms }}
cache_openssl_source: ${{ matrix.pqc == true || matrix.lms == true }}
github_token: ${{ secrets.GITHUB_TOKEN }}

- name: Build wolfProvider (PQC=${{ matrix.pqc }}, replace=${{ matrix.replace }})
- name: Build wolfProvider (PQC=${{ matrix.pqc }}, LMS=${{ matrix.lms }}, replace=${{ matrix.replace }})
run: |
ARGS="--enable-pqc"
ARGS=""
if [ "${{ matrix.pqc }}" = "true" ]; then
ARGS="$ARGS --enable-pqc"
fi
if [ "${{ matrix.lms }}" = "true" ]; then
ARGS="$ARGS --enable-lms"
fi
if [ "${{ matrix.replace }}" = "true" ]; then
ARGS="$ARGS --replace-default"
fi
if [ "${{ matrix.pqc }}" != "true" ]; then
ARGS=""
fi
# The KAT runs OpenSSL's evp_test; replace-default builds omit it
# ('no-tests') unless we ask for it here.
if [ "${{ matrix.pqc }}" = "true" ]; then
if [ "${{ matrix.pqc }}" = "true" ] || \
[ "${{ matrix.lms }}" = "true" ]; then
ARGS="$ARGS --enable-openssl-test"
fi
OPENSSL_TAG=${{ needs.discover-versions.outputs.openssl-tag }} \
Expand All @@ -162,18 +174,19 @@ jobs:
openssl_source_hit: ${{ steps.deps.outputs.openssl_source_hit }}
cache_openssl_source: ${{ steps.deps.outputs.cache_openssl_source }}

# Runs all 2602 OpenSSL ML-KEM/ML-DSA vectors. In normal mode every file
# Runs all OpenSSL ML-KEM/ML-DSA/LMS vectors. In normal mode every file
# must pass and the full count must run; in force-fail mode the run fails
# and check-workflow-result.sh inverts that to a pass, proving wolfProvider
# genuinely served the crypto with no silent OpenSSL fallback.
- name: PQC KAT (all 2602 OpenSSL vectors)
if: matrix.pqc == true
- name: PQC KAT (ML-KEM, ML-DSA, and LMS OpenSSL vectors)
if: matrix.pqc == true || matrix.lms == true
shell: bash
run: |
set +e
# The build step already built the provider with --enable-pqc; the KAT
# only runs against it (no rebuild). Force-fail is a runtime env var.
# The KAT only runs against the provider built in the prior step.
export ${{ matrix.force_fail }}
export WOLFPROV_PQC=${{ matrix.pqc == true && '1' || '0' }}
export WOLFPROV_LMS=${{ matrix.lms == true && '1' || '0' }}
./scripts/test-pqc-kat.sh
TEST_RESULT=$?
$GITHUB_WORKSPACE/.github/scripts/check-workflow-result.sh \
Expand Down
48 changes: 26 additions & 22 deletions .github/workflows/wolfssl-versions-pqc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,8 @@ name: wolfSSL Versions (PQC)
# floor, then the build job runs three rows: pre-PQC floor, dynamically
# resolved latest -stable, and master.
#
# PQC is opt-in (--enable-pqc). PQC_FLOOR is v5.9.1-stable: the wc_MlDsaKey_*
# seed/message API wolfProvider's PQC code depends on lands post-v5.9.1-stable
# (wolfSSL PR #10436), so v5.9.2-stable+ is the first PQC-eligible release.
# PQC is opt-in (--enable-pqc). PQC_FLOOR is v5.9.2-stable, the first release
# with the complete ML-KEM and ML-DSA interfaces required by wolfProvider.
# PQC rows build with --enable-pqc against the latest OpenSSL (>= 3.6 required);
# older/no-flag rows build without it and verify PQC is absent (opt-in).

Expand Down Expand Up @@ -60,21 +59,19 @@ jobs:
echo "Latest OpenSSL: $OSSL"
echo "latest-stable=$LATEST" >> "$GITHUB_OUTPUT"
echo "openssl-tag=$OSSL" >> "$GITHUB_OUTPUT"
# Enable PQC when $LATEST is strictly newer than v5.9.1-stable
# (i.e. v5.9.2-stable, v5.10+, v6+, ...). Anything at or before
# the floor lacks the wc_MlDsaKey_* / wc_dilithium_sign_ctx_msg
# API and stays on the no-symbol path.
PQC_FLOOR="v5.9.1-stable"
# Enable PQC at v5.9.2-stable and later. Older releases stay on the
# no-symbol path.
PQC_FLOOR="v5.9.2-stable"
if [ "$(printf '%s\n%s\n' "$PQC_FLOOR" "$LATEST" \
| sort -V | tail -n1)" != "$PQC_FLOOR" ]; then
| sort -V | head -n1)" = "$PQC_FLOOR" ]; then
LATEST_PQC_ELIGIBLE=true
else
LATEST_PQC_ELIGIBLE=false
fi
echo "latest-stable PQC eligible: $LATEST_PQC_ELIGIBLE"
# Each row carries the build flag (enable) and which PQC test families
# must result (expect: both | mlkem | mldsa | none). This exercises the
# combined, per-algorithm, and opt-in-absent paths.
# must result (expect: both | mlkem | mldsa | lms | none). This
# exercises the combined, per-algorithm, and opt-in-absent paths.
MATRIX=$(jq -nc \
--arg latest "$LATEST" \
--argjson latest_pqc "$LATEST_PQC_ELIGIBLE" '{
Expand All @@ -91,7 +88,11 @@ jobs:
{"name":"master (--enable-mlkem only)",
"wolfssl-ref":"master","enable":"--enable-mlkem","expect":"mlkem"},
{"name":"master (--enable-mldsa only)",
"wolfssl-ref":"master","enable":"--enable-mldsa","expect":"mldsa"}
"wolfssl-ref":"master","enable":"--enable-mldsa","expect":"mldsa"},
{"name":"master (--enable-lms only)",
"wolfssl-ref":"master","enable":"--enable-lms","expect":"lms"},
{"name":"v5.9.2-stable (--enable-lms only)",
"wolfssl-ref":"v5.9.2-stable","enable":"--enable-lms","expect":"lms"}
]
}')
echo "matrix=$MATRIX" >> "$GITHUB_OUTPUT"
Expand Down Expand Up @@ -121,7 +122,7 @@ jobs:
id: deps
uses: ./.github/actions/oras-build-deps
with:
variant: pqc
variant: ${{ matrix.expect == 'lms' && 'pqc-lms' || 'pqc' }}
openssl_ref: ${{ needs.discover-versions.outputs.openssl-tag }}
wolfssl_ref: ${{ matrix.wolfssl-ref }}
extra_key: ${{ matrix.enable }}
Expand All @@ -143,24 +144,27 @@ jobs:
wolfssl_hit: ${{ steps.deps.outputs.wolfssl_hit }}

# Opt-in is per-algorithm: assert exactly the expected PQC test families
# are present (both / mlkem / mldsa / none). This catches a leaked
# are present (both / mlkem / mldsa / lms / none). This catches a leaked
# algorithm, a missing one, or PQC dragged in without a flag.
- name: Verify PQC test presence matches opt-in (${{ matrix.expect }})
run: |
export LD_LIBRARY_PATH="$(pwd)/wolfssl-install/lib:$(pwd)/openssl-install/lib:$(pwd)/openssl-install/lib64"
tests=$(./test/unit.test --list) || exit 1
kem=0; dsa=0
kem=0; dsa=0; lms=0
printf '%s\n' "$tests" | grep -q 'test_mlkem_keygen' && kem=1
printf '%s\n' "$tests" | grep -q 'test_mldsa_sign_verify' && dsa=1
echo "expect=${{ matrix.expect }} mlkem=$kem mldsa=$dsa"
printf '%s\n' "$tests" | grep -q 'test_lms_import_export' && lms=1
echo "expect=${{ matrix.expect }} mlkem=$kem mldsa=$dsa lms=$lms"
case "${{ matrix.expect }}" in
both) [ "$kem" = 1 ] && [ "$dsa" = 1 ] ;;
mlkem) [ "$kem" = 1 ] && [ "$dsa" = 0 ] ;;
mldsa) [ "$kem" = 0 ] && [ "$dsa" = 1 ] ;;
none) [ "$kem" = 0 ] && [ "$dsa" = 0 ] ;;
both) [ "$kem" = 1 ] && [ "$dsa" = 1 ] && [ "$lms" = 0 ] ;;
mlkem) [ "$kem" = 1 ] && [ "$dsa" = 0 ] && [ "$lms" = 0 ] ;;
mldsa) [ "$kem" = 0 ] && [ "$dsa" = 1 ] && [ "$lms" = 0 ] ;;
lms) [ "$kem" = 0 ] && [ "$dsa" = 0 ] && [ "$lms" = 1 ] ;;
none) [ "$kem" = 0 ] && [ "$dsa" = 0 ] && [ "$lms" = 0 ] ;;
*) false ;;
esac || { echo "ERROR: PQC test families do not match expect=${{ matrix.expect }}"; exit 1; }

# Run the ML-KEM / ML-DSA / hybrid unit tests: keygen, sign/verify,
# Run the ML-KEM / ML-DSA / LMS / hybrid unit tests: keygen, sign/verify,
# encap/decap, PEM encoder/decoder round-trip, X.509 signing, and the
# hybrid-group KEM. Selected by index from --list so it tracks however
# many PQC tests are registered. LD_LIBRARY_PATH carries libwolfprov's
Expand All @@ -169,7 +173,7 @@ jobs:
if: matrix.expect != 'none'
run: |
export LD_LIBRARY_PATH="$(pwd)/wolfssl-install/lib:$(pwd)/openssl-install/lib:$(pwd)/openssl-install/lib64"
idxs=$(./test/unit.test --list | grep -iE 'mlkem|mldsa|mlx' \
idxs=$(./test/unit.test --list | grep -iE 'mlkem|mldsa|mlx|lms' \
| grep -oE '^[0-9]+')
if [ -z "$idxs" ]; then
echo "::error::No PQC unit tests found"
Expand Down
1 change: 1 addition & 0 deletions ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ number where the code change was added.

## New Feature Additions
* Add ML-KEM (FIPS 203) and ML-DSA (FIPS 204) post-quantum support via `--enable-pqc` (PR 399)
* Add LMS signature verification support (PR 459)

## Enhancements and Optimizations
* Route KDF key-exchange and MAC-signature operations through wolfProvider directly (PR 429)
Expand Down
21 changes: 21 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,27 @@ Without an enable flag no PQC code is compiled, regardless of what wolfSSL enabl
* ML-KEM (FIPS 203): ML-KEM-512, ML-KEM-768, ML-KEM-1024 (key encapsulation)
* ML-DSA (FIPS 204): ML-DSA-44, ML-DSA-65, ML-DSA-87 (signatures, pure mode with empty context per FIPS 204 sec 5.2)

### LMS (RFC 8554 / NIST SP 800-208)
LMS verification is independently opt-in and requires wolfSSL 5.9.2-stable or
newer and OpenSSL 3.6 or newer:

```bash
./scripts/build-wolfprovider.sh --enable-lms
```

wolfProvider follows OpenSSL's LMS provider contract: it imports and exports
raw XDR public keys and supports one-shot verification through
`EVP_PKEY_verify_message_init()` followed by `EVP_PKEY_verify()`. The OpenSSL
wire format omits the single-level HSS header; wolfProvider adds that wrapper
only at the wolfCrypt boundary. LMS signing, key generation, private-key
import, and streaming operations are not exposed because OpenSSL's LMS
provider is verification-only for SP 800-208 software modules.

The PQC KAT workflow runs OpenSSL's 320 LMS verification vectors, including
valid and corrupted messages, signatures, and key encodings. Focused unit
tests cover public-key import/export, XDR decoding, selection handling, and
the unsupported stateful operations.


## Support

Expand Down
13 changes: 13 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,14 @@ AC_ARG_ENABLE([mldsa],
[ ENABLED_MLDSA=$enableval ],
[ ENABLED_MLDSA= ]
)
AC_ARG_ENABLE([lms],
[AS_HELP_STRING([--enable-lms],[Enable LMS verification (default: disabled).])],
[ ENABLED_LMS=$enableval ],
[ ENABLED_LMS=no ]
)
if test "x$ENABLED_LMS" != "xyes" && test "x$ENABLED_LMS" != "xno"; then
AC_MSG_ERROR([--enable-lms accepts only yes or no])
fi

# --enable-pqc is shorthand for both ML-KEM and ML-DSA, unless one was
# explicitly disabled (--enable-pqc --disable-mldsa keeps ML-DSA off).
Expand All @@ -194,6 +202,9 @@ fi
if test "x$ENABLED_MLDSA" = "xyes"; then
AM_CFLAGS="$AM_CFLAGS -DWOLFPROV_HAVE_MLDSA"
fi
if test "x$ENABLED_LMS" = "xyes"; then
AM_CFLAGS="$AM_CFLAGS -DWOLFPROV_HAVE_LMS"
fi

# Set OpenSSL lib directory for installing libdefault.so
if test "x$ENABLED_REPLACE_DEFAULT" = "xyes"; then
Expand Down Expand Up @@ -251,11 +262,13 @@ echo " * Debug silent mode: $ENABLED_DEBUG_SILENT"
echo
test "x$ENABLED_MLKEM" = "xyes" || ENABLED_MLKEM=no
test "x$ENABLED_MLDSA" = "xyes" || ENABLED_MLDSA=no
test "x$ENABLED_LMS" = "xyes" || ENABLED_LMS=no
echo " Features "
echo " * User settings: $ENABLED_USERSETTINGS"
echo " * Dynamic provider: $ENABLED_DYNAMIC_PROVIDER"
echo " * Replace default: $ENABLED_REPLACE_DEFAULT"
echo " * ML-KEM (FIPS 203): $ENABLED_MLKEM"
echo " * ML-DSA (FIPS 204): $ENABLED_MLDSA"
echo " * LMS verification: $ENABLED_LMS"
echo ""
echo "---"
Loading
Loading