Skip to content

wolfSCEP interop gaps, and scope the server config per protocol - #14

Merged
philljj merged 11 commits into
wolfSSL:mainfrom
Frauschi:scep-wolfscep-compat
Aug 3, 2026
Merged

wolfSCEP interop gaps, and scope the server config per protocol#14
philljj merged 11 commits into
wolfSSL:mainfrom
Frauschi:scep-wolfscep-compat

Conversation

@Frauschi

@Frauschi Frauschi commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Close the wolfSCEP interop gaps, and scope the server config per protocol

Three wolfSCEP deployment shapes were unreachable: a multi-CA responder needs the CA identifier in message= on GetCACaps and GetCACert (RFC 8894 section 3.5.2), wolfSCEP derives the transactionID from the signer public key so retries of one key share an ID, and no GetCACaps keyword advertises AES-256, so a peer requiring it could not be asked. This adds those three options, then reshapes WolfCertServerCfg around the fact that a connection is either EST or SCEP and never both. Two bugs surfaced along the way and are fixed here.

It also ships docs/MIGRATING-FROM-WOLFSCEP.md, since this is the change that makes wolfCert a viable target for an existing wolfSCEP integration, and adds the one option that writing the guide showed was still missing.

11 commits, 28 files, +1857/-216. Each commit builds clean and passes the full suite on its own, so it is worth reading in order.

API changes

Backward compatibility was deliberately waived. The per-protocol fields move into a proto_opts union of WolfCertEstServerOpts and WolfCertScepServerOpts, selected by the existing protocol field. Both arms are zero-init safe. The union member is named rather than anonymous, because anonymous unions are C11 and the public headers must stay usable at -std=c99 on embedded toolchains.

Before After
srv.allow_post_handshake_auth srv.proto_opts.est.allow_post_handshake_auth
srv.auto_csrattrs srv.proto_opts.est.auto_csrattrs
srv.username / srv.password srv.proto_opts.est.username / .password
n/a (new) srv.proto_opts.scep.ca_id
n/a (new) srv.proto_opts.scep.txid_mode
n/a (new) srv.proto_opts.scep.content_cipher
n/a (new) srv.proto_opts.scep.renewal_msg_type

One behaviour change to be aware of. An https:// SCEP endpoint with verify_server off used to complete a silent anonymous handshake on the one-shot calls; only the session open refused it. All six one-shot entry points now apply the same gate, so that combination returns WOLFCERT_ERR_TLS instead of connecting. verify_server is the transport's only peer-verification switch, so the old behaviour was an unauthenticated TLS session that looked like a secure one.

HTTP Basic is now EST-only: RFC 8894 authenticates inside the pkiMessage and defines nothing at the HTTP layer, so the SCEP entry points no longer send credentials. Both interop targets agree, using --challenge rather than HTTP auth.

One migration hazard: est.username and scep.ca_id are both const char* at offset 0 of their arms, so rewriting .username to .proto_opts.est.username while leaving protocol = WOLFCERT_PROTO_SCEP compiles silently and puts the username into a GetCACaps URL. protocol is the discriminator and the library always reads the matching arm; the CLI rejects the combination outright.

Bugs fixed

  • The keep-alive EST session never sent the credentials it was given. Its request builders left WolfCertHttpRequest.basic_user unset, so moving from the one-shot calls to a session silently dropped the Authorization header. It now copies the pair at open, replays it on every request in both the blocking and async forms, and zeroizes both halves on close.
  • The teardown after a failed transport open leaked those copies and never scrubbed the password. EST requires server verification, so any DNS failure, refused connection or untrusted certificate lands there, and a retry loop stranded one plaintext copy per attempt.
  • wolfcert_client_enroll read an inactive union member, testing auto_csrattrs before the protocol. Harmless today only because the EST arm is longer than the SCEP arm; two more SCEP fields would have made a SCEP option switch on /csrattrs discovery.

