Skip to content

More Fenrir fixes - #46

Open
Frauschi wants to merge 9 commits into
wolfSSL:mainfrom
Frauschi:fenrir_scep
Open

Frauschi wants to merge 9 commits into
wolfSSL:mainfrom
Frauschi:fenrir_scep

Conversation

@Frauschi

Copy link
Copy Markdown
Member

Problem

A batch of Fenrir findings against the SCEP client and test server, the server's bind and TLS configuration, and the two CLIs:

  • Unenveloped GetCertInitial (F-11063): the test server ignored a de-enveloping failure for messageType 20 and matched the pending entry by transactionID alone, so a signed poll with absent or garbage content could drain a parked request and trigger issuance.
  • Issuance failure reply (F-12110): a CSR with a broken self-signature is only caught inside wolfcert_ca_issue, and that branch answered with a plain HTTP 400. The client got no signed CertRep, transactionID, recipientNonce or failInfo, and saw a PKI rejection as a transport error.
  • AES-128-CBC not enforced (F-12894): RFC 8894 section 2.9 makes AES128-CBC mandatory-to-implement, but a wolfSSL built with NO_AES_128, NO_AES_CBC or NO_AES_DECRYPT still configured with SCEP on. The client then refused an explicit AES-128 request and in AUTO mode sent 3DES to a CA advertising AES, and the server silently dropped to 3DES.
  • transactionID encoding (F-12895): the GetCertInitial entry points put any caller-supplied transactionID on the wire tagged as a PrintableString, including _, control bytes and NUL.
  • senderNonce length (F-12896): the server only checked that a senderNonce was present, then echoed it back as the recipientNonce, so an 8- or 17-byte nonce was processed, possibly issued, and answered with a malformed CertRep.
  • bind_host fallback (F-9813): wolfcert_server_start fell back to INADDR_ANY whenever inet_pton rejected bind_host, so --listen localhost:8080, an IPv6 literal or a typo bound every IPv4 interface.
  • PHA outside EST (F-11068): with tls_post_handshake_auth set, the handshake completes without a client certificate and only the EST enroll handler requests one later. A SCEP server with a client CA and PHA on therefore served clients that never presented a certificate.
  • Secrets in argv (F-12911, F-12912): wolfcert-server kept the Basic password and --challenge, and wolfcert-client kept --pass and --challenge, in argv for the life of the process, readable through ps or /proc/<pid>/cmdline.

Fix

  • scep: require a pkcsPKIEnvelope on GetCertInitial: every PKIOperation whose envelope does not decrypt to the CA now takes the signed FAILURE CertRep path, polls included.
  • scep: answer a CA issuance failure with a CertRep FAILURE: the server replies with pkiStatus FAILURE and failInfo badRequest and closes the connection, for both PKCSReq/RenewalReq and the first poll of a parked request.
  • scep: require AES-128-CBC for SCEP builds: check_config.h, configure.ac and CMake all reject such a wolfSSL when SCEP is enabled. The AES-128 guards in the client cipher selection and the server's 3DES branch are gone. The client's 3DES fallback for a CA that does not advertise AES is unchanged.
  • scep: reject a non-PrintableString transactionID on GetCertInitial: scep_build_txid, which the one-shot and both session paths share, validates an inherited ID and returns WOLFCERT_ERR_BAD_ARG before any network I/O. Generated IDs are hex and unaffected.
  • scep: reject a senderNonce that is not 16 bytes: handle_pki_op answers any other length with HTTP 400, ahead of de-enveloping, issuance and any pending-queue change.
  • Reject a bind_host that is not a numeric IPv4 address: bind_host is parsed before any allocation, so a bad address also no longer generates and persists a CA before the start fails. Only a NULL bind_host binds all interfaces. The unused cfg_bind_host copy is gone, and the --listen help says ADDR:PORT.
  • Reject post-handshake auth for non-EST server protocols: wolfcert_server_start returns WOLFCERT_ERR_BAD_ARG for PHA with any protocol other than EST.
  • Scrub server CLI secrets from argv after parsing: parse_basic zeroes the password half of its argument after copying it, and --challenge is copied before its optarg is zeroed. Both copies are wiped before they are freed, including when a flag is repeated, and a failed copy of --challenge exits instead of starting the server without the challenge check.
  • Scrub client CLI credentials from argv after parsing: a new opt_secret() helper copies --pass and --challenge, zeroes the argv original and wipes any earlier copy on a repeat. opts_free() wipes and frees both.

