Skip to content

Dual Alg Certificate preTBS improvements#13

Open
Frauschi wants to merge 17 commits into
masterfrom
claude/dual-alg-certs-patch-XlfOg
Open

Dual Alg Certificate preTBS improvements#13
Frauschi wants to merge 17 commits into
masterfrom
claude/dual-alg-certs-patch-XlfOg

Conversation

@Frauschi
Copy link
Copy Markdown
Owner

Summary

Improvements to the preTBS certificate generation used by hybrid dual-algorithm
(X9.146) certificates. The preTBS is now generated directly from the Cert /
DecodedCert structures instead of going through the full X.509 / OpenSSL-extra
machinery, which substantially reduces resource consumption (≈25 kB RAM / code
size). The preTBS DER buffer size is also computed as a tighter upper bound
(certificate size − both signature sizes) instead of the generic
WC_MAX_CERT_VERIFY_SZ, saving a few kB of stack.

Commit 1 — Dual Alg Certificate preTBS improvements

Author: Tobias Frauenschläger.

  • wolfcrypt/src/asn.c
    • New wc_GeneratePreTBS() implemented on top of MakeAnyCert /
      MakeCertReq, copying metadata from DecodedCert directly into a
      transient Cert (small-stack-aware).
    • MakeAnyCert / MakeCertReq learn an optional Cert.pubKeyDer /
      pubKeyLen shortcut so the public key SPKI can be re-used verbatim
      instead of being re-encoded from a parsed key object.
    • MakeAnyCert / MakeCertReq no longer hard-fail when keyType is
      pre-set by the caller (allows caller to drive key selection).
    • GetCertKey() now records rawPublicKey / rawPubKeySize on the
      DecodedCert whenever WOLFSSL_DUAL_ALG_CERTS is enabled.
    • SetAltNamesFromDcert now also flattens altEmailNames into the
      output and propagates the SAN-extension criticality bit.
    • AddDNSEntryToList now always appends (preserving order) instead of
      prepending under the non-OPENSSL_EXTRA build.
    • ParseCertRelative allocates the preTBS buffer at the new tighter
      upper bound and uses the new wc_GeneratePreTBS.
  • src/x509.c
    • Removes the previous wc_GeneratePreTBS() (the X.509-based one); the
      new implementation in wolfcrypt/src/asn.c replaces it.
  • wolfssl/wolfcrypt/asn.h
    • DecodedCert gains rawPublicKey / rawPubKeySize (DUAL_ALG_CERTS).
  • wolfssl/wolfcrypt/asn_public.h
    • Cert gains pubKeyDer / pubKeyLen (DUAL_ALG_CERTS).
  • wolfssl/wolfcrypt/settings.h
    • DUAL_ALG_CERTS no longer force-enables OPENSSL_EXTRA, WOLFSSL_CUSTOM_OID,
      HAVE_OID_ENCODING, HAVE_OID_DECODING.
    • DUAL_ALG_CERTS now force-enables WOLFSSL_ASN_TEMPLATE.

Commit 2 — settings.h: re-enable feature flags required by WOLFSSL_DUAL_ALG_CERTS

Build fix-up so --enable-dual-alg-certs --enable-experimental builds cleanly
on master after Commit 1 removes the OpenSSL-extra force-enables. Re-enables
the minimal set of flags that other DUAL_ALG_CERTS code paths still depend on:

  • KEEP_PEER_CERTtls13.c::decodeRsaKey/decodeEccKey/decodeDilithiumKey
    reference ssl->peerCert.
  • WOLFSSL_ALT_NAMESwc_GeneratePreTBS() calls SetDatesFromDcert() /
    SetAltNamesFromDcert(), which live inside #ifdef WOLFSSL_ALT_NAMES.
  • WOLFSSL_CUSTOM_OID + HAVE_OID_ENCODING + HAVE_OID_DECODING
    tests/api.c (and existing example code) still uses
    wc_SetCustomExtension() to manually craft the
    sapki / altSigAlg / altSigVal extensions for round-trip tests.

OPENSSL_EXTRA stays off, preserving the bulk of the size reduction.

Review notes

A few minor things worth a second look in Commit 1 (left as-authored, not
modified by the build fix-up):

  1. wolfcrypt/src/asn.c wc_GeneratePreTBS() — the closing #endif
    comment reads WOLFSSL_CCERT_EXT but the matching #ifdef is
    WOLFSSL_CERT_EXT. Cosmetic typo.
  2. wolfcrypt/src/asn.c wc_GeneratePreTBS()crlInfo is XMEMCPY'd
    unconditionally; if extCrlInfoRawSz is 0 / extCrlInfoRaw is NULL
    it's a no-op but worth a guard for clarity.
  3. The CSR path in wc_GeneratePreTBS() skips SetDatesFromDcert (good)
    but the alt-names block above doesn't gate on isCSR; CSRs don't
    carry SAN the same way, may want to confirm intent.
  4. MAX_CERTPOL_NB cap on copying cert policies silently drops entries
    beyond the cap — pre-existing behavior, just calling out.

Test plan

  • ./configure --enable-dual-alg-certs --enable-experimental --disable-shared && make -j succeeds (-Werror clean).
  • ./tests/unit.test reports unit_test: Success for all configured tests.
  • CI runs across other build configurations.
  • Confirm size/stack savings vs. previous implementation on target build.

Generated by Claude Code

Frauschi and others added 17 commits April 30, 2026 18:02
Improvements for the preTBS certificate generation for using hybrid dual
algorithm certificates. The preTBS is now generated without the X.509
code, resulting in reduces ressource consumption. (a lot of code size,
around 25kB RAM).

Furthermore, the preTBS der size is now also calculated for an upper bound
(certificate size minus the two signature sizes). This is much lower than
the generic WC_MAX_CERT_VERIFY_SZ value, saving a few kB of additional stack).