A second Skoll pass over the finished branch turned up five more, all fixed here: the CA identifier never reached GetNextCACert, the one-shot https:// gate above, the explicit AES cipher arms lacked the build guard their 3DES neighbour has, basic_auth_header left the cleartext credential intact on its allocation-failure path, and the new test's capture thread had an unterminated strstr, a libc free() of wolfCert-heap memory and no receive timeout.

Fenrir's automated review on the PR caught three more, all folded into the commits that introduced them: a DecodedCert left unfreed on the wc_ParseCert failure path of a test helper, and two gaps in the credential scrubbing above. basic_auth_header writes into the caller's stack buffer with snprintf before checking for truncation, so its error path left a partial credential behind; and the scrub in both callers only ran once the request head was built, so the basic_auth_header failure and header-allocation failure exits bypassed it, the latter with the complete credential still in the buffer. Every exit clears it now, and so does the assembled request head, on the truncated-head path, after the one-shot write, and in sm_reset. What is left is wolfSSL's own write buffer, which is its concern rather than wolfCert's.

Two smaller ones from the first review: basic_auth_header freed the literal user:pass buffer and its base64 form without clearing them, and the SCEP transactionID lost its wc_ForceZero on the RNG-failure path.

Also in here

A shared wolfcert_hex_encode replaces three copies of the same table, the transactionID inputs collapse from three scep_prepare arguments into one selector with the random draw moved inside the branch that consumes it, and the DecodedCert behind the public-key derivation moves off the stack (2384 bytes).

The CLI gains --ca-id, --txid-mode random|pubkey and --content-cipher auto|aes128|aes256|des3, and now rejects an option belonging to the other protocol in both directions. --pha and --csrattrs-auto used to be accepted and ignored under SCEP. --challenge stays unscoped, since a challengePassword is legitimate in an EST CSR when /csrattrs asks for one.

Migration guide

docs/MIGRATING-FROM-WOLFSCEP.md maps the wolfSCEP calls onto wolfCert and covers the differences that bite. The framing matters more than the table: wolfSCEP is HTTP transport, building and parsing no pkiMessages at all, so every integration on it carries its own wc_PKCS7 code for the EnvelopedData, the SignedData wrapper and the signed attributes, plus its own CertRep parse, fingerprint check and polling loop. A migration therefore deletes the cryptographic message layer, not just the plumbing, and the guide leads with that.

Four things need a decision from the reader: an MD5 CA fingerprint has to be re-pinned (wolfCert offers SHA-1, SHA-256 and SHA-512 and refuses a 16-byte value), the CA identifier is no longer sent by default where wolfSCEP always sent one defaulting to the literal CAIdentifier, the transactionID is random unless txid_mode asks for the public-key derivation, and the content cipher is caps-driven so an AES-256 peer has to be told.

Writing it turned up a fifth, which is fixed here rather than documented as a caveat. An enrollment request pairs a messageType with a signing certificate, and RFC 8894 pairs them as 19 with a throwaway self-signed cert or 17 with the certificate being replaced. CAs predating RenewalReq expect 19 signed by the old certificate, and wolfCert had no way to send it. proto_opts.scep.renewal_msg_type now selects it, defaulting to the RFC form, with WolfCertScepCaps.renewal as the way to tell which a CA wants.

Testing

New coverage: the pubkey-hash transactionID (recomputed from the CSR, plus determinism across two enrollments), AES-128 and AES-256 content ciphers, the GetCACaps and GetCACert URL builder, the hex helper, HTTP Basic over both the blocking and async EST session, and the renewal messageType read off the wire.

That last one needs a loopback capture rather than a round trip, because the in-tree server routes 19 and 17 through the same handler: the test parses the POSTed pkiMessage with wolfcert_scep_parse_pki_message and asserts the attribute. Forcing the helper to return 17 unconditionally fails it, so it discriminates.

  • The full suite on every one of the eleven commits, checked before each was recorded.
  • -DWOLFCERT_WERROR=ON with -Wall -Wextra -Wshadow -Wpedantic: zero warnings. ASan and UBSan: clean.
  • The credential leak proven both ways with macOS leaks: 50 failed session opens give 0 leaks after the fix, 100 leaks for 2400 total leaked bytes in wolfcert_strdup with it backed out. Both new EST credential tests also confirmed to fail with the plumbing removed.
  • The content cipher is asserted on the wire, not just by a successful enrollment: the test intercepts the POSTed pkiMessage and reads the EnvelopedData OID, since the server de-envelops any algorithm and cannot distinguish an honoured override from an ignored one. Forcing the switch to the AUTO branch fails it.
  • CLI end to end against wolfcert-server, including --ca-id "my CA/1" captured on the wire as GET /scep?operation=GetCACert&message=my%20CA%2F1.

Interop

scep_micromdm.sh gains all three options, all strict. --content-cipher aes256 started as a non-fatal probe, because RFC 8894 defines no GetCACaps keyword for AES-256 and it was unknown whether the peer could decrypt it; a manual Interop run on 2026-07-30 answered that (micromdm accepts it), so it is a plain assertion now. est_stepca.sh gets --ca-id only, its SCEP being the same smallstep/micromdm code path.

These have now run against the real peers, not just locally: a manual Interop dispatch on 2026-07-30 passed all five jobs with nothing skipped, including --ca-id, --txid-mode pubkey and AES-256 against micromdm, and --ca-id against step-ca. A Sanitizers dispatch passed as well, which matters because it runs LeakSanitizer, unavailable on macOS where the rest of the local verification was done.

@Frauschi Frauschi self-assigned this Jul 30, 2026
@Frauschi
Frauschi force-pushed the scep-wolfscep-compat branch from 8a57a0d to c071012 Compare July 30, 2026 13:40

@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 #14

Scan targets checked: wolfcert-bugs, wolfcert-src
Findings: 2
1 finding(s) posted as inline comments (see file-level comments below)

Medium (1)

Basic-auth credential scrub bypassed on early-return paths

File: src/http.c:883
Function: http_write_request
Category: Resource leaks on error paths

The new wc_ForceZero(auth, sizeof(auth)) only runs after head is allocated and filled. If basic_auth_header returns a completed header but WOLFCERT_XMALLOC for head then fails (or basic_auth_header itself truncates and returns negative), the function returns immediately, leaving a full or partial plaintext/base64 Basic-auth credential unscrubbed on the stack. The identical pattern exists in build_head.

Recommendation: Zeroize auth on every return path after it may hold data, e.g. via a single cleanup label or by scrubbing immediately after use in basic_auth_header's caller.

Referenced code: src/http.c:883-885 (3 lines)


This review was generated automatically by Fenrir. Findings are non-blocking.

Comment thread tests/integration/test_scep_roundtrip.c
@Frauschi
Frauschi force-pushed the scep-wolfscep-compat branch from c071012 to 9de58b5 Compare July 30, 2026 18:17
Comment thread tests/integration/test_scep_roundtrip.c

@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 #14

Scan targets checked: wolfcert-bugs, wolfcert-src
Findings: 1

Low (1)

Truncated Basic-auth header left unscrubbed on the stack on overflow

File: src/http.c:869
Function: http_write_request
Category: Missing ForceZero

basic_auth_header writes the base64 credential into the caller's auth stack buffer via snprintf before checking n >= out_cap, so an overlong user/pass leaves a truncated but recoverable credential in auth. Both http_write_request and build_head return n on this path without zeroing auth, unlike every other exit this PR added scrubbing for.

Recommendation: Call wc_ForceZero(auth, sizeof(auth)) before returning when basic_auth_header reports n < 0, in both http_write_request and build_head.

Referenced code: src/http.c:869-874 (6 lines)


This review was generated automatically by Fenrir. Findings are non-blocking.

@Frauschi
Frauschi force-pushed the scep-wolfscep-compat branch from 9de58b5 to 0c92acc Compare July 30, 2026 19:15

@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 #14

Scan targets checked: wolfcert-bugs, wolfcert-src

Findings: 1
1 finding(s) posted as inline comments (see file-level comments below)

This review was generated automatically by Fenrir. Findings are non-blocking.

Comment thread tests/integration/test_scep_roundtrip.c
@Frauschi
Frauschi force-pushed the scep-wolfscep-compat branch from 0c92acc to 4b84b02 Compare July 30, 2026 20:08
Comment thread tests/integration/test_scep_roundtrip.c

@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 #14

Scan targets checked: wolfcert-bugs, wolfcert-src

No new issues found in the changed files. ✅

@Frauschi Frauschi assigned wolfSSL-Bot and unassigned Frauschi Jul 30, 2026
@Frauschi
Frauschi requested a review from philljj July 30, 2026 21:26
@philljj philljj self-assigned this Jul 30, 2026

@philljj philljj 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.

First pass, looks good so far. Just a few observations and questions.

Comment thread src/http.c Outdated
Comment thread cli/wolfcert_client.c
Comment thread tests/integration/test_scep_roundtrip.c
Comment thread src/scep/scep_client.c
Comment thread wolfcert/types.h Outdated

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.

Pull request overview

This PR closes several SCEP interoperability gaps (CA identifier propagation, transactionID derivation, and content-cipher forcing), tightens transport security behavior for SCEP over TLS, and refactors WolfCertServerCfg so protocol-specific knobs are scoped under a proto_opts union selected by protocol.

Changes:

  • Add SCEP client options: ca_id, txid_mode (random vs pubkey-hash), content_cipher (auto/aes128/aes256/des3), and renewal_msg_type (17 vs 19), with corresponding URL building and on-the-wire validation tests.
  • Reshape server/client config API to WolfCertServerCfg.proto_opts.{est|scep} and enforce protocol scoping/keyword validation in the CLI.
  • Fix and harden credential handling in HTTP/EST sessions (propagation + scrubbing/zeroization) and align one-shot SCEP TLS verification gating with session behavior.

Reviewed changes

Copilot reviewed 28 out of 28 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
wolfcert/types.h Introduces protocol-scoped option structs/enums and the proto_opts union on WolfCertServerCfg.
wolfcert/server.h Updates server-side test-server comment references for proto_opts.est.auto_csrattrs.
wolfcert/scep.h Clarifies SCEP authentication (no HTTP Basic; challengePassword in CSR) and documents proto_opts placement.
wolfcert/est.h Updates PHA configuration docs and documents HTTP Basic behavior for EST sessions via proto_opts.est.
src/client.c Fixes wolfcert_client_enroll union-member access ordering for auto_csrattrs under EST only.
src/est/est_client.c Copies Basic credentials into EST sessions, replays them on every request, and scrubs on close/error paths.
src/http.c Strengthens Basic-auth header construction and request/session header scrubbing (zeroization on all exit paths).
src/scep/scep_client.c Adds verified-TLS gate for one-shot SCEP over https, CA-id URL builder, txid derivation modes, cipher override, and renewal messageType selection.
src/internal.h Exposes new internal helpers (wolfcert_hex_encode, wolfcert_scep_build_getca_url) for white-box tests.
src/internal.c Implements shared wolfcert_hex_encode helper.
cli/wolfcert_client.c Adds SCEP flags (--ca-id, --txid-mode, --content-cipher) and enforces strict cross-protocol option scoping.
tests/unit/test_est.c Adds unit coverage for wolfcert_hex_encode.
tests/unit/test_scep_msg.c Adds unit coverage for GetCA URL building and AES128/AES256 enveloping OID selection.
tests/integration/test_est_roundtrip.c Adds end-to-end coverage that EST keep-alive sessions honor HTTP Basic credentials.
tests/integration/test_est_async_roundtrip.c Adds end-to-end coverage that async EST sessions honor HTTP Basic credentials.
tests/integration/test_est_pha_roundtrip.c Updates PHA tests to use proto_opts.est.allow_post_handshake_auth.
tests/integration/test_est_mldsa_roundtrip.c Updates EST ML-DSA roundtrip config to proto_opts.est for Basic auth.
tests/integration/test_est_csr_attrs_apply_roundtrip.c Updates csrattrs auto-apply test to set proto_opts.est.auto_csrattrs.
tests/integration/test_scep_roundtrip.c Adds end-to-end SCEP coverage for pubkey-hash txid, cipher override (wire + roundtrip), renewal messageType, GetNextCACert ca_id, and TLS verify gate.
tests/integration/cli_proto_scoping.sh Adds CLI regression coverage for protocol-specific option rejection/validation.
tests/CMakeLists.txt Registers the new CLI scoping test and marks 77 as skip for NO_MALLOC builds.
tests/interop/scep_micromdm.sh Extends micromdm interop script to exercise --ca-id, --txid-mode pubkey, and --content-cipher aes256.
tests/interop/est_stepca.sh Adds step-ca --ca-id GetCACert check.
README.md Documents new CLI SCEP knobs and points to the migration guide.
docs/MIGRATING-FROM-WOLFSCEP.md Adds a wolfSCEP-to-wolfCert migration guide and explains behavioral differences/options.
docs/INTEROP.md Updates interop matrix notes to reflect new SCEP option coverage (incl. AES-256 case).
docs/ARCHITECTURE.md Documents proto_opts union design and the new SCEP client options at a high level.
CLAUDE.md Updates project guidance to reflect SCEP content-cipher override and migration guide pointer.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread wolfcert/types.h Outdated
Comment thread src/internal.h Outdated
Comment thread tests/integration/test_scep_roundtrip.c
@philljj philljj assigned Frauschi and unassigned wolfSSL-Bot and philljj Jul 31, 2026
Frauschi added 3 commits July 31, 2026 09:45
Three wolfSCEP deployment shapes were unreachable. Add the client-side
options that cover them, all zero-init to the previous behavior.

scep_ca_id is sent as the message= query parameter on GetCACaps and
GetCACert (RFC 8894 section 3.5.2), which is how a multi-CA responder is
told which CA the request is for. wolfcert_scep_build_getca_url builds
both URLs so the percent-encoding rule lives in one place.

scep_txid_mode selects how the enrollment transactionID is derived.
WOLFCERT_SCEP_TXID_PUBKEY_HASH takes the SHA-256 of the signer
SubjectPublicKeyInfo (RFC 8894 section 3.2.1) and hex-encodes it in upper
case, matching wolfSCEP, so that repeated enrollments of one key reuse a
single transactionID and the server can recognise the retry. The default
stays a fresh random value.

scep_content_cipher forces the content encryption of the request
EnvelopedData. The caps-driven default picks AES-128-CBC when the CA
advertises AES and triple DES otherwise, but no GetCACaps keyword
advertises AES-256, so a peer that requires it can only be reached by
asking for it explicitly.
Three copies of the same 16-byte lookup table had accumulated in
scep_client.c: one in url_encode for percent-encoding, and one each in
the two transactionID builders, which differ only in letter case.

Replace them with wolfcert_hex_encode next to the base64 helpers in
internal.c. It writes two characters per input byte and no NUL, so it
serves both the string builders, which terminate the buffer themselves,
and the fixed-length wire fields such as the transactionID. The `upper`
flag covers the one case difference: RFC 3986 asks for upper-case
percent-encoding and wolfSCEP derives an upper-case transactionID, while
the random transactionID keeps its existing lower-case form.

The unit test checks both cases, the single-byte call shape url_encode
uses, and that nothing is written past 2 * in_len. The expected value in
the SCEP round-trip test keeps its own local table on purpose, so that
the test does not check the encoder against itself.
scep_prepare took the inherited transactionID, its length and the
derivation mode as three separate arguments, then chose between them in a
three-way branch in the middle of the function. Gather them into one
ScepTxidSel and move the choice into scep_build_txid, so the two callers
that know both halves, the one-shot round trip and the session begin,
build the selector and the rest of scep_prepare stops caring.

The random draw now happens inside the branch that consumes it, rather
than unconditionally before the branch. The senderNonce still always
comes from the RNG, so there is still exactly one wc_InitRng_ex, but an
inherited or public-key-derived transactionID no longer spends RNG output
it will not use. Doing it this way, instead of guarding the draw with a
predicate, means the buffer lives in the one branch that fills it and
cannot be read uninitialised if the guard and the branch ever disagree.

The DecodedCert behind the public-key derivation moves to the heap. It is
a little over 2 KB, which is more than an MCU task stack should carry for
a transient parse, and it takes the caller's heap hint like every other
sizeable allocation in the library.
@Frauschi
Frauschi force-pushed the scep-wolfscep-compat branch from 4b84b02 to 6d91577 Compare July 31, 2026 07:49

@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 #14

Scan targets checked: wolfcert-bugs, wolfcert-src

No new issues found in the changed files. ✅

@Frauschi Frauschi assigned philljj and unassigned Frauschi Jul 31, 2026
@Frauschi
Frauschi requested a review from philljj July 31, 2026 09:27
Comment thread src/http.c
WolfCertServerCfg had grown EST-only and SCEP-only fields side by side,
which does not match how it is used: a connection is either EST or SCEP,
never both, so at any moment roughly half the struct is dead weight that
a reader still has to reason about.

Move the per-protocol knobs into a proto_opts union of the new
WolfCertEstServerOpts and WolfCertScepServerOpts, selected by the protocol
field that already exists. Both arms stay zero-init safe, so leaving the
union untouched keeps the defaults of either protocol. The union member is
named rather than anonymous: anonymous unions are C11, and the public
headers have to stay usable at -std=c99 on embedded toolchains.

The one place that had to change beyond a rename is wolfcert_client_enroll,
the only caller that dispatches on protocol at run time. It tested
auto_csrattrs before testing the protocol, which on a SCEP config reads an
arm of the union that is not the active member. Test the protocol first.
The WOLFCERT_ERR_UNSUPPORTED that used to follow is gone with it, because
the flag now exists only on the EST arm and cannot be asked for under SCEP
in the first place.
The keep-alive EST session never sent the credentials it was given. Its
four request builders left WolfCertHttpRequest.basic_user unset, so a
caller that moved from the one-shot calls to a session silently lost the
Authorization header, even though the transport would have emitted it.
RFC 7030 section 3.2.3 lists HTTP Basic as a client authentication
mechanism, so the session has to carry it across every request on the
connection, not just the first.

The session now copies the pair at open, since the caller's
WolfCertServerCfg need not outlive it, replays it on every request in both
the blocking and the async form, and zeroizes both halves on close. The
teardown after a failed transport open goes through that same close
helper. It previously freed only the URL and the session, leaking the two
credential copies and leaving the password unscrubbed in the heap. That
path is routine rather than exotic: EST requires server verification, so
any DNS failure, refused connection or untrusted server certificate lands
there, and an enrolment retry loop would strand one plaintext copy per
attempt.

username and password move into the EST arm of proto_opts at the same
time, and the SCEP entry points stop setting them. RFC 8894 authenticates
the enrollment inside the pkiMessage, through the CMS signature bound to
the CA/RA bundle plus the PKCS#9 challengePassword, and defines nothing at
the HTTP layer. Neither SCEP interop target uses HTTP credentials either:
est_stepca.sh passes --user and --pass for EST but --challenge for SCEP,
and scep_micromdm.sh passes none at all. The CLI grows the matching guard,
so asking for --user with --proto scep is an error rather than a flag that
quietly does nothing.

Both new tests fail without the plumbing: the blocking one enrolls with
the good credentials and then requires a rejection with a wrong password,
so a session that dropped the header cannot pass both halves, and the
async one pumps /cacerts and /simpleenroll against a second Basic-only
server.
basic_auth_header assembles the credential as a literal user:pass buffer,
base64 encodes it into a second buffer, and freed both without clearing
them. Base64 is an encoding, not protection, so both were a plaintext
credential sitting in freed heap for the rest of the process lifetime.
Clear them before the free, on the encoder error path as well.

The Authorization line itself is then copied into the request head by both
the one-shot writer and the session head builder, so the caller's stack
copy is cleared once that copy has been made.

The assembled request head carries the same Authorization line, so it is
cleared too: on the truncated-head error path, after the one-shot write, and
in sm_reset, which is the only place the session's stored head is released.

What remains unscrubbed is wolfSSL's own write buffer, which holds the encoded
credential until the connection is torn down. Reaching into that is a wolfSSL
concern rather than a wolfCert one, so this stops at the last copy wolfCert
owns.
The three SCEP options had no command-line surface, so a multi-CA
responder or a peer that requires AES-256 could not be reached from
wolfcert-client at all, and neither could be reproduced by hand when an
interop script fails. Add --ca-id, --txid-mode and --content-cipher,
unprefixed like the existing --challenge, each validated against its
keyword set rather than silently falling back to a default. The commands
already pass the whole WolfCertServerCfg into the SCEP calls, so filling
the union arm is all that is needed.

check_est_only_opts becomes check_proto_only_opts and rejects in both
directions, one arm per protocol, so the SCEP options are refused under
EST exactly as --user, --pass, --pha and --csrattrs-auto are refused under
SCEP. The last two used to be accepted and then ignored.

--challenge is deliberately left unscoped. A PKCS#9 challengePassword is
SCEP's authenticator, but it is legitimate in an EST CSR as well, since
/csrattrs can ask for one, and the EST enforcement test relies on that.

The --san-* help text loses its EST tag while it is being touched: the
certificate metadata is built before the protocol dispatch, so those names
reach a SCEP CSR too.
The three SCEP options had in-tree coverage against wolfCert's own server
but none against a third-party one, which is the only place they can show
whether a real peer accepts what the client now sends.

scep_micromdm.sh gains all three. --ca-id and --txid-mode pubkey are
strict: a single-CA responder has to keep serving GetCACert when the
message= parameter appears, and it has to accept a 64-character
transactionID rather than truncating or rejecting it, which is the whole
reason that option exists. --content-cipher aes256 is a non-fatal probe.
RFC 8894 defines no GetCACaps keyword for AES-256, so AUTO can never
negotiate it and a peer that decrypts only AES-128 or triple DES is
behaving legitimately; the script reports INFO and carries on rather than
turning the nightly red over someone else's cipher support.

est_stepca.sh gets the --ca-id case only. Its SCEP is the same
smallstep/micromdm code path micromdm/scep already exercises, so repeating
the cipher and transactionID cases there would re-test one library rather
than two. Both scripts run --ca-id before their enrollment assertions,
since GetCACert verifies no CertRep and so keeps reporting even when an
enrollment regression takes the rest of the script down.

Note that only the wolfCert side of these invocations has been run here:
scepserver, scepclient and step-ca are installed by the workflow, not
available locally, so the peer-facing behavior is first exercised by the
nightly.
An enrollment request carries two things that have to agree: the messageType
signed attribute, and the certificate that signs the pkiMessage. The signer is
how the CA decides who is asking. RFC 8894 pairs them as 19 (PKCSReq) with a
throwaway self-signed certificate that proves possession of the new key, or 17
(RenewalReq) with the certificate being replaced, which the CA recognises as
its own issuance.

CAs that predate RenewalReq expect a renewal as messageType 19 signed by the
old certificate instead. wolfCert could not send that: renewal_req_ex
hardcoded 17 and pkcs_req_ex hardcodes 19 with a signer it derives from the
new key, so the combination had no arguments that produced it. A wolfSCEP
integration can emit it, because there the application builds the pkiMessage
itself, so this was the one shape that did not survive a migration.

Add proto_opts.scep.renewal_msg_type, zero-init to the RFC form and read by
the one-shot and session renewals. Only the attribute changes: the signer
stays the certificate being replaced, so nothing else in the round trip moves,
and initial enrollment is untouched. Callers can tell which form their CA
wants from WolfCertScepCaps.renewal, already parsed from the GetCACaps
Renewal keyword.

The in-tree server routes 19 and 17 through the same handler, so a renewal
that succeeds proves nothing about which type went out. The test captures the
POSTed pkiMessage on a loopback listener and reads the attribute back with
wolfcert_scep_parse_pki_message, asserting 17 by default and 19 with the
option set. Making the helper return 17 unconditionally fails the second case,
so the assertion is real.
wolfCert now matches wolfSCEP on the three behaviours that are not in RFC
8894, and on the renewal messageType, so an existing wolfSCEP integration has
somewhere to go. Nothing told those users how to get there. The guide ships
with the change that makes the claim true rather than following it.

The migration is not a rename table, because the two libraries divide the work
differently. wolfSCEP is HTTP transport: it frames each request, moves the
bytes and hands back the reply body. It builds and parses no pkiMessages at
all, so every wolfSCEP integration carries its own wc_PKCS7 code for the
EnvelopedData, the SignedData wrapper and the transactionID, senderNonce and
messageType attributes, plus its own CertRep parse, fingerprint check and
polling loop. wolfCert owns all of that. The document therefore leads with
what the caller gets to delete, which is mostly the cryptographic message
layer rather than the plumbing, and only then maps the calls.

Four differences will actually bite, and each is called out with what to do:
an MD5 CA fingerprint has to be re-pinned, because wolfCert offers SHA-1,
SHA-256 and SHA-512 and refuses a 16-byte value; the CA identifier is no
longer sent by default, where wolfSCEP always sent one defaulting to the
literal CAIdentifier; the transactionID is random unless txid_mode asks for
the public-key derivation; and the content cipher is caps-driven, so a peer
requiring AES-256 has to be told, since no GetCACaps keyword advertises it.
Renewal gets its own section, since which messageType a CA expects is not
something the reader can guess and WolfCertScepCaps.renewal is the way to find
out.

Every wolfCert and wolfSCEP symbol named in the guide was checked against the
respective headers, and the worked example plus the fingerprint, transactionID
and cipher claims were run against the in-tree SCEP server.
wolfcert_scep_session_open already declined to open an https:// endpoint with
verify_server off, because verify_server is the transport's only peer
verification switch and running without it completes a handshake that
authenticates nothing while looking like TLS. The one-shot entry points did
not: they forwarded the flag into the request and left the decision there, so
the same URL and config that the session refuses would connect anonymously and
carry the pkiMessage over it.

SCEP survives that better than most protocols, since RFC 8894 authenticates
the enrollment inside the pkiMessage, which is why plaintext http:// is
accepted here at all. But a caller who wrote https:// asked for an
authenticated channel and did not get one, and the two halves of the same API
disagreeing about it is worse than either rule on its own.

Move the check into a helper and call it from all six one-shot operations.
A caller relying on https:// with verify_server off now gets
WOLFCERT_ERR_TLS, which is the intent: the fix is the behaviour change.

The test asserts both directions, since a gate that fires unconditionally
would pass a one-sided check: unverified is refused, and the same URL gets
past the gate and fails on the network once verify_server is set.
@Frauschi
Frauschi force-pushed the scep-wolfscep-compat branch from 6d91577 to 73a5cd5 Compare August 1, 2026 12:13
@Frauschi
Frauschi requested a review from philljj August 1, 2026 12:19
@philljj
philljj merged commit c124b6f into wolfSSL:main Aug 3, 2026
21 checks passed
@Frauschi
Frauschi deleted the scep-wolfscep-compat branch August 3, 2026 07:12
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