Closes F-9813, F-11063, F-11068, F-12110, F-12894, F-12895, F-12896, F-12911, F-12912.

Behaviour changes for callers

  • A wolfSSL built with NO_AES_128, NO_AES_CBC or NO_AES_DECRYPT now fails configure and the check_config.h gate when SCEP is enabled. Build with --disable-scep (CMake -DWOLFCERT_ENABLE_SCEP=OFF) to keep using such a wolfSSL.
  • WolfCertServerCfgSrv.bind_host must be NULL or a numeric IPv4 address. A hostname or IPv6 literal now fails with WOLFCERT_ERR_BAD_ARG instead of binding every interface, and so does wolfcert-server --listen localhost:PORT.
  • tls_post_handshake_auth is accepted only with WOLFCERT_PROTO_EST.
  • wolfcert_scep_get_cert_initial() and the session GetCertInitial calls return WOLFCERT_ERR_BAD_ARG for a transactionID outside the PrintableString repertoire.
  • The SCEP test server rejects a senderNonce that is not 16 bytes, and answers a failed issuance with a FAILURE CertRep rather than HTTP 400.

Tests

  • scep_poll_roundtrip: an unenveloped and a malformed GetCertInitial for a pending transaction both get FAILURE/badRequest, and the real poll still succeeds afterwards. The long-ID poll case now fills with A instead of 0x11, and new cases refuse a transactionID holding _, 0x11 or NUL on the one-shot API.
  • scep_roundtrip: a CSR with one signature bit flipped gets a FAILURE CertRep. check_required_attrs posts an 8-byte and a 17-byte senderNonce and expects both to be rejected.
  • scep_async_roundtrip: the long-ID case fills with A instead of 0x11, and a transactionID holding _ is refused on the blocking session API.
  • server_ca_store unit: localhost, ::1, an empty string, 127.0.0.300 and an address with a trailing space are each refused with WOLFCERT_ERR_BAD_ARG.
  • est_pha_roundtrip: a SCEP server with PHA enabled fails to start.
  • cli_proto_scoping.sh: a server started with --basic and --challenge, and a client parked on a --trust FIFO after option parsing, show neither secret in ps output.

@Frauschi Frauschi self-assigned this Sep 30, 2026
Copilot AI balanced review requested due to automatic review settings September 30, 2026 15:12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟢 Approval recommended

The security fixes are coherent across implementation, configuration, documentation, and regression tests, with no unresolved correctness issues found.

Review effort: Balanced
Findings: None

What changed in this PR

This PR addresses Fenrir findings across SCEP validation/error handling, server configuration, and CLI credential hygiene.

Changes:

  • Enforces valid SCEP envelopes, attributes, transaction IDs, and AES-128-CBC support.
  • Rejects invalid bind addresses and non-EST post-handshake authentication.
  • Scrubs CLI secrets from process arguments and adds regression coverage.
File Description
wolfcert/​server.h Documents bind-address and PHA restrictions.
wolfcert/​scep.h Documents transaction ID validation.
wolfcert/​check_config.h Enforces AES-128-CBC for SCEP builds.
tests/​unit/​test_server_ca_store.c Tests invalid bind hosts.
tests/​integration/​test_scep_roundtrip.c Tests nonce lengths and issuance failures.
tests/​integration/​test_scep_poll_roundtrip.c Tests malformed polls and transaction IDs.
tests/​integration/​test_scep_async_roundtrip.c Tests session transaction ID validation.
tests/​integration/​test_est_pha_roundtrip.c Tests PHA protocol scoping.
tests/​integration/​cli_proto_scoping.sh Tests argv secret scrubbing.
tests/​CMakeLists.txt Adds internal headers to the poll test.
src/​server.c Validates bind hosts and PHA usage.
src/​scep/​scep_server.c Tightens request validation and signed failure replies.
src/​scep/​scep_client.c Validates transaction IDs and requires AES support.
src/​internal.h Removes the obsolete copied bind host.
Makefile.am Adds the poll test's internal include path.
docs/​ARCHITECTURE.md Documents EST-only PHA behavior.
configure.ac Adds the Autoconf AES capability gate.
CMakeLists.txt Adds the CMake AES capability gate.
cli/​wolfcert_server.c Scrubs server secrets and clarifies listen syntax.
cli/​wolfcert_client.c Copies, scrubs, and securely frees client secrets.
CLAUDE.md Documents the SCEP AES requirement.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@Frauschi

Copy link
Copy Markdown
Member Author

@wolfSSL-Fenrir-bot review balanced

@wolfSSL-Fenrir-bot wolfSSL-Fenrir-bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fenrir Automated Review — PR #46

Scan targets checked: wolfcert-src, wolfcert-bugs
Coverage: 9 of 12 in-scope changed file(s) opened by the reviewer; not opened: tests/unit/test_server_ca_store.c, wolfcert/scep.h, wolfcert/server.h

Fenrir result: Approved ✅

No new issues found in the changed files.

Advisory only — this automated result does not count as a GitHub approval.

Review tier: Balanced

@yosuke-wolfssl yosuke-wolfssl left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most of the fixes look right to me. The branch builds without warnings, and ctest passes 30/30 on macOS.

Main points, inline below:

  • GetCertInitial (F-11063): a poll still finds its parked request by transactionID alone. Requiring the envelope stops a poll with absent or garbage content, but not a valid envelope signed by a different key.
  • parse_basic(): a failed copy starts the server with Basic auth weakened or off. --challenge already exits on the same failure.
  • PrintableString: the new check covers one client path; enc_printable_n() writes the 0x13 tag for every attribute.
  • Issuance failure on a poll: the case the commit message names is untested, and SCEP parks a CSR without checking its signature.
  • AES-128-CBC requirement: no negative-configure case, the user-facing docs do not mention it, and WOLFCERT_NO_WOLFSSL_FEATURE_CHECK now skips it with no fallback.
  • bind_host test: it cannot see the CA-persist ordering the commit message describes.
  • Nits: a bare 16, a stale test comment, a long line.

Not on diff lines:

  • The HAVE_AES_CBC / WOLFSSL_AES_128 guards in tests/integration/test_scep_roundtrip.c (lines 424, 1067, 1419, 1510, 1586, 1638) and tests/unit/test_scep_msg.c (1954) are now always true in a SCEP build. So is the reason given at test_scep_roundtrip.c:422, "only exist when wolfSSL can supply one".
  • wolfcert_server_free() frees cfg_basic_pass and cfg_challenge without wiping them. With the CLIs now wiping their own copies, those are the copies left in the heap.
  • wolfcert-server reports a rejected --listen host only as "start failed (Bad argument)"; the bind_host message reaches stderr only with WOLFCERT_LOG set. --listen localhost:PORT used to start, so the error deserves a hint.
  • The --tls-post-handshake-auth help text still lists only the TLS requirements, not --proto est.
  • Test cases worth adding:
    • a GetCertInitial with a valid envelope, signed by a different key
    • a bad-signature CSR parked on the approval server, then polled
    • a transactionID using the allowed punctuation (-, :, .), and one with @ or a byte above 0x7F
    • a start with bind_host = NULL

Comment thread src/scep/scep_server.c
if (rc != WOLFCERT_OK && strcmp(mt, "20") != 0) {
/* Decryption matters for 19/17 (CSR inside); for 20 the payload
* is IssuerAndSubject which the server matches by txid anyway. */
if (rc != WOLFCERT_OK) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This stops a poll whose content is absent or is not an envelope. handle_get_cert_initial() still finds the entry with pending_find(p, tid, tid_len) and never looks at the poll's signer, and anyone can build a valid envelope because the CA certificate is public.

I checked it on this branch. After the PKCSReq in poll_path(), a wolfcert_scep_get_cert_initial() with r1.transaction_id, a fresh RSA key and that key's own CSR drains the entry and issues. The caller cannot decrypt the reply, which is enveloped to the stored signer, but the real poll at test_scep_poll_roundtrip.c:203 then gets badCertId.

handle_enroll() already has the rule. Passing signer_cert into handle_get_cert_initial() and checking it before pending_remove() would tie the poll to its request:

    if (signer_cert == NULL ||
            signer_matches_csr(signer_cert, signer_cert_len,
                               e->csr_der, e->csr_len, s->heap) != WOLFCERT_OK)
        return send_pki_failure(s, fd, tid, tid_len, snonce, snonce_len,
                                "4" /* badCertId */);

On a mismatch the entry stays queued. That poll would make a good third case next to the two unenveloped ones.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c0f64f6: the poll's signer must match the parked CSR's key (signer_matches_csr()), else badCertId and the entry stays. Your foreign-key poll is now a test case.

Comment thread cli/wolfcert_server.c
free(*user);
free_secret(*pass);
*user = strndup(arg, (size_t)(colon - arg));
*pass = strdup(colon + 1);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If either copy fails, the server still starts. A NULL user makes check_basic_auth() admit every request, and a NULL pass makes user: the accepted credential. The --challenge case exits on the same failure, so the two now disagree:

    *user = strndup(arg, (size_t)(colon - arg));
    *pass = strdup(colon + 1);
    wc_ForceZero(colon + 1, (word32)strlen(colon + 1));
    if (*user == NULL || *pass == NULL)
        return -1;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f11ae82: a failed copy of either half exits, like --challenge.

Comment thread src/scep/scep_client.c Outdated

/* X.680 PrintableString repertoire, which the transactionID attribute is typed
* as (RFC 8894 section 3.2.1.1). */
static int scep_is_printable_string(const uint8_t* s, size_t len)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing in wolfCert checked the PrintableString characters before this, and wolfSSL has no public helper for it, so the check is needed. But it guards one caller, and enc_printable_n() in src/scep/scep_msg.c is where every 0x13 attribute is written. Checking there would also cover the test server, which copies the request's transactionID into its CertRep unchecked. wolfcert_scep_build_pki_message() runs before the POST, so the rejection would still come before any network I/O, as long as it maps to WOLFCERT_ERR_BAD_ARG.

The CSR subject has the same gap, outside this PR: wolfcert_csr_build() with C=U_,serialNumber=ab_c@1 writes both as PrintableString. wolfSSL defaults countryEnc and serialDevEnc to CTC_PRINTABLE, and parse_subject_dn() checks only length. One helper in src/internal.h could serve both.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved in d96b045: enc_printable_n() now checks every 0x13 attribute via wolfcert_is_printable_string() in internal.c, returning WOLFCERT_ERR_BAD_ARG before the POST. The test server rejects a bad transactionID with 400. Added the punctuation, @ and 0x80 cases. CSR subject is noted for a follow-up; the helper is ready for it.

Comment thread src/scep/scep_server.c

/* Closed by the non-OK return; the flag is for the header. */
s->keep_alive = 0;
send_rc = send_pki_failure(s, fd, tid, tid_len, snonce, snonce_len,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two things on the poll path:

  • Untested. check_bad_csr_sig() runs against the server without approval, so it reaches this branch only through handle_enroll(). A bad-signature CSR parked on the approval server in test_scep_poll_roundtrip.c, then polled, would cover the case the commit message names. The first poll should get FAILURE/badRequest, and a second poll badCertId.
  • Parked without checking. That case exists because handle_enroll() parks a CSR without checking its signature, so the client is told PENDING for a request that cannot succeed. Various hardening and fixes #45 makes the EST server verify a CSR before parking it. Doing the same here would refuse it on the PKCSReq instead.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went with verifying before parking (5d8a236): a bad-signature CSR now gets FAILURE/badRequest on the PKCSReq. The poll test checks that, plus badCertId on a later poll since nothing was parked.

Comment thread tests/unit/test_server_ca_store.c Outdated
WolfCertServerCfgSrv cfg;
WolfCertServer* srv = NULL;

ca_store_cfg(&cfg, NULL);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With no store configured, this checks only the return code. If the inet_pton() check moved back below CA generation, this test would still pass. The commit message's claim, that a bad address no longer generates and persists a CA, needs a store here and a check that nothing was written to it.

Every server in the suite sets bind_host, so the NULL branch, now the only way to bind all interfaces, is never run. One start with bind_host = NULL would cover it.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 05403a4: bad addresses now run against a memory store and assert nothing was written (verified by moving the check below CA generation), plus a bind_host = NULL start.

Comment thread src/scep/scep_server.c Outdated
if (tid == NULL || tid_len == 0 || snonce == NULL || snonce_len == 0 ||
/* RFC 8894 section 3.2.1 requires all three in every message, with a
* 16-byte senderNonce, or no CertRep could answer it. */
if (tid == NULL || tid_len == 0 || snonce == NULL || snonce_len != 16 ||

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: scep_client.c names this SCEP_NONCE_SZ. Moving that into src/internal.h and using it here would tie the server's check to the size the client generates.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, SCEP_NONCE_SZ is in internal.h now (3f4f883).

Comment thread tests/integration/test_scep_roundtrip.c Outdated
memset(snonce, 0x22, sizeof(snonce));
memset(snonce_long, 0x33, sizeof(snonce_long));

/* Each round omits one required attribute; the last is the control that

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rounds 6 and 7 send every attribute, with a senderNonce of the wrong length, so "omits one required attribute" no longer holds. Perhaps:

    /* Each round omits or mis-sizes one required attribute; the last is the
     * control that proves this raw-POST harness reaches the issuance path. */

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Took your wording.

Comment thread wolfcert/check_config.h
/* RFC 8894 section 2.9 makes AES128-CBC mandatory-to-implement for SCEP. */
#if defined(WOLFCERT_HAVE_SCEP) && (!defined(WOLFSSL_AES_128) || \
!defined(HAVE_AES_CBC) || !defined(HAVE_AES_DECRYPT))
#error "wolfCert SCEP requires AES-128-CBC (RFC 8894). Rebuild wolfSSL without NO_AES_128, NO_AES_CBC or NO_AES_DECRYPT, or drop WOLFCERT_HAVE_SCEP."

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  • No negative-configure case. scripts/ci/assert-configure-fails.sh has no-rsa-scep for the RSA rule. A matching row for a wolfSSL built with -DNO_AES_128 would show that both build systems keep rejecting it.
  • Docs. Only CLAUDE.md records the requirement. docs/EMBEDDED.md:38 still lists just "AES", and neither examples/user_settings.h.example nor the WOLFCERT_SCEP help in zephyr/Kconfig mentions it. Embedded builds are the ones most likely to drop AES-128.
  • Opt-out. This #error sits inside the WOLFCERT_NO_WOLFSSL_FEATURE_CHECK block, and the source guards it replaces are gone. An opted-out build without AES-128 now compiles; the client then fails inside the PKCS#7 encoder, and the test server advertises AES and SCEPStandard. Is that intended, or should this check sit outside the opt-out like the RSA rule at line 71?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the no-aes128-scep negative case (both build systems reject it) and documented the requirement in EMBEDDED.md, user_settings.h.example and the Zephyr Kconfig (b1f26d8). The opt-out is intended: it behaves like the NO_AES check above it, and in Tier 1 it would misfire whenever wolfSSL's options are unresolved.

Comment thread wolfcert/server.h Outdated
* checked against `tls_client_ca_pem`, which it requires.
* wolfcert_server_start() returns WOLFCERT_ERR_UNSUPPORTED without
* checked against `tls_client_ca_pem`, which it requires. Other protocols
* reject it with WOLFCERT_ERR_BAD_ARG. wolfcert_server_start() returns WOLFCERT_ERR_UNSUPPORTED without

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this line runs to about 105 columns, where the rest of the header wraps at 80. docs/ARCHITECTURE.md:169 has the same issue.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrapped, along with ARCHITECTURE.md (fbc2f24).

The test server ignored a de-enveloping failure for messageType 20 and
dispatched the poll anyway, matching the pending entry by transactionID
alone. A signed GetCertInitial with absent or non-envelope content could
therefore drain a parked request and trigger issuance. Route every
PKIOperation whose envelope does not decrypt to the CA through the signed
FAILURE CertRep path, as RFC 8894 requires the pkcsPKIEnvelope for all of
them.

The envelope alone does not bind the poll to its request: the CA
certificate is public, so anyone can build one. The poll's signer must
now carry the same public key as the parked CSR, the rule PKCSReq
already enforces. A mismatch gets FAILURE/badCertId and leaves the entry
queued.

The poll roundtrip test now posts an unenveloped, a malformed and a
validly enveloped GetCertInitial signed by a different key for a pending
transaction, expects each to be refused, and checks the legitimate poll
still succeeds afterwards.

Fixes F-11063.
The server parses the CSR without verification for its SPKI and
challenge checks, so a CSR with a broken self-signature is only
refused inside wolfcert_ca_issue. That branch replied with a plain
HTTP 400, leaving the client without a signed CertRep, its
transactionID, recipientNonce or failInfo, and surfacing a PKI
rejection as a transport error.

Reply with pkiStatus FAILURE and failInfo badRequest instead and close
the connection, matching the other rejection paths. The integration
test enrolls with a CSR whose signature has one bit flipped and
expects the FAILURE CertRep.

A server that requires approval used to park such a CSR and answer
PENDING for a request that could never issue. It now verifies the CSR
signature before parking and refuses it on the PKCSReq with the same
FAILURE/badRequest, as the EST server does. The poll test sends a
bad-signature PKCSReq to the approval server, expects FAILURE, and
then a poll for its transactionID gets badCertId since nothing was
parked.

Fixes F-12110.
RFC 8894 section 2.9 makes AES128-CBC mandatory-to-implement, but the
build only rejected NO_AES. A wolfSSL built with NO_AES_128, NO_AES_CBC
or NO_AES_DECRYPT still configured with SCEP enabled. The client then
refused an explicit AES-128 request and, in AUTO mode, sent 3DES even to
a CA advertising AES, while the test server silently dropped to 3DES and
stopped advertising SCEPStandard.

Reject such a wolfSSL whenever SCEP is enabled, in check_config.h,
configure.ac and CMake alike, and drop the AES-128 guards from the client
cipher selection and the 3DES branch from the server. The 3DES fallback
for a CA that does not advertise AES is unchanged. The check_config.h
rule sits with the other wolfSSL feature checks, so
WOLFCERT_NO_WOLFSSL_FEATURE_CHECK skips it as it skips NO_AES.

A new neg-no-aes128 wolfSSL config and no-aes128-scep case in
assert-configure-fails.sh keep both build systems rejecting it.
EMBEDDED.md, user_settings.h.example and the Zephyr WOLFCERT_SCEP help
state the requirement, and the SCEP tests lose the AES-128 guards that
are now always true.

Fixes F-12894.
RFC 8894 types the transactionID attribute as a PrintableString, and the
signed-attribute encoder tags it 0x13 without looking at its bytes. The
GetCertInitial entry points accepted any caller-supplied byte sequence,
so a value holding characters such as '_', control bytes or NUL went on
the wire as a malformed PrintableString. The test server likewise copied
a request's transactionID into its CertRep unchecked. The IDs the
library generates are hex and always conform, so only an inherited or
peer-supplied ID can trip this.

Check the repertoire in enc_printable_n, which writes every 0x13
attribute (messageType, transactionID, pkiStatus, failInfo), and return
WOLFCERT_ERR_BAD_ARG from wolfcert_scep_build_pki_message. That runs
before the POST, so the one-shot and both session GetCertInitial paths
still fail before any network I/O. The check lives in
wolfcert_is_printable_string in internal.c so the CSR subject can reuse
it. The test server rejects a non-PrintableString transactionID with
HTTP 400 alongside the other required-attribute checks.

The existing long-ID poll tests filled their buffers with 0x11 and now
use 'A', and the unit and raw-POST tests that used binary transactionIDs
now use hex digits. New cases cover '_', '@', 0x11, 0x80 and NUL on the
one-shot API, '_' on the blocking session API, and a transactionID made
of the allowed punctuation, which reaches the server.

Fixes F-12895.
The test server checked only that a PKIOperation carried a non-empty
senderNonce, and the parser keeps whatever length the request supplied.
Every dispatch path hands that nonce to send_cert_rep, which echoes it as
the CertRep recipientNonce, so a signed request with a short or long
senderNonce was processed, possibly issued, and answered with a malformed
recipientNonce. RFC 8894 section 3.2.1 fixes the senderNonce at 16 bytes.

Extend the required-attribute check in handle_pki_op to reject any
senderNonce whose length is not 16 with HTTP 400, ahead of de-enveloping,
issuance and any pending-queue change.

check_required_attrs in the SCEP roundtrip test now posts an 8-byte and
a 17-byte senderNonce and expects both to be rejected.

Fixes F-12896.
wolfcert_server_start fell back to INADDR_ANY whenever inet_pton could
not parse bind_host, so a hostname such as localhost, an IPv6 literal,
an empty string or a mistyped address made the server listen on every
IPv4 interface instead of the one the operator asked for. The CLI
passes --listen through unchanged, so --listen localhost:8080 exposed
the enrollment endpoint externally.

Parse bind_host before any allocation and return WOLFCERT_ERR_BAD_ARG
when it is not a numeric IPv4 address. Only a NULL bind_host now binds
all interfaces. Checking up front also stops a bad address from
generating and persisting a CA before the start fails.

The wolfcert-server --listen help now says the address must be
numeric IPv4, and a failed start prints the library's error detail, so
--listen localhost:PORT names the reason instead of only "Bad
argument".

The unit test starts each bad address against a memory store and
checks no CA was written to it, and starts once with a NULL bind_host.
cli_proto_scoping.sh checks the refusal reason reaches stderr.

Fixes F-9813.
With tls_post_handshake_auth set, tls_setup drops
WOLFSSL_VERIFY_FAIL_IF_NO_PEER_CERT so the handshake can complete
anonymously and the client certificate is requested later. Only the EST
enrollment handler ever requests and checks that certificate. A SCEP
server started with a client CA and PHA enabled therefore served clients
that never presented a certificate, silently turning the configured
mutual TLS into optional client auth.

wolfcert_server_start now rejects tls_post_handshake_auth with
WOLFCERT_ERR_BAD_ARG for any protocol other than EST. The header,
ARCHITECTURE.md and the wolfcert-server --tls-post-handshake-auth help
document the restriction.

Fixes F-11068.
wolfcert-server took the EST Basic password and the SCEP challenge
password on the command line and left both in argv for the life of the
process. The challenge was used directly as the optarg pointer, and
parse_basic copied the password without touching the original argument,
so any local user able to read process arguments through ps or
/proc/<pid>/cmdline could recover them.

parse_basic now zeroes the password half of its argument after copying
it, and --challenge is copied with strdup before its optarg is zeroed.
Both copies are wiped before they are freed. A failed copy of either
option exits instead of starting the server without it: a NULL Basic
user would have turned Basic auth off, and a NULL password would have
accepted "user:". wolfcert_server_free also wipes the library's own
copies of the Basic password and the challenge before freeing them.

A new group in cli_proto_scoping.sh starts the server with both options
and checks that neither secret appears in ps output once it is
listening.

Fixes F-12911.
wolfcert-client kept --pass and --challenge as direct optarg pointers,
so the EST Basic password and the SCEP challenge password stayed in
argv for the whole run. Enrollment with SCEP pending polling or an EST
Retry-After wait can keep the client alive for minutes, during which any
local user able to read process arguments through ps or
/proc/<pid>/cmdline could recover them.

parse_common now copies both values through a new opt_secret helper,
which zeroes the argv original and wipes any earlier copy when the flag
is repeated. opts_free wipes and frees the copies. A new group in
cli_proto_scoping.sh parks the client on a --trust FIFO after option
parsing and checks that neither secret appears in its ps output.

Fixes F-12912.
@Frauschi

Frauschi commented Oct 1, 2026

Copy link
Copy Markdown
Member Author

Thanks! All addressed and folded into the owning commits. Off-diff items: AES test guards removed (b1f26d8), wolfcert_server_free() wipes the secrets (f11ae82), a failed --listen now prints the reason (05403a4), PHA help mentions --proto est (fbc2f24).

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.

5 participants