Signed-off-by: Tobias Frauenschläger <tobias.frauenschlaeger@oth-regensburg.de>
Follow-up to "Dual Alg Certificate preTBS improvements" which removed the
forced-enable of OPENSSL_EXTRA / WOLFSSL_CUSTOM_OID / HAVE_OID_*. Without
those, several dependent code paths fail to compile when only
WOLFSSL_DUAL_ALG_CERTS is enabled:

  - tls13.c references ssl->peerCert which requires KEEP_PEER_CERT
  - wc_GeneratePreTBS() calls SetDatesFromDcert() / SetAltNamesFromDcert()
    which live under WOLFSSL_ALT_NAMES
  - tests/api.c (and example code) call wc_SetCustomExtension() which
    requires WOLFSSL_CUSTOM_OID + HAVE_OID_ENCODING/DECODING

Re-enable these targeted flags. OPENSSL_EXTRA stays off, which preserves
the bulk of the size reduction the original commit was after.
Correctness:
 - wc_GeneratePreTBS: bounds-check every fixed-size copy from DecodedCert
   into Cert (serial, skid, akid, crlInfo, issRaw, sbjRaw); a malformed
   cert with oversized fields previously overran the destination.
 - wc_GeneratePreTBS: skip the crlInfo / issRaw / sbjRaw XMEMCPY when
   the source pointer is NULL or length is 0 (XMEMCPY(dst,NULL,0) is UB).
 - ParseCertRelative: validate sigLength + altSigValLen < maxIdx before
   subtracting in unsigned arithmetic; a malformed cert could otherwise
   wrap to a huge XMALLOC.
 - GetCertKey: cast the (signed) length expression to word32 when storing
   into DecodedCert.rawPubKeySize; revert the unrelated DUAL_ALG_CERTS
   guard expansion on `pubLen` (it would warn-as-error when neither ECC
   nor DSA is enabled).
 - MakeAnyCert / MakeCertReq: tighten the no-key-passed branch added by
   the patch. Accept it only when the caller has supplied both a non-zero
   keyType *and* a pre-encoded SubjectPublicKeyInfo (pubKeyDer/pubKeyLen);
   otherwise return BAD_FUNC_ARG as before. The previous form silently
   accepted any non-zero keyType regardless of whether the encoder had
   anything to work with.

Style / portability:
 - Hoist the `int i` declaration in wc_GeneratePreTBS out of the for()
   header so the file still builds under --enable-c89.
 - Replace stray // comments and dead TODO blocks with /* */ form.
 - Fix #endif comment typo (WOLFSSL_CCERT_EXT -> WOLFSSL_CERT_EXT) and
   in-comment typos (temporarly, easiers, withing, form -> from).

Test gating:
 - tests/api.c: gate the dual-alg-cert test on WOLFSSL_CUSTOM_OID +
   HAVE_OID_ENCODING since it manually crafts the sapki / altSigAlg /
   altSigVal extensions through wc_SetCustomExtension.

Settings:
 - Drop the WOLFSSL_CUSTOM_OID / HAVE_OID_ENCODING / HAVE_OID_DECODING
   force-enables from the WOLFSSL_DUAL_ALG_CERTS block; with the test
   gated, those flags are no longer prerequisites of the feature itself.
 - Document why KEEP_PEER_CERT and WOLFSSL_ALT_NAMES are still required
   (tls13.c reaches into ssl->peerCert for the alt public key, and
   wc_GeneratePreTBS depends on the SetDates/SetAltNames helpers under
   #ifdef WOLFSSL_ALT_NAMES; alt-name round-tripping is also necessary
   for any cert that carries a SubjectAltName extension).
The X9.146 CertificateVerify path in tls13.c (decodeRsaKey / decodeEccKey
/ decodeDilithiumKey) needed the peer's alternative public key (sapki)
between the Cert message and the CertVerify message. The previous code
read it from ssl->peerCert.sapkiDer, which only exists when
KEEP_PEER_CERT is enabled - so the dual-alg-cert build had to force
KEEP_PEER_CERT on, pulling in unrelated WOLFSSL_X509 machinery.

Add a small owning copy of the peer's sapki on the WOLFSSL struct
(peerSapkiDer / peerSapkiLen, gated on WOLFSSL_DUAL_ALG_CERTS),
populated from args->dCert in ProcessPeerCerts immediately after the
existing KEEP_PEER_CERT block, and freed in wolfSSL_ResourceFree.
The decode*Key functions now read from this field instead of
ssl->peerCert.

Also gate test_dual_alg_ecdsa_mldsa on WOLFSSL_CUSTOM_OID + HAVE_OID_ENCODING
(it crafts the sapki / altSigAlg / altSigVal extensions through
wc_SetCustomExtension, which already pulls in those flags) and localize
its LARGE_TEMP_SZ define so the test compiles when the surrounding test
block is gated out.

With this in place, drop the KEEP_PEER_CERT force-enable from the
WOLFSSL_DUAL_ALG_CERTS block in settings.h. Verified with
--enable-dual-alg-certs --enable-experimental:
  - KEEP_PEER_CERT is no longer auto-defined.
  - tests/unit.test reports Success for all configured tests.
With dilithium and CUSTOM_OID flags additionally enabled, the
end-to-end dual-alg TLS handshake tests (test_dual_alg_support,
test_dual_alg_crit_ext_support, test_dual_alg_ecdsa_mldsa) all pass.
Replace the Sapki-stash approach (peerSapkiDer / peerSapkiLen on the
WOLFSSL struct, decoded lazily in DoTls13CertificateVerify via
decode{Rsa,Ecc,Dilithium,Falcon}Key) with a single dispatch in
ProcessPeerCerts that decodes the alternative public key into the
matching peer*Key slot at the same point the primary key is decoded.

X9.146 dual-algorithm certificates assume the primary and alternative
public keys use different signature algorithms - the previous code
already implicitly required this (decode{Rsa,...}Key returned
INVALID_PARAMETER if its peer*KeyPresent flag was already set by the
primary). Pre-population now makes that assumption explicit: when the
alt key's algorithm collides with the primary's, ProcessPeerCerts
returns PEER_KEY_ERROR with a clear log message instead of failing
later with a generic decode error.

Removed:
 - decodeRsaKey / decodeEccKey / decodeDilithiumKey / decodeFalconKey
   wrappers in src/tls13.c.
 - The lazy-decode dispatch switch in DoTls13CertificateVerify; the
   "swap in the alternative" else-chain that frees the non-matching
   key for SIGSPEC_ALTERNATIVE keeps working unchanged because it
   already operates on the (now pre-populated) peer*Key slots.
 - peerSapkiDer / peerSapkiLen from struct WOLFSSL and the matching
   XMALLOC/XMEMCPY in ProcessPeerCerts and XFREE in
   wolfSSL_ResourceFree.

Added:
 - A new switch on args->dCert->sapkiOID right after the existing
   primary-key switch in ProcessPeerCerts, decoding sapkiDer into
   peerRsaKey / peerEccDsaKey / peerFalconKey / peerDilithiumKey.
   Coverage matches the old decode*Key wrappers (RSA, ECC, Falcon,
   Dilithium / ML-DSA, including FIPS204_DRAFT levels).

The trade-off is one extra wc_*PublicKeyDecode call when the cert
carries a sapki extension but the handshake ends up purely native
(SIGSPEC_NATIVE never consults the alt key). This is intentional and
will be revisited when the CKS extension processing is reworked.

Verified with --enable-dual-alg-certs --enable-experimental
(default config) and additionally with --enable-dilithium --enable-mldsa
+ -DWOLFSSL_CUSTOM_OID -DHAVE_OID_ENCODING -DHAVE_OID_DECODING:
  - KEEP_PEER_CERT remains undefined.
  - All three dual-alg tests (test_dual_alg_support,
    test_dual_alg_crit_ext_support, test_dual_alg_ecdsa_mldsa) pass.
  - tests/unit.test reports 0 failures.
Refactor the inline alt-public-key dispatch added in the previous commit
into a static helper (DecodePeerAltPubKey) and tighten its semantics.

Behaviour changes:
 - Unsupported sapkiOIDs are now logged and skipped instead of failing
   the handshake. A purely-native (sigSpec == NATIVE) handshake against
   a peer cert that carries an alt key in an algorithm we do not
   implement now interoperates again; if the alt key is actually
   needed, the existing validSigAlgo / "swap in alternative" path in
   DoTls13CertificateVerify still fails the handshake later via the
   normal absence-of-peer-key check.
 - Same-algorithm collisions between the primary and alt keys remain
   fatal: silently using the primary key to verify the alt signature
   would otherwise be a security bug, so PEER_KEY_ERROR with a clear
   "...keys collide on X slot" log is the correct outcome.
 - Decode failures, collisions and key-size violations now set
   args->fatal alongside ret so the surrounding ProcessPeerCerts
   error-handling treats them consistently with primary-key errors.

Security:
 - Alt key now goes through the same minimum-key-size policy checks the
   primary dispatch applies (minRsaKeySz / minEccKeySz / minFalconKeySz
   / minDilithiumKeySz). Without this, a peer could ship a sub-policy
   alt key that policy-rejected as primary but slipped through as
   alternative.

Robustness:
 - Mirror the primary dispatch's AllocKey/ReuseKey pattern so a peer
   key slot that was allocated by a prior handshake but not currently
   "present" gets re-initialised before decode, instead of decoding
   into stale state.
 - The Dilithium level-mapping inner switch now has an explicit
   default that returns PEER_KEY_ERROR (and sets fatal) instead of
   silently leaving level=0 and relying on wc_dilithium_set_level()
   to error out.
 - Mid-block declarations replaced with start-of-block declarations
   (per-case scope), keeping --enable-c89 builds happy.

Style:
 - Multi-line WOLFSSL_MSG literal-concatenations collapsed to single
   lines (project convention).

Documentation:
 - settings.h dual-alg-cert block now documents the
   "primary alg != alt alg" assumption explicitly so users hitting
   the new "...keys collide on X slot" PEER_KEY_ERROR have somewhere
   to land.
 - tls13.c "swap in the alternative" else-chain now carries a comment
   explaining its post-pre-population semantics and the requirement
   that any new algorithm added to DecodePeerAltPubKey be mirrored
   into the swap chain.

Verified with --enable-dual-alg-certs --enable-experimental
(default) and additionally with --enable-dilithium --enable-mldsa
+ -DWOLFSSL_CUSTOM_OID -DHAVE_OID_ENCODING -DHAVE_OID_DECODING:
  - Both configs build clean under -Werror.
  - tests/unit.test reports 0 failures (1419 tests).
  - test_dual_alg_support, test_dual_alg_crit_ext_support and
    test_dual_alg_ecdsa_mldsa all pass under the dilithium config.
…cision

The previous wc_GeneratePreTBS reconstructed the preTBS by copying
DecodedCert metadata into a transient Cert and feeding it through
MakeAnyCert. Audit showed this approach is fundamentally fragile for
X9.146 interop:

  - Re-encoding can never guarantee byte-identical output to the
    issuer's original TBSCertificate. Extension order, criticality
    flags (the encoder hardcodes KU=critical, has no knob for EKU /
    CertPolicies / CRLDP / CertPolicies criticality), DN canonical
    encoding, integer min-encoding and many other DER details all
    have to align with whatever produced the original cert.
  - DecodedCert only models a fixed set of extensions; AIA, Name
    Constraints, Policy Constraints/Mappings, CT SCTs, OCSP Must
    Staple, Subject Information Access and any vendor extension are
    silently dropped.
  - Even within the modelled set, basicConstCrit, AKID
    authorityCertIssuer/SerialNumber, and the ACME identifier
    extension were never copied; the encoder ignored most criticality
    flags anyway.
  - issRaw / sbjRaw are bounded by sizeof(CertName) (~352B) so long
    DNs hit BUFFER_E.

X9.146 specifies the preTBS as exactly the TBSCertificate with the
altSignatureValue extension removed - byte for byte. The new
implementation does exactly that:

  - Verifier-side (cert has altSigValue):
      - Locate the [3] EXPLICIT wrapper using DecodedCert.extensions /
        extensionsIdx / extensionsSz.
      - Walk Extension items, find the one whose extnValue OCTET
        STRING is dCert->altSigValDer (pointer overlap into source).
      - Recompute the inner Extensions SEQUENCE / [3] EXPLICIT /
        outer TBS SEQUENCE lengths.
      - Emit: new TBS header + TBS prefix verbatim + new [3] header
        + new inner SEQ header + extensions before altSigVal verbatim
        + extensions after altSigVal verbatim.
  - Issuer-side fast path (cert has no altSigValue yet, used while
    computing the alt signature input): copy the parsed TBS verbatim.

Embedded-friendly: zero heap allocations, ~60 bytes of stack, single
forward parse of the extensions list, three XMEMCPY calls into the
caller's buffer. Function no longer depends on WOLFSSL_CERT_GEN /
WOLFSSL_CERT_EXT / WOLFSSL_ALT_NAMES / SetDatesFromDcert /
SetAltNamesFromDcert / MakeAnyCert / MakeCertReq.

Removed scaffolding (only existed to feed the old reconstruction
path; pure dead-weight under the new approach):

  - DecodedCert.rawPublicKey / rawPubKeySize
  - Cert.pubKeyDer / pubKeyLen
  - GetCertKey raw-SPKI capture
  - MakeAnyCert / MakeCertReq "pre-encoded SPKI" fast paths
  - MakeAnyCert / MakeCertReq keyType-only validation relaxation
  - SetAltNamesFromDcert altEmailNames merge + criticality propagation
    (was working around DecodedCert -> Cert metadata loss; not needed
    when we don't go through Cert)
  - AddDNSEntryToList ordering change (was workaround for SAN order
    in re-encoded preTBS)
  - WOLFSSL_ALT_NAMES force-enable in settings.h DUAL_ALG_CERTS block
    (was only required by the SetAltNamesFromDcert call)

CSR preTBS support deliberately left out: CSR extensions sit inside
the attributes field, not under a [3] EXPLICIT wrapper, so excision
needs a different walker. wc_GeneratePreTBS now returns
NOT_COMPILED_IN for CSRs; the existing dual-alg-cert verify flow does
not exercise CSRs.

Verified with --enable-dual-alg-certs --enable-experimental
(default) and additionally --enable-dilithium --enable-mldsa
+ -DWOLFSSL_CUSTOM_OID -DHAVE_OID_ENCODING -DHAVE_OID_DECODING:
  - Both configs build clean under -Werror.
  - tests/unit.test reports 0 failures.
  - test_dual_alg_support, test_dual_alg_crit_ext_support and
    test_dual_alg_ecdsa_mldsa all pass under the dilithium config -
    the latter exercises the issuer-side fast path (TBS verbatim)
    and the test_dual_alg_support TLS handshake exercises the
    verifier-side excision path against an actual peer cert.
X9.146 / ITU-T X.509:2019 Annex A mandates that the altSignatureValue
extension be the last extension in TBSCertificate.extensions. Encode
that assumption explicitly:

  - Reject any cert where the altSigValue extension is followed by
    further extensions, with ASN_PARSE_E and a clear log message.
    Silently accepting such a cert would let a peer inject extension
    content the alt signature does not cover.
  - Drop the post-altSigValue verbatim copy from the emit path - it
    is always empty for a conforming cert. Two XMEMCPY calls instead
    of three; one fewer branch in the common path.

Verified with --enable-dual-alg-certs --enable-experimental and
additionally --enable-dilithium --enable-mldsa
+ -DWOLFSSL_CUSTOM_OID -DHAVE_OID_ENCODING -DHAVE_OID_DECODING:
  - Both configs build clean under -Werror.
  - tests/unit.test reports 0 failures.
  - All three dual-alg tests still pass; test_dual_alg_ecdsa_mldsa
    in particular exercises the verifier path (wc_GeneratePreTBS on
    a fully-formed cert with altSigValue last).
Extend wc_GeneratePreTBS to support PKCS#10 CertificationRequests as
well as X.509 certificates. Refactored into:

  - wc_GeneratePreTBS()        public dispatcher (input validation +
                               issuer-side "no altSigValue yet"
                               verbatim-copy fast path).
  - GenerateCertPreTBS()       static helper, X.509 cert path
                               (unchanged logic, just relocated).
  - GenerateCsrPreTBS()        static helper, PKCS#10 CSR path (new).

The CSR helper walks the CRInfo wrapper chain
  CRInfo SEQ -> attributes [0] IMPLICIT
            -> extensionRequest Attribute SEQ
              -> values SET
                -> Extensions SEQ
                  -> Extension items
to locate the altSignatureValue extension (using the same pointer-
overlap match against dCert->altSigValDer that the cert path uses),
recomputes shrunken lengths for each enclosing wrapper bottom-up, and
emits the new CRInfo as a sequence of new wrapper headers + verbatim
copies of unchanged regions.

Other Attributes (challengePassword, contentType, ...) and extensions
preceding altSigValue pass through verbatim, so any CSR layout
conforming to X9.146 round-trips byte-for-byte.

Like the cert path, the CSR path enforces that altSigValue is the
last extension (per X9.146 / ITU-T X.509:2019 Annex A) and rejects
non-conforming inputs with ASN_PARSE_E.

Embedded budget: still no heap allocation, ~80 bytes of stack peak,
single forward parse, fixed (small) number of XMEMCPY calls into
the caller's buffer.

Verified with --enable-dual-alg-certs --enable-experimental and
--enable-dilithium --enable-mldsa
+ -DWOLFSSL_CUSTOM_OID -DHAVE_OID_ENCODING -DHAVE_OID_DECODING:
  - Both configs build clean under -Werror.
  - tests/unit.test reports 0 failures (1419 tests).
  - All three existing dual-alg tests still pass; the cert path was
    not regressed by the refactor.

Note: end-to-end CSR test coverage is currently absent from the test
suite (the existing dual-alg tests only exercise certificates, not
CSRs). The CSR helper has been compiled, statically reasoned about
against the parser's emitted offsets (extensions / extensionsIdx /
extensionsSz / altSigValDer), and the wrapper math has been
hand-verified, but a CSR-shaped test that builds, signs, excises,
verifies should be added as a follow-up.
… roundtrip

Add three new tests in tests/api.c, each gated identically to the
existing test_dual_alg_ecdsa_mldsa (WOLFSSL_DUAL_ALG_CERTS +
HAVE_DILITHIUM + HAVE_ECC + WOLFSSL_CUSTOM_OID + HAVE_OID_ENCODING +
non-small-stack):

  test_dual_alg_pretbs_cert
    Direct unit test for wc_GeneratePreTBS on an X.509 dual-alg cert.
    Covers:
      - NULL / zero-size argument validation (BAD_FUNC_ARG).
      - Issuer-side fast path: cert built with sapki+altSigAlg only,
        preTBS must equal the parsed TBS verbatim (XMEMCMP against
        dCert->source[certBegin..sigIndex]).
      - BUFFER_E when the output buffer is too small.
      - Verifier-side excision: after embedding altSigValue and
        re-emitting, preTBS must be strictly smaller than TBS and
        re-encode cleanly. (Byte-comparing the resulting alt sig
        directly is impossible because ML-DSA is randomised; the
        end-to-end "alt sig verifies against preTBS" check is
        already exercised by test_dual_alg_support.)

  test_dual_alg_pretbs_altsigval_not_last
    Builds a cert in which altSigValue is followed by another
    extension (a trailing custom OID), violating the X9.146 /
    ITU-T X.509:2019 Annex A "altSignatureValue must be the last
    extension" requirement. Verifies wc_GeneratePreTBS returns
    ASN_PARSE_E. The dummy altSigValue content is a properly-formed
    BIT STRING (real ML-DSA signature over scratch bytes), since the
    cert parser validates the BIT STRING layout before our "last
    extension" check fires.

  test_dual_alg_csr_roundtrip
    First end-to-end CSR-shaped test for the dual-alg-cert feature.
    Builds a PKCS#10 CertificationRequest carrying sapki+altSigAlg in
    its extensionRequest attribute, parses it, generates the preTBS
    via wc_GeneratePreTBS (issuer-side fast path: byte-equal CRInfo),
    signs the preTBS with the ML-DSA alt key, embeds altSigValue,
    re-emits and re-signs the CSR, then parses it again and runs
    preTBS through the verifier-side path (GenerateCsrPreTBS).
    Verifies the verifier-side preTBS is strictly smaller than the
    original CRInfo and still leads with a SEQUENCE tag. Gated on
    WOLFSSL_CERT_REQ in addition to the dual-alg flags above.

Verified with:
  --enable-dual-alg-certs --enable-experimental --disable-shared
    -> base build: 0 failures.
  + --enable-dilithium --enable-mldsa --enable-certreq --enable-certgen
    -DWOLFSSL_CUSTOM_OID -DHAVE_OID_ENCODING -DHAVE_OID_DECODING
    -> all six dual-alg tests pass:
       test_dual_alg_support, test_dual_alg_crit_ext_support,
       test_dual_alg_ecdsa_mldsa, test_dual_alg_pretbs_cert,
       test_dual_alg_pretbs_altsigval_not_last,
       test_dual_alg_csr_roundtrip.
Four new tests, each gated on the appropriate dual-alg flags:

  test_dual_alg_pretbs_csr_altsigval_not_last
    CSR analogue of test_dual_alg_pretbs_altsigval_not_last. Builds a
    CSR whose extensionRequest places altSigValue before another
    extension; wc_GeneratePreTBS must return ASN_PARSE_E (X9.146 /
    ITU-T X.509:2019 Annex A enforcement on the CSR path).

  test_dual_alg_collision_handshake
    Builds a self-signed dual-alg cert with primary ECC + alt ECC and
    runs a memio TLS 1.3 handshake against it. ProcessPeerCerts ->
    DecodePeerAltPubKey must reject with PEER_KEY_ERROR (-342) because
    both keys target the peerEccDsaKey slot. Documents the X9.146
    "primary alg != alt alg" assumption with an executable test.

  test_dual_alg_minkeysize_handshake
    Builds a self-signed dual-alg cert with primary RSA-2048 + alt ECC
    P-256 and runs a memio TLS 1.3 handshake. The client raises
    minEccKeySz to 384 (on both ctx and ssl, since SSL inherits at
    creation time). DecodePeerAltPubKey must reject the alt key with
    ECC_KEY_SIZE_E (-410). Confirms the minimum-key-size policy
    cannot be bypassed via the alt key.

  test_dual_alg_unsupported_alt_native
    Builds a self-signed cert with primary ECC + alt Ed25519 (a real
    Ed25519 key, real Ed25519 alt sig). Verifies the parser tolerates
    the unrecognised sapki OID (parses with NO_VERIFY, all three
    dual-alg ext flags set) and that wc_GeneratePreTBS still excises
    the altSigValue extension correctly.

    A pure TLS-handshake regression test for this case isn't
    currently feasible: the cert's alt signature must verify before
    ProcessPeerCerts/DecodePeerAltPubKey is reached, but the Ed25519
    leg of ConfirmSignature (cert/ed25519.c) calls
    wc_ed25519_import_public on cert->ca->sapkiDer - which is the
    SPKI DER, not the raw 32-byte key the function expects (the ECC
    and RSA cases use wc_*PublicKeyDecode which accepts SPKI DER).
    That's a pre-existing wolfSSL gap independent of this PR; the
    test comment documents it for follow-up.

Two shared in-memory cert builders introduced for the negative TLS
handshake tests:

  do_build_dual_alg_self_signed         primary ECC, configurable alt
                                        ECC key
  do_build_dual_alg_self_signed_rsa     primary RSA, ECC alt
                                        (gated on !NO_RSA + WOLFSSL_KEY_GEN)
  do_build_dual_alg_self_signed_ed25519alt
                                        primary ECC, Ed25519 alt

Each builder mirrors the issuer flow: build interim TBS, generate
preTBS, sign with alt key, embed altSigValue, re-emit and re-sign
primary - so the resulting cert exercises the entire dual-alg
generation path before being handed to a memio TLS context.

Verified with --enable-dual-alg-certs --enable-experimental
(default) and additionally with --enable-dilithium --enable-mldsa
--enable-certreq --enable-certgen --enable-keygen --enable-ed25519
+ -DWOLFSSL_CUSTOM_OID -DHAVE_OID_ENCODING -DHAVE_OID_DECODING:
  - Both configs build clean under -Werror.
  - All 10 dual-alg tests pass:
      test_dual_alg_support
      test_dual_alg_crit_ext_support
      test_dual_alg_ecdsa_mldsa
      test_dual_alg_pretbs_cert
      test_dual_alg_pretbs_altsigval_not_last
      test_dual_alg_csr_roundtrip
      test_dual_alg_pretbs_csr_altsigval_not_last
      test_dual_alg_collision_handshake
      test_dual_alg_minkeysize_handshake
      test_dual_alg_unsupported_alt_native
…rify

The Ed25519 and Ed448 legs of ConfirmSignature called
wc_ed{25519,448}_import_public on the supplied key blob, which expects
the *raw* public key bytes. That works for the primary-signature
verify path because StoreKey strips the SubjectPublicKeyInfo BIT
STRING wrapper into cert->publicKey, but it broke the X9.146
alternative-signature verify path: dual-alg sapki is preserved as the
full SubjectPublicKeyInfo DER (cert->ca->sapkiDer in
ParseCertRelative), so for an Ed25519/Ed448 sapki the import call
returned BAD_FUNC_ARG and the cert was rejected before
DecodePeerAltPubKey could run.

Try the raw import first (preserves the existing primary path) and
fall back to wc_Ed{25519,448}PublicKeyDecode (which strips the SPKI
wrapper) when the raw form doesn't fit. Same dual-input pattern as
the comment notes; one extra call only on the SPKI path.

This unblocks the test_dual_alg_unsupported_alt_native test, which is
now a real TLS 1.3 handshake exercising:

  - issuer-side: build a self-signed cert with primary ECDSA + alt
    Ed25519 (real key, real Ed25519 alt signature),
  - parse-time: ConfirmSignature verifies the Ed25519 alt sig against
    the cert's own Ed25519 sapki (via the new SPKI fallback),
  - DecodePeerAltPubKey: hits its default "log + skip" branch for the
    unrecognised Ed25519 sapki OID,
  - handshake: completes successfully because sigSpec == NATIVE never
    consumes the alt key.

Verified with --enable-dual-alg-certs --enable-experimental
(default) and additionally with --enable-dilithium --enable-mldsa
--enable-certreq --enable-certgen --enable-keygen --enable-ed25519
+ -DWOLFSSL_CUSTOM_OID -DHAVE_OID_ENCODING -DHAVE_OID_DECODING:
  - Both configs build clean under -Werror.
  - All 10 dual-alg tests pass.
  - test_dual_alg_unsupported_alt_native is now an end-to-end TLS
    handshake (was a parser-only fallback in the previous commit).
Address review of the previous Ed25519/Ed448 alt-sig fix (305172d).

asn.c
 - wc_Ed25519PublicKeyDecode and wc_Ed448PublicKeyDecode now accept
   either a raw public key or a full SubjectPublicKeyInfo DER, mirroring
   the dual-input pattern of wc_Falcon_PublicKeyDecode and
   wc_Dilithium_PublicKeyDecode (both of which already do raw-then-SPKI
   internally). Try the raw form first; fall back to DecodeAsymKeyPublic
   only when the raw size doesn't fit.
 - ConfirmSignature ED25519k / ED448k cases now just call
   wc_Ed25519PublicKeyDecode / wc_Ed448PublicKeyDecode, matching the
   Falcon and Dilithium cases. The earlier inline raw-then-SPKI
   fallback (in ConfirmSignature) is gone - the dual-input handling
   lives in the public decoder now and is reusable by anyone who
   passes either form (e.g. test_dual_alg_eddsa_pubkey_decode_dual_input
   below). One copy of the logic instead of two.

tests/api.c
 - Collapsed three near-duplicate dual-alg cert builders
   (do_build_dual_alg_self_signed, do_build_dual_alg_self_signed_rsa,
   do_build_dual_alg_self_signed_ed25519alt) into a single
   parameterised do_build_dual_alg_self_signed taking
   (primaryKeyType, primaryKey, primarySigType, altKeyType, altKey,
   altSigType, rng). Three call sites updated. ~150 lines deleted.
 - test_dual_alg_collision_handshake now asserts wolfSSL_get_error
   returns PEER_KEY_ERROR rather than just "any non-zero handshake
   return", so a future change failing the handshake for an unrelated
   reason can't silently pass.
 - test_dual_alg_minkeysize_handshake similarly asserts ECC_KEY_SIZE_E.
 - New test_dual_alg_eddsa_pubkey_decode_dual_input: focused regression
   for the SPKI/raw fallback in wc_Ed{25519,448}PublicKeyDecode. Builds
   a real Ed25519 key, exports it both as raw and as SPKI, and
   round-trips both forms back through the decoder. Same for Ed448 when
   built. Lightweight (no TLS), gated on key export/import flags only -
   so it covers the new behaviour even when WOLFSSL_DUAL_ALG_CERTS is
   off.

Verified with --enable-dual-alg-certs --enable-experimental
(default) and additionally with --enable-dilithium --enable-mldsa
--enable-certreq --enable-certgen --enable-keygen --enable-ed25519
--enable-ed448 + -DWOLFSSL_CUSTOM_OID -DHAVE_OID_ENCODING
-DHAVE_OID_DECODING:
  - Both configs build clean under -Werror.
  - All 11 dual-alg tests pass:
      test_dual_alg_support
      test_dual_alg_crit_ext_support
      test_dual_alg_ecdsa_mldsa
      test_dual_alg_pretbs_cert
      test_dual_alg_pretbs_altsigval_not_last
      test_dual_alg_csr_roundtrip
      test_dual_alg_pretbs_csr_altsigval_not_last
      test_dual_alg_collision_handshake
      test_dual_alg_minkeysize_handshake
      test_dual_alg_unsupported_alt_native
      test_dual_alg_eddsa_pubkey_decode_dual_input
X9.146 / ITU-T X.509:2019 9.8.4 defines preTBSCertificate as
TBSCertificate with both
  - the signature field (the primary signature
    AlgorithmIdentifier inside TBSCertificate), and
  - the altSignatureValue extension
removed. The previous excision-based wc_GeneratePreTBS only handled the
altSignatureValue extension and copied the rest of the TBS verbatim,
which left the signature AlgorithmIdentifier in place. Existing tests
passed only because both the issuer-side and verifier-side flows went
through the same buggy implementation, so the alt sigs always matched
each other (but they would not match a spec-conforming peer's preTBS).

The old MakeAnyCert-based implementation handled this correctly via
'tbsCert->sigType = 0' triggering 'noOut' on the algorithm SEQUENCE -
the excision rewrite missed the equivalent step.

GenerateCertPreTBS now:
  1. Walks past version (optional [0] EXPLICIT) and serialNumber.
  2. Records the signature AlgorithmIdentifier SEQUENCE bounds.
  3. Optionally locates altSignatureValue inside the Extensions wrapper
     (verifier-side; skipped on issuer-side flows that haven't added
     altSigValue yet).
  4. Emits: new TBS SEQUENCE header + version+serial verbatim + (skip
     signature AlgID) + issuer..SPKI..uniqueIDs verbatim + (if
     altSigValue is present) reconstructed [3]/Extensions wrappers
     + extensions before altSigValue verbatim.

The CertificationRequestInfo path is unaffected: CSRs carry the
signature AlgorithmIdentifier on the outer CertificationRequest, not
inside CRInfo, so the verbatim copy on the no-altSigValue path is
still correct. The dispatcher is restructured so the issuer-side
"verbatim" fast path lives on the CSR branch only.

tests/api.c
 - test_dual_alg_pretbs_cert's issuer-side assertion updated: preTBS
   is now strictly smaller than the parsed TBS (sig AlgID excised) and
   no longer byte-equal to source[certBegin..sigIndex]; replaced with
   an outer-SEQUENCE-tag sanity check.

Verified with --enable-dual-alg-certs --enable-experimental
(default) and additionally with --enable-dilithium --enable-mldsa
--enable-certreq --enable-certgen --enable-keygen --enable-ed25519
--enable-ed448 + -DWOLFSSL_CUSTOM_OID -DHAVE_OID_ENCODING
-DHAVE_OID_DECODING:
  - Both configs build clean under -Werror.
  - All 11 dual-alg tests pass, including the end-to-end TLS handshake
    tests test_dual_alg_support and test_dual_alg_unsupported_alt_native
    which confirm the new spec-conforming preTBS still verifies on the
    wire.
wolfSSL's dual-alg-cert support does not currently exercise Ed25519 or
Ed448 alt keys, so the additional plumbing in this PR is out of scope
and only adds review surface. Revert the four Ed-specific changes:

asn.c
 - wc_Ed25519PublicKeyDecode and wc_Ed448PublicKeyDecode go back to
   SPKI-only input (dropping the raw-or-SPKI dual-input pattern that
   mirrored Falcon/Dilithium).
 - ConfirmSignature ED25519k / ED448k cases go back to direct
   wc_ed25519_import_public / wc_ed448_import_public on the supplied
   raw key (the historical primary-signature behaviour).

tests/api.c
 - Remove test_dual_alg_unsupported_alt_native (depended on Ed25519 alt
   keys reaching DecodePeerAltPubKey, which is unreachable now).
 - Remove test_dual_alg_eddsa_pubkey_decode_dual_input (covered the
   reverted decoder behaviour).
 - Remove the ED25519_TYPE case from do_build_dual_alg_self_signed's
   alt-key SPKI export switch and update the helper's pairings comment.
 - Update the registration list and the negative-tests comment block.

Other dual-alg-cert work in this PR is unaffected. After the revert:

  - 9 dual-alg tests still pass under the full config
    (--enable-dual-alg-certs --enable-experimental --enable-dilithium
    --enable-mldsa --enable-certreq --enable-certgen --enable-keygen
    + -DWOLFSSL_CUSTOM_OID -DHAVE_OID_ENCODING -DHAVE_OID_DECODING):
      test_dual_alg_support
      test_dual_alg_crit_ext_support
      test_dual_alg_ecdsa_mldsa
      test_dual_alg_pretbs_cert
      test_dual_alg_pretbs_altsigval_not_last
      test_dual_alg_csr_roundtrip
      test_dual_alg_pretbs_csr_altsigval_not_last
      test_dual_alg_collision_handshake
      test_dual_alg_minkeysize_handshake
  - Base config (--enable-dual-alg-certs --enable-experimental):
    clean -Werror, 0 failures.

Ed25519/Ed448 alt-sig support can be added in a follow-up PR alongside
DecodePeerAltPubKey cases for those algorithms.
…trs)

BouncyCastle's PKCS10CertificationRequestBuilder.build(signer,
altPubKey, altSigner) - the API EJBCA's HybridCertificateSystemTest
uses to issue dual-algorithm CSRs - emits sapki / altSigAlg /
altSigValue as top-level PKCS#10 attributes whose OIDs are the X.509
extension OIDs (2.5.29.72/73/74). wolfSSL's CSR parser previously
only recognised them nested inside an extensionRequest attribute (the
IETF lamps draft style; how wolfSSL's own wc_SetCustomExtension flow
emits them). Net result: wolfSSL could not parse a BC- or
EJBCA-issued dual-algorithm CSR.

Two minimal changes:

  1. DecodeCertReqAttrValue (the WOLFSSL_ASN_TEMPLATE-path CSR
     attribute decoder) now recognises SUBJ_ALT_PUB_KEY_INFO_OID,
     ALT_SIG_ALG_OID and ALT_SIG_VAL_OID as top-level attributes,
     calling the existing DecodeSubjAltPubKeyInfo / DecodeAltSigAlg /
     DecodeAltSigVal directly on the SET content. Both encodings -
     top-level (BC/EJBCA) and extensionRequest-nested (lamps draft) -
     populate the same DecodedCert fields; downstream code is
     encoding-agnostic.

  2. GenerateCsrPreTBS now first scans the CRInfo's top-level
     attributes for ALT_SIG_VAL_OID; if found, the altSignatureValue
     is excised as a single Attribute SEQUENCE removal (BC encoding,
     two XMEMCPY emit). If not found, the existing extensionRequest
     -> SET -> Extensions -> Extension chain is walked and the
     altSigValue Extension is excised (lamps draft encoding).

  The wc_GeneratePreTBS dispatcher's "extensions != NULL" guard for
  the CSR path is dropped; in the BC encoding altSigValDer is set by
  the top-level attribute parser without populating dCert->extensions.

Verified end-to-end against BouncyCastle 1.80.2 (the JAR EJBCA-CE
9.x bundles; same code path EJBCA's HybridCertificateSystemTest
exercises):

  Direction 1: BC -> wolfSSL
    BC emits a self-signed dual-alg cert (ECDSA P-256 primary +
    ML-DSA-44 alt) and a matching dual-alg PKCS#10 CSR. wolfSSL
    parses both; wc_GeneratePreTBS produces the byte-exact preTBS
    that lets wc_dilithium_verify_ctx_msg confirm the embedded alt
    signature. Both cert and CSR pass.

  Direction 2: wolfSSL CSR -> BC -> wolfSSL
    wolfSSL emits a dual-alg CSR (its native lamps-draft encoding).
    BC reads it, extracts the alt public key from the
    extensionRequest, generates its own CA dual key pair, and issues
    a dual-alg cert binding the CSR's subject keys. wolfSSL parses
    the issued cert; wc_GeneratePreTBS produces a preTBS that
    successfully validates against the BC CA's ML-DSA-44 alt key.

Existing self-tests continue to pass (default config and full
dilithium config: 9/9 dual-alg tests green). The integration test
tooling (BC generator, BC issuer, wolfSSL verifiers) lives outside
the wolfSSL tree at /tmp/ejbca-interop/ and is reproducible from
the bundled BouncyCastle JARs in EJBCA-CE.
Followup to a56735e. Tightens the new CSR-attribute decoder path and
GenerateCsrPreTBS based on review:

DecodeCertReqAttrValue (X9.146 cases)
 - Propagate the underlying decoder error instead of clobbering with
   ASN_PARSE_E. Callers can now distinguish MEMORY_E, ASN_BITSTR_E,
   ASN_OBJECT_ID_E, etc. from a generic parse failure.
 - New CheckSinglePkcs10AttrValue() helper enforces that the SET OF
   AttributeValue holds exactly one TLV. Rejects an over-stuffed SET
   that would silently feed only the first value to the decoder.
 - Updated function docstring to enumerate the X9.146 cases and the
   new ASN_OBJECT_ID_E return when an X9.146 attribute appears at
   both the top level and nested inside extensionRequest (already
   detected by VERIFY_AND_SET_OID; just wasn't documented).

OidFromId / oidCsrAttrType
 - Register SUBJ_ALT_PUB_KEY_INFO_OID, ALT_SIG_ALG_OID and
   ALT_SIG_VAL_OID under oidCsrAttrType so the OID validation in
   GetObjectId can match them against the canonical bytes. Previously
   the cases worked by accident because GetObjectId silently accepts
   unknown OIDs (per the existing TODO at line ~7348); if that TODO is
   ever resolved the new path would have broken.

GenerateCsrPreTBS
 - Renamed the encoding selector from `bcMode` (int 0/1, two
   semantically-distinct states) to `topLevelAttr` with a doc comment.
 - Added an explicit comment in the topLevelAttr branch explaining why
   the lamps-draft "altSigValue must be last" enforcement is *not*
   applied here (PKCS#10 attributes is SET OF, ordering is determined
   by DER's SET OF rule, removing one element leaves the SET sorted).
 - Added a sanity check on the lamps-draft branch: if the cert claims
   the lamps-draft encoding (no top-level altSigValue attribute) but
   the parser didn't populate dCert->extensions / extensionsIdx, fail
   with ASN_PARSE_E rather than walking off the end of an incomplete
   DecodedCert.

Interop coverage unchanged but re-validated:
  - BC -> wolfSSL: cert + CSR alt sigs verify (BouncyCastle 1.80.2
    artefacts, the JAR EJBCA-CE 9.x bundles).
  - wolfSSL CSR -> BC issuer -> wolfSSL: BC issues a dual-alg cert
    from a wolfSSL CSR; wolfSSL verifies the issued cert.
  - All 9 self-tests under --enable-dual-alg-certs --enable-experimental
    --enable-dilithium --enable-mldsa --enable-certreq --enable-certgen
    --enable-keygen still pass (default config: clean -Werror, 0 fail).

Note: a wolfSSL self-test that exercises the BC top-level encoding in
isolation still requires either a hand-crafted (~4 KB) DER fixture or
a runtime BouncyCastle dependency, neither of which are appropriate
for the in-tree test suite right now. The integration test tooling
under /tmp/ejbca-interop/ remains the reproducer.
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