Skip to content

Trim liboqs to Falcon only; migrate Falcon OIDs to oqs-provider#7

Open
Frauschi wants to merge 10 commits intomasterfrom
claude/remove-liboqs-mlkem-mldsa-27LZ1
Open

Trim liboqs to Falcon only; migrate Falcon OIDs to oqs-provider#7
Frauschi wants to merge 10 commits intomasterfrom
claude/remove-liboqs-mlkem-mldsa-27LZ1

Conversation

@Frauschi
Copy link
Copy Markdown
Owner

@Frauschi Frauschi commented Apr 21, 2026

Summary

End-to-end refactor of the post-quantum story:

  1. Remove liboqs ML-KEM / ML-DSA. The native implementations (WOLFSSL_WC_MLKEM, WOLFSSL_WC_DILITHIUM) are now the only ones shipped. ext_mlkem.c / ext_mlkem.h are deleted; all the HAVE_LIBOQS call sites in tls.c, ssl.c, asn.c, dilithium.c, cryptocb.h, test.c, benchmark.c are gone.
  2. Retire the HAVE_PQC umbrella (its semantics were already inconsistent — defined by MLKEM or LIBOQS, but not by native Dilithium/LMS/XMSS). Every call site now uses the specific algorithm macro it actually depends on (WOLFSSL_HAVE_MLKEM, HAVE_FALCON, HAVE_SPHINCS).
  3. Keep Falcon via liboqs but gate it explicitly. New --enable-falcon (Autotools) and WOLFSSL_FALCON (CMake) options. Passing --with-liboqs without --enable-falcon — or vice versa — is a configure error.
  4. Unify mlkem.h + wc_mlkem.h into a single header (renamed to wc_mlkem.h to match wolfcrypt/src/wc_mlkem.c). The prior split only existed because liboqs provided an alternate MlKemKey definition; now there's one struct, one header.
  5. Migrate Falcon X.509 OIDs to the current oqs-provider values: Falcon-512 1.3.9999.3.61.3.9999.3.11; Falcon-1024 1.3.9999.3.91.3.9999.3.14. The legacy OQS-OpenSSL_1_1_1-era OIDs are dropped outright.
  6. Align Falcon TLS 1.3 SignatureAlgorithm codepoints with oqs-provider: Falcon-512 0xFEAE0xFED7; Falcon-1024 0xFEB10xFEDA; the three Falcon hybrid HYBRID_*_SA_MINOR values shift accordingly. wolfSSL ↔ openssl Falcon TLS handshakes now work out of the box with no OQS_CODEPOINT_* env overrides.
  7. Drop the legacy non-RFC-5958 Falcon private-key layout. parse_private_key is deleted; wc_falcon_import_private_{only,key} take raw key bytes (not PKCS8 DER); wc_Falcon_PrivateKeyDecode goes through DecodeAsymKey and delegates straight to the raw-import API. Callers that used to hand in PKCS8 DER (src/internal.c, src/ssl_load.c, wolfcrypt/src/evp_pk.c, wolfcrypt/benchmark/benchmark.c) now go through wc_Falcon_PrivateKeyDecode.
  8. Regenerate certs/falcon/bench_falcon_level{1,5}_key.der with fresh Falcon keypairs under the new OIDs, produced by wc_Falcon_KeyToDer → standard RFC 5958 with publicKey in [1]. wolfssl/certs_test.h regenerated via gencertbuf.pl.
  9. Fix pre-existing wc_Falcon_KeyToDer bug — was passing the secret-key length as pubKeyLen to SetAsymKeyDer, producing DER with 384 bytes of junk in the publicKey field for level 1. Now passes FALCON_LEVEL{1,5}_PUB_KEY_SIZE.
  10. SPHINCS+ left alone — handled separately in Replace liboqs SPHINCS+ with SLH-DSA in certificate layer wolfSSL/wolfssl#10261 in favour of native SLH-DSA. INSTALL note points at that PR.

Rationale for the padded-variant non-change: FIPS 206 (FN-DSA) is still draft. Earlier commits in this branch added Falcon-padded-{512,1024} support, then backed it out per reviewer guidance — only non-padded stays.

Interop tests run locally

Built against the current releases:

  • liboqs 0.15.0 — Falcon-only minimal build
  • oqs-provider 0.11.0 — built against OpenSSL 3.6.2 (from source; the provider-based TLS sigalg interface requires OpenSSL ≥ 3.2)
  • Bouncy Castle 1.84 — from Maven Central; its bcprov advertises Falcon under the current oqs-provider OIDs
  • Botan 3.11.1 — does not ship Falcon at all, so cross-library testing wasn't applicable

X.509 cert interop (all self-signed, both levels)

Producer ↓ / Verifier → wolfSSL oqs-provider (openssl) Bouncy Castle 1.84
wolfSSL (L1) OK OK OK
wolfSSL (L5) OK OK OK
oqs-provider (L1) OK OK OK
oqs-provider (L5) OK OK OK
Bouncy Castle 1.84 (L1) OK OK OK
Bouncy Castle 1.84 (L5) OK OK OK

TLS 1.3 handshake (Falcon authentication)

No environment overrides needed after the codepoint alignment:

Falcon-512 Falcon-1024
openssl s_server → wolfSSL examples/client OK OK
wolfSSL examples/serveropenssl s_client OK (Signature type: falcon512, Verification: OK) OK (Signature type: falcon1024, Verification: OK)

Test plan

  • ./configure && make check — baseline passes.
  • ./configure --enable-mlkem --enable-mldsa && make check — native PQ passes; nm libwolfssl.so | grep OQS_ is empty.
  • ./configure --enable-experimental --with-liboqs fails with "requires --enable-falcon"; ./configure --enable-experimental --enable-falcon fails with "requires --with-liboqs".
  • CMake equivalents (-DWOLFSSL_MLKEM=yes, -DWOLFSSL_DILITHIUM=yes) build; -DWOLFSSL_EXPERIMENTAL=yes -DWOLFSSL_OQS=yes without WOLFSSL_FALCON errors with matching FATAL_ERROR.
  • Full build with --enable-experimental --with-liboqs=<path> --enable-falcon --enable-mlkem --enable-mldsamake check passes; ./wolfcrypt/benchmark/benchmark -falcon_level1 -falcon_level5 runs against the regenerated bench keys.
  • wolfSSL_CTX_use_PrivateKey_file loads wolfSSL-emitted RFC 5958 DER bench keys (round-trip).
  • X.509 Falcon interop matrix above (wolfSSL ↔ oqs-provider ↔ Bouncy Castle 1.84).
  • TLS 1.3 Falcon handshake matrix above (wolfSSL ↔ oqs-provider).

Notes for reviewers

Breaking wire-format changes (all deliberate and aligned with the post-migration state of the ecosystem):

  • Falcon cert OIDs go from the legacy OQS-OpenSSL_1_1_1 values (.6/.9) to the oqs-provider values (.11/.14). wolfSSL no longer parses the old OIDs.
  • Falcon TLS 1.3 sig codepoints go from 0xFEAE/0xFEB1 to 0xFED7/0xFEDA (oqs-provider's values). wolfSSL ↔ wolfSSL Falcon handshakes that used the old codepoints no longer interoperate.
  • Falcon PKCS8 private-key encoding switched from the legacy double-OCTET(priv||pub) wrapping to RFC 5958 with publicKey in [1]. Consequence: oqs-provider 0.11.0 (and older) private-key PEM files will not load via wc_Falcon_PrivateKeyDecode because that release still emits the legacy layout. Newer oqs-provider releases that emit RFC 5958 are unaffected. Cert-level interop is unaffected because certs go through DecodeAsymKey directly.

Other things worth flagging:

  • oid_sum.h is regenerated from scripts/asn1_oid_sum.pl. The 16-bit-CPU note was folded into the generator's header block so it survives future regenerations. FALCON_LEVEL5k and CTC_FALCON_LEVEL5 use an explicit oid_sum => 279 override because the natural byte-sum for 1.3.9999.3.14 would have collided with SPHINCS_FAST_LEVEL1k on the WC_16BIT_CPU path.
  • Dilithium is always native now regardless of --with-liboqs — the if test "$ENABLED_LIBOQS" = "no" gate on WOLFSSL_WC_DILITHIUM is removed in configure.ac. Anyone previously relying on liboqs-backed Dilithium loses it silently.

Open follow-ups (not in this PR)

  • Falcon-padded-{512,1024} support. Reviewer guidance was to defer until FIPS 206 stabilises; the scaffolding was prototyped and reverted on this branch.
  • TLS 1.3 handshake support for Botan and Bouncy Castle (neither bctls nor Botan currently implement Falcon at the TLS layer — only at the certificate/primitive layer for BC; Botan has no Falcon at all).

claude added 10 commits April 21, 2026 19:17
ML-KEM and ML-DSA are now provided exclusively by the native wolfSSL
implementations (WOLFSSL_WC_MLKEM, WOLFSSL_WC_DILITHIUM). The liboqs
back-ends for these algorithms are removed.

Falcon remains available via liboqs but must be opted into explicitly:
--enable-falcon in Autotools or WOLFSSL_FALCON=yes in CMake, paired
with --with-liboqs / WOLFSSL_OQS. Enabling one without the other is a
configure error.

The HAVE_PQC umbrella macro is retired: each former call site now uses
the specific algorithm macro it actually depends on (WOLFSSL_HAVE_MLKEM
for ML-KEM groups, HAVE_FALCON / HAVE_SPHINCS for their respective
headers and sources).

SPHINCS+ liboqs integration is left alone here; it is being removed
separately in wolfSSL#10261 in favor of native SLH-DSA.
The split existed so mlkem.h could expose only the public API while
wc_mlkem.h held the struct definition and WOLFSSL_LOCAL prototypes,
letting the now-removed liboqs back-end supply its own struct layout.
With only the native implementation left, the split no longer serves
a purpose.

wc_mlkem.h is folded into mlkem.h, and every caller drops its second
include (autotools, CMake/Zephyr descriptors, Xcode projects, and the
Espressif templates).
Match the source file name (wolfcrypt/src/wc_mlkem.c). Include guard and
file-header comment updated accordingly, and every caller now includes
wolfssl/wolfcrypt/wc_mlkem.h.
Migrate Falcon-512 / Falcon-1024 OIDs to the oqs-provider values
(1.3.9999.3.11 and 1.3.9999.3.14) so wolfSSL-generated certs interop
with current OpenSSL 3 + oqs-provider deployments. The legacy
OQS-OpenSSL_1_1_1 OIDs (3.6 and 3.9) are dropped outright.

Add Falcon-padded-512 and Falcon-padded-1024 (1.3.9999.3.16 and
1.3.9999.3.19) as new keytypes FALCON_LEVEL{1,5}_PADDEDk with
matching CTC_FALCON_LEVEL{1,5}_PADDED signature OIDs. Padded is an
orthogonal signature-encoding flag carried on falcon_key via a new
WC_BITFIELD and exposed through wc_falcon_set_padded /
wc_falcon_get_padded. wc_falcon_sign_msg / wc_falcon_verify_msg
pick OQS_SIG_alg_falcon_padded_{512,1024} when the flag is set;
wc_falcon_sig_size reports the right length per variant. Added
code is guarded so builds against older liboqs without the padded
variants keep compiling.

Every TLS/X.509 site that cases on FALCON_LEVEL{1,5}k is extended
to handle FALCON_LEVEL{1,5}_PADDEDk as well - key-size enforcement,
dynamic-type dispatch, crypto-cb private-key check, DecodeCert
StoreKey path, SignatureCheck, signer free, and the cert-load
auto-detect in GetKeyOID. NIST FIPS 206 (FN-DSA) is leaning toward
shipping both padded and unpadded formats, so keeping both is the
right pre-standard position.

NB: this is a deliberately breaking change for Falcon
wolfSSL-to-wolfSSL certificate interop; the previous wolfSSL OIDs
(3.6 / 3.9) were never compatible with the current oqs-provider in
the first place.
Back out the Falcon-padded-512 / Falcon-padded-1024 plumbing added in
the previous commit while keeping the OID migration to the current
oqs-provider values (1.3.9999.3.11 / 1.3.9999.3.14). Removed pieces:

- FALCON_LEVEL{1,5}_PADDEDk and CTC_FALCON_LEVEL{1,5}_PADDED enum
  entries from scripts/asn1_oid_sum.pl and the regenerated
  wolfssl/wolfcrypt/oid_sum.h.
- Padded OID byte arrays, case branches, and keytype dispatch in
  wolfcrypt/src/asn.c.
- padded:1 bitfield from struct falcon_key plus wc_falcon_set_padded /
  wc_falcon_get_padded and their header declarations.
- Padded OQS_SIG_alg selection in wc_falcon_sign_msg /
  wc_falcon_verify_msg and padded keytype routing in
  wc_Falcon_PrivateKeyDecode / wc_Falcon_PublicKeyDecode /
  wc_Falcon_PublicKeyToDer / wc_Falcon_KeyToDer /
  wc_Falcon_PrivateKeyToDer.
- The GetKeyOID two-stage padded probe.
- Padded case branches in internal.c, ssl.c, ssl_load.c,
  ssl_certman.c, ssl_api_pk.c, and x509.c.
- FALCON_LEVEL{1,5}_PADDED_SIG_SIZE in falcon.h.

Non-padded Falcon keeps the oqs-provider-compatible OIDs.
Rotate certs/falcon/bench_falcon_level{1,5}_key.der to fresh Falcon-512
and Falcon-1024 keypairs (generated against liboqs 0.15.0) wrapped in
PKCS8 with OIDs 1.3.9999.3.11 and 1.3.9999.3.14 - matching wolfSSL's
post-migration keytypes and current oqs-provider. The previous files
still carried 1.3.9999.3.1 / 1.3.9999.3.4 (earlier Falcon submission
OIDs) which did not match any OID wolfSSL actually recognized, so the
Falcon benchmark could not load them.

The emitted DER keeps wolfSSL's legacy OCTET STRING(OCTET STRING(priv
|| pub)) layout so it loads via wc_falcon_import_private_key as the
benchmark expects; only the algorithm OID and the key material change.

Regenerated wolfssl/certs_test.h from the new .der files via
gencertbuf.pl; verified by running wolfcrypt/benchmark/benchmark
-falcon_level1 -falcon_level5 (all sign/verify operations succeed).
Fix pre-existing wc_Falcon_KeyToDer pubKeyLen typo: it was passing
FALCON_LEVELx_KEY_SIZE (secret-key size) as the pubKeyLen argument to
SetAsymKeyDer, producing DER with padding/junk bytes instead of the
real public key. Now passes FALCON_LEVELx_PUB_KEY_SIZE.

Restore the "Note for some CPUs smaller than 32 bit..." header comment
to the oid_sum.h generator so it survives regeneration. Was silently
dropped by the previous regen.

Make Falcon private-key decode accept both wire formats:

  * wc_Falcon_PrivateKeyDecode no longer routes the full DER back
    through parse_private_key's legacy OCTET(OCTET(priv||pub)) parser.
    After DecodeAsymKey extracts privKey and pubKey separately, either
    use them directly (RFC 5958, as oqs-provider emits) or split the
    concatenated priv||pub if the legacy double-OCTET wrapping is
    present.
  * ProcessBufferTryDecodeFalcon now auto-detects the level via the
    OID (by trying each level through wc_Falcon_PrivateKeyDecode),
    and falls back to wc_falcon_import_private_only only when the DER
    length actually matches a Falcon raw-blob size. The previous
    length-based level guess erroneously matched Falcon-1024 against
    an ML-DSA-65 seed-priv PKCS8, masking the correct Dilithium
    dispatch.

Minor: sweep "see mlkem.h" comments in Espressif user_settings.h
templates to "see wc_mlkem.h" to match the renamed header, and point
the INSTALL SPHINCS+ note at wolfSSL#10261 where the native SLH-DSA
replacement is landing.

Verified end-to-end against oqs-provider 0.10.0 on OpenSSL 3.0.13:
 * Four-way X.509 cert matrix (oqs<->wolfSSL, level 1 + 5) passes.
 * wolfSSL_CTX_use_PrivateKey_file loads both oqs-provider RFC 5958
   PEM keys and wolfSSL legacy-format DER bench keys.
 * make check passes with --enable-falcon --with-liboqs.
Delete the parse_private_key helper and the double-OCTET(priv||pub)
layout it handled. Falcon private keys are now accepted only in RFC
5958 / OneAsymmetricKey form: privateKey as a plain OCTET STRING of the
secret scalar, publicKey optionally in the [1] context-specific field.

wc_falcon_import_private_only and wc_falcon_import_private_key are
repurposed: both now take raw key bytes, not PKCS8 DER. The secret
input must be exactly FALCON_LEVELx_KEY_SIZE; the public input must be
exactly FALCON_LEVELx_PUB_KEY_SIZE. Callers that used to hand them
PKCS8 DER (src/internal.c, src/ssl_load.c, wolfcrypt/src/evp_pk.c,
wolfcrypt/benchmark/benchmark.c) now go through wc_Falcon_PrivateKeyDecode.

wc_Falcon_PrivateKeyDecode is also simplified: after DecodeAsymKey
extracts priv/pub it delegates straight to wc_falcon_import_private_key,
with no fallback for the concatenated legacy layout. ProcessBufferTryDecodeFalcon
loses the length-based raw-blob heuristic (it was causing ML-DSA-65
seed-priv PKCS8 files to false-match Falcon-1024 by size).

certs/falcon/bench_falcon_level{1,5}_key.der are regenerated via the
now-correct wc_Falcon_KeyToDer, producing standard RFC 5958 DER with
publicKey in the [1] field. certs_test.h regenerated via gencertbuf.pl.

Interop status:
 * Cert verification with oqs-provider 0.10.0 remains bidirectional
   (cert parsing uses a different code path).
 * Private-key loading of oqs-provider 0.10.0 PEM output no longer
   works - that provider emits the legacy double-OCTET concat layout.
   Newer oqs-provider releases that produce RFC 5958 output are
   unaffected.
Move Falcon-512 from 0xFEAE to 0xFED7 and Falcon-1024 from 0xFEB1 to
0xFEDA, the codepoints oqs-provider registers and that any future
Falcon-capable library will almost certainly inherit. This removes the
need to set OQS_CODEPOINT_FALCON512 / OQS_CODEPOINT_FALCON1024 on the
oqs-provider side for wolfSSL <-> openssl TLS interop.

Hybrid codepoints shift in lockstep:
 * HYBRID_P256_FALCON_LEVEL1_SA_MINOR    0xAF -> 0xD8
 * HYBRID_RSA3072_FALCON_LEVEL1_SA_MINOR 0xB0 -> 0xD9
 * HYBRID_P521_FALCON_LEVEL5_SA_MINOR    0xB2 -> 0xDB

All four Falcon 1.3 handshake combinations (wolfSSL <-> openssl
s_server/s_client, levels 1 and 5) now succeed out of the box with no
environment overrides.

Breaking change note: existing wolfSSL <-> wolfSSL Falcon-authenticated
handshakes that negotiated the old 0xFEAE / 0xFEB1 codepoints will stop
working. Consistent with the OID migration in the same PR, we're
committing to matching the ecosystem rather than preserving prior
wolfSSL wire values. All of these codepoints live under the
experimental 0xFExx range and will change once FN-DSA gets an official
IANA allocation.
With the liboqs back-ends for ML-KEM and Dilithium gone, having two
feature macros per algorithm (WOLFSSL_HAVE_MLKEM and WOLFSSL_WC_MLKEM,
HAVE_DILITHIUM and WOLFSSL_WC_DILITHIUM) is redundant - the WC_ variant
is always set whenever the umbrella is, and vice versa. Keep the
umbrellas, drop the WC_ variants.

  WOLFSSL_WC_MLKEM     -> WOLFSSL_HAVE_MLKEM
  WOLFSSL_WC_DILITHIUM -> HAVE_DILITHIUM

Touches ~40 files: all #ifdef gates in wolfcrypt / src / tests, the
Espressif/STM32/Zephyr/PlatformIO/C# user_settings.h templates, the
ASM files that ship behind these macros, cmake/options.h.in, and the
build-system entries. configure.ac loses the ENABLED_WC_MLKEM shell
variable (was always tied to ENABLED_MLKEM anyway) and the -D flag
emissions for both macros; CMakeLists.txt likewise stops defining the
WC_ variants.

Verified: ./configure --enable-mlkem --enable-mldsa && make check, the
full Falcon build (--enable-experimental --with-liboqs --enable-falcon
--enable-mlkem --enable-mldsa --enable-keygen --enable-certgen) passes
make check, and the TLS 1.3 Falcon handshake matrix with openssl +
oqs-provider is still green.

Breaking for external users who defined WOLFSSL_WC_MLKEM /
WOLFSSL_WC_DILITHIUM in their own user_settings.h - they should switch
to WOLFSSL_HAVE_MLKEM / HAVE_DILITHIUM. Same class of breakage as the
rest of this PR: we're deleting a legacy surface rather than preserving
it.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants