Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 14 additions & 1 deletion .github/workflows/interop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,17 @@ jobs:
sudo apt-get install -y libssl-dev
git clone --depth 1 https://github.com/cisco/libest "$RUNNER_TEMP/libest"
cd "$RUNNER_TEMP/libest"
./configure --prefix="$RUNNER_TEMP/libest-install" --with-ssl-dir=/usr
# libest is archived and built for OpenSSL 1.x. OpenSSL 3.0 removed
# FIPS_mode()/FIPS_mode_set(), which libest still calls, so the link
# fails. Force-include a compat header that turns both into no-ops;
# doing it in a header (not -D) keeps the macro parens off gcc's
# command line. --disable-safec: libest now requires an explicit
# safec choice and we don't need the bounds-checked string library.
printf '%s\n' '#define FIPS_mode() 0' '#define FIPS_mode_set(x) 0' \
> "$RUNNER_TEMP/fips_compat.h"
./configure --prefix="$RUNNER_TEMP/libest-install" --with-ssl-dir=/usr \
--disable-safec \
CFLAGS="-g -O2 -include $RUNNER_TEMP/fips_compat.h"
make -j "$(nproc)"
make install
echo "$RUNNER_TEMP/libest-install/bin" >> "$GITHUB_PATH"
Expand All @@ -132,6 +142,9 @@ jobs:
- name: Install step-ca and step CLI
continue-on-error: true
run: |
# step-ca's cgo build links libpcsclite (PC/SC, for PIV/YubiKey
# provisioners); without its pkg-config file the build aborts.
sudo apt-get install -y libpcsclite-dev
go install github.com/smallstep/certificates/cmd/step-ca@latest
go install github.com/smallstep/cli/cmd/step@latest
- name: Run est_stepca.sh
Expand Down
38 changes: 38 additions & 0 deletions docs/INTEROP.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Third-party interoperability

wolfCert's EST (RFC 7030) and SCEP (RFC 8894) clients and test server are exercised against independent implementations under `tests/interop/`. These scripts are **hand-run and best-effort** - they are driven by the nightly `Interop` GitHub workflow (`.github/workflows/interop.yml`) but are not part of `ctest`.

## Best-effort / skip convention

Every interop script sources `tests/interop/lib/common.sh`, whose `need <bin>` helper exits **77** when a required third-party binary is missing. The workflow treats 77 as a neutral skip (a dependency that could not be installed), so a genuinely-absent tool does not fail the job. Any *other* non-zero exit is a real interop regression and fails loudly.

Because a failed dependency *build* also ends in a 77 skip (the binary never lands in `PATH`), a broken build recipe can masquerade as a green run. Keep the dependency build steps in `interop.yml` working, not just the scripts.

## Targets

| Script | Peer | What it checks |
|---|---|---|
| `openssl_pkcs7_xcheck.sh` | OpenSSL `cms`/`x509` | Lower-bound cross-check of wolfCert-produced PKCS#7 / certs. Always present. |
| `scep_micromdm.sh` | micromdm/scep (`apt install scep`) | D2 wolfcert-client -> scepserver, D1 scepclient -> wolfcert-server. |
| `est_globalsign.sh` | globalsign/est (Go) | wolfcert-client <-> globalsign estserver/estclient, both directions. |
| `est_libest.sh` | cisco/libest (built from source) | wolfcert-client -> libest estserver, libest estclient -> wolfcert-server. |
| `est_stepca.sh` | smallstep/step-ca (Go) | EST probe + SCEP enrollment against step-ca. |

## Dependency build notes

- **cisco/libest** is archived and written for OpenSSL 1.x. On a modern OpenSSL 3.0 host it needs two build tweaks (applied in `interop.yml`): `--disable-safec` (its configure now demands an explicit safec choice) and a force-included compat header defining `FIPS_mode()` / `FIPS_mode_set()` as no-ops (OpenSSL 3.0 removed both symbols, which libest still references).
- **smallstep/step-ca** links `libpcsclite` via cgo (PC/SC, for its PIV/YubiKey provisioners); install `libpcsclite-dev` before `go install`, or the build aborts at pkg-config.

## Notes

1. **micromdm D1 signed-attribute set.** `wolfcert-server`'s SCEP CertRep emits the full RFC 8894 signed-attribute set (including `recipientNonce`). On a malloc-enabled wolfSSL its PKCS#7 encoder grows the signed-attribute array on the heap past the inline `MAX_SIGNED_ATTRIBS_SZ`, so D1 passes as-is. Only a `WOLFSSL_NO_MALLOC` build needs wolfSSL rebuilt with `-DMAX_SIGNED_ATTRIBS_SZ>=9`.

2. **micromdm content encryption.** micromdm content-encrypts its `pkcsPKIEnvelope` with single DES-CBC (`1.3.14.3.2.7`), which wolfSSL compiles out by default (`NO_DES3`). The `full` CI wolfSSL build enables `--enable-des3` so `wc_PKCS7_DecodeEnvelopedData` can decode both directions.

3. **Server-side TLS is native.** The EST/SCEP test server terminates TLS itself via `--tls-cert` / `--tls-key`; no external fronting terminator is needed. Scripts mint a throwaway loopback identity for it.

4. **step-ca has no open-source EST.** RFC 7030 EST endpoints ship only in Smallstep's commercial Certificate Manager; open-source `step-ca` returns HTTP 404 on `/.well-known/est/*`. `est_stepca.sh` probes the endpoint and skips EST if absent, then exercises SCEP.

5. **step-ca needs an RSA CA for SCEP.** SCEP (RFC 8894) is RSA-only: the client encrypts the `pkcsPKIEnvelope` to the RA/CA public key with CMS key transport, and the CA decrypts it with the matching private key - neither works with an ECC key. wolfCert only builds a `KeyTransRecipientInfo` (RSA); an ECC RA cert is rejected up front with `WOLFCERT_ERR_UNSUPPORTED` (rather than failing deep in wolfSSL's encoder with `BAD_KEYWRAP_ALG_E`, `-239`). Because `step ca init` creates an **ECDSA** chain by default, `est_stepca.sh` swaps in an RSA root + intermediate (per Smallstep's own guidance) so the intermediate - which step-ca uses as the SCEP decrypter - and every cert `GetCACert` returns are RSA. The provisioner is set to AES-128-CBC (`--encryption-algorithm-identifier 1`) for the CertRep so wolfCert can decrypt it.

With the RSA chain in place the pkcsPKIEnvelope encrypts and step-ca issues the cert, but step-ca's SCEP is built on `github.com/smallstep/scep` (the micromdm library), so its CertRep carries the same `DigestInfo` signature quirk as micromdm and verifying it hits the same wolfSSL PKCS#7 gap as the micromdm interop (`VerifySignedData` / `ASN_SIG_CONFIRM_E`, `-229`). `est_stepca.sh` [2] is therefore a **strict** assertion, like the micromdm interop: it stays red until wolfSSL PR #10928 reaches `master`, then self-heals to a PASS.
12 changes: 12 additions & 0 deletions src/scep/scep_msg.c
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,18 @@ WOLFCERT_TEST_VIS int wolfcert_scep_envelop(const uint8_t* ra_cert_der,
return WOLFCERT_ERR_WC(rc, "scep", "InitWithCert");
}

/* RFC 8894 is RSA-only: the pkcsPKIEnvelope is encrypted to the RA/CA
* public key with CMS key transport, which wolfSSL only supports for an
* RSA recipient. A non-RSA (e.g. ECC) RA certificate would otherwise fall
* through to the key-agreement path and fail deep in the encoder with
* BAD_KEYWRAP_ALG_E; reject it up front with an actionable diagnostic. */
if (p7->publicKeyOID != RSAk) {
wc_PKCS7_Free(p7);
return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "scep",
"SCEP requires an RSA RA/CA certificate (RFC 8894); the server "
"presented a non-RSA key");
}

WC_RNG rng;
if (wc_InitRng_ex(&rng, heap, WOLFCERT_DEVID_SOFTWARE) != 0) {
wc_PKCS7_Free(p7);
Expand Down
46 changes: 23 additions & 23 deletions tests/interop/est_libest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
# `estserver` and `estclient` in PATH. Install by:
#
# git clone https://github.com/cisco/libest
# cd libest && ./configure --with-ssl-dir=$(pkg-config --variable prefix openssl)
# cd libest && ./configure --disable-safec \
# --with-ssl-dir=$(pkg-config --variable prefix openssl)
# make && sudo make install
#
# The script exercises:
Expand Down Expand Up @@ -117,9 +118,7 @@ kill_if "$ES_PID"; ES_PID=""
echo "[2] libest estclient -> wolfcert-server (HTTPS via built-in TLS)"
EST_PORT=$(free_port)

# Mint a loopback server identity specifically for wolfcert-server's
# --tls-cert/--tls-key (no external fronting terminator required; this
# was note 4 in docs/INTEROP.md before server-side TLS landed).
# Mint a loopback server identity for wolfcert-server's --tls-cert/--tls-key.
openssl req -x509 -newkey rsa:2048 -nodes -days 1 \
-keyout wc-srv.key -out wc-srv.crt -subj "/CN=127.0.0.1" \
-addext "subjectAltName=IP:127.0.0.1" >/dev/null 2>&1
Expand All @@ -131,27 +130,28 @@ WC_PID=$!
trap 'kill_if "$WC_PID"' EXIT
wait_port 127.0.0.1 "$EST_PORT"

# estclient needs to trust wc-srv.crt as the server bootstrap anchor;
# pass it explicitly. Flag spellings vary between libest releases, so
# try a couple and skip cleanly if none match.
set +e
# estclient pins the server cert as its trust anchor via --trustanchor,
# auto-generates an RSA identity key when none is supplied (-x), and writes the
# enrolled cert to <out_dir>/cert-<thread>-<iter>.pkcs7 as base64 PKCS#7. -o is
# an output DIRECTORY, not a file.
mkdir -p d2out
estclient -e -s "127.0.0.1" -p "$EST_PORT" \
--common-name "libest-cli-1" \
--pem-file wc-srv.crt -o libest-cli.crt >estclient.log 2>&1
RC=$?
if [ "$RC" -ne 0 ]; then
estclient -e -s "127.0.0.1" -p "$EST_PORT" \
--common-name "libest-cli-1" \
-c wc-srv.crt -o libest-cli.crt >>estclient.log 2>&1
RC=$?
fi
set -e
if [ "$RC" -eq 0 ] && [ -s libest-cli.crt ]; then
echo " PASS"
else
echo " SKIP (estclient invocation did not match this libest build;"
echo " adjust the trust-anchor flag for your version.)"
fi
--trustanchor wc-srv.crt \
-o d2out >estclient.log 2>&1 \
|| { echo " FAIL (estclient enroll failed):"; cat estclient.log; exit 1; }

# Decode the base64 PKCS#7 response and confirm wolfcert-server issued the
# requested identity.
# `|| true`: under `set -e`+pipefail an unmatched glob makes ls exit non-zero
# and would abort here before the friendly guard below could report it.
cert_p7=$(ls d2out/cert-*.pkcs7 2>/dev/null | head -1 || true)
[ -n "$cert_p7" ] || { echo " FAIL (no cert emitted)"; cat estclient.log; exit 1; }
openssl base64 -d -in "$cert_p7" -out d2out/cert.p7der
openssl pkcs7 -inform DER -in d2out/cert.p7der -print_certs -out d2out/cert.pem
openssl x509 -in d2out/cert.pem -noout -subject \
| grep -q "CN *= *libest-cli-1"
echo " PASS"
kill_if "$WC_PID"

echo "OK"
49 changes: 35 additions & 14 deletions tests/interop/est_stepca.sh
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,36 @@ step ca init \
--deployment-type standalone \
>init.log 2>&1

# RFC 8894 SCEP is RSA-only: the CA decrypts the pkcsPKIEnvelope with its
# private key, which step-ca's default ECDSA chain cannot do (see
# docs/INTEROP.md note 5). Swap in an RSA root + intermediate so the SCEP
# decrypter - and every cert GetCACert hands the client - is RSA. step-ca uses
# the intermediate as the SCEP decrypter when none is configured explicitly.
# The intermediate key stays encrypted under STEP_PASSWORD (step-ca loads it at
# startup); the throwaway root key is only needed here to sign the intermediate.
ROOT_CRT="$STEPPATH/certs/root_ca.crt"
ROOT_KEY="$STEPPATH/secrets/root_ca_key"
INT_CRT="$STEPPATH/certs/intermediate_ca.crt"
INT_KEY="$STEPPATH/secrets/intermediate_ca_key"
step certificate create "wolfCert-interop RSA Root" "$ROOT_CRT" "$ROOT_KEY" \
--profile root-ca --kty RSA --size 2048 --not-after 87600h \
--force --no-password --insecure >rsa-root.log 2>&1
step certificate create "wolfCert-interop RSA Intermediate" "$INT_CRT" "$INT_KEY" \
--profile intermediate-ca --kty RSA --size 2048 --not-after 87600h \
--ca "$ROOT_CRT" --ca-key "$ROOT_KEY" \
--force --password-file <(echo -n "$STEP_PASSWORD") >rsa-int.log 2>&1

# Enable SCEP provisioner (EST is on by default in recent step-ca).
# Configure a challenge so we exercise RFC 8894 section 2.9 end-to-end.
# --encryption-algorithm-identifier 1 selects AES-128-CBC for the CertRep
# content encryption (step-ca defaults to legacy DES-CBC), matching what
# wolfcert-client advertises via GetCACaps.
SCEP_CHALLENGE="wolfcert-interop-challenge"
step ca provisioner add "SCEP" --type SCEP \
--force-cn \
--challenge "$SCEP_CHALLENGE" \
>scep-add.log 2>&1 || echo " (scep provisioner add skipped: $?)"
--encryption-algorithm-identifier 1 \
>scep-add.log 2>&1

CA_PORT=$(grep -oE '127\.0\.0\.1:[0-9]+' "$STEPPATH/config/ca.json" | cut -d: -f2 | head -1)
echo "[setup] step-ca will listen on :$CA_PORT"
Expand Down Expand Up @@ -98,27 +121,25 @@ fi
# ---- SCEP enrollment ------------------------------------------------------

echo "[2] SCEP: wolfcert-client enroll against step-ca (RSA device key)"
# With the RSA chain in place the pkcsPKIEnvelope encrypts and step-ca issues
# the cert, but its CertRep is signed by github.com/smallstep/scep (the
# micromdm library), so verifying that signature hits the same wolfSSL PKCS#7
# gap as the micromdm interop (ASN_SIG_CONFIRM_E, -229). This is a STRICT
# assertion by design: it fails until wolfSSL PR #10928 (pkcs7_fix) reaches
# master, then self-heals to PASS. See docs/INTEROP.md note 5.
SCEP_URL="https://localhost:$CA_PORT/scep/SCEP"
set +e
"$WC_CLIENT" enroll --proto scep \
--url "$SCEP_URL" \
--trust ca-root.pem \
--challenge "$SCEP_CHALLENGE" \
--key-type rsa:2048 \
--subject "CN=stepca-interop-scep" \
--out-key scep.key --out-cert scep.crt \
>wc-scep.log 2>&1
RC=$?
set -e
if [ "$RC" -eq 0 ]; then
openssl x509 -in scep.crt -noout -subject \
| grep -q "CN *= *stepca-interop-scep"
echo " PASS (cert returned; challengePassword accepted by step-ca)"
else
WCDETAIL=$(grep -m1 "detail" wc-scep.log 2>/dev/null || true)
echo " KNOWN-FAIL (wolfcert-client rc=$RC - see docs/INTEROP.md note 6."
echo " ${WCDETAIL:-(no detail))}"
fi
>wc-scep.log 2>&1 \
|| { echo " FAIL (scep enroll failed):"; cat wc-scep.log; exit 1; }
openssl x509 -in scep.crt -noout -subject \
| grep -q "CN *= *stepca-interop-scep"
echo " PASS (cert returned; challengePassword accepted by step-ca)"

kill_if "$CA_PID"
echo "OK"
132 changes: 132 additions & 0 deletions tests/unit/test_scep_msg.c
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@
#include <wolfssl/wolfcrypt/asn_public.h>
#include <wolfssl/wolfcrypt/pkcs7.h>
#include <wolfssl/wolfcrypt/rsa.h>
#ifdef HAVE_ECC
#include <wolfssl/wolfcrypt/ecc.h>
#endif
#include <wolfssl/wolfcrypt/random.h>
#ifndef NO_SHA
#include <wolfssl/wolfcrypt/sha.h>
Expand Down Expand Up @@ -138,6 +141,131 @@ static int make_ca(uint8_t** cert_out, size_t* cert_out_len,
return ret;
}

#ifdef HAVE_ECC
/* Generate a throwaway self-signed ECC (P-256) CA cert, DER. Ownership of the
* returned buffer passes to the caller (free with free()). */
static int make_ecc_ca(uint8_t** cert_out, size_t* cert_out_len)
{
ecc_key key;
WC_RNG rng;
Cert* cert = NULL;
uint8_t* cert_der = NULL;
int ret = 0;
int body_n = 0;
int cert_n = 0;

if (wc_InitRng(&rng) != 0)
return -1;
if (wc_ecc_init(&key) != 0) {
wc_FreeRng(&rng);
return -1;
}

if (ret == 0 && wc_ecc_make_key(&rng, 32, &key) != 0) /* 32 bytes = P-256 */
ret = -1;

if (ret == 0) {
cert_der = (uint8_t*)malloc(4096);
if (cert_der == NULL)
ret = -1;
}

if (ret == 0) {
cert = wc_CertNew(NULL);
if (cert == NULL)
ret = -1;
}

if (ret == 0) {
wc_InitCert_ex(cert, NULL, INVALID_DEVID);
strncpy(cert->subject.commonName, "wolfCert Test ECC CA",
CTC_NAME_SIZE - 1);
cert->subject.commonName[CTC_NAME_SIZE - 1] = '\0';
cert->isCA = 1;
cert->selfSigned = 1;
cert->sigType = CTC_SHA256wECDSA;
cert->daysValid = 2;

body_n = wc_MakeCert(cert, cert_der, 4096, NULL, &key, &rng);
if (body_n <= 0)
ret = -1;
}

if (ret == 0) {
cert_n = wc_SignCert(cert->bodySz, cert->sigType, cert_der, 4096,
NULL, &key, &rng);
if (cert_n <= 0)
ret = -1;
}

if (ret == 0) {
*cert_out = cert_der;
*cert_out_len = (size_t)cert_n;
cert_der = NULL; /* ownership transferred */
}

if (cert != NULL)
wc_CertFree(cert);
free(cert_der);
wc_ecc_free(&key);
wc_FreeRng(&rng);
return ret;
}

/* wolfcert_scep_envelop must reject a non-RSA RA certificate up front with a
* clear WOLFCERT_ERR_UNSUPPORTED, not fail deep in the PKCS#7 encoder with
* BAD_KEYWRAP_ALG_E - RFC 8894 is RSA-only. An RSA RA cert still succeeds. */
static int test_envelop_rejects_ecc_ra(void)
{
static const uint8_t payload[] = { 0x30, 0x03, 0x02, 0x01, 0x00 };
uint8_t* ecc_ca = NULL;
size_t ecc_ca_len = 0;
uint8_t* rsa_ca = NULL;
size_t rsa_ca_len = 0;
uint8_t* rsa_key = NULL;
size_t rsa_key_len = 0;
WolfCertBuffer ecc_env = { 0 };
WolfCertBuffer rsa_env = { 0 };
int made_ecc, made_rsa;
int ecc_rc = 0, rsa_rc = 0;
int ecc_env_empty = 0, rsa_env_ok = 0;

made_ecc = make_ecc_ca(&ecc_ca, &ecc_ca_len);
made_rsa = make_ca(&rsa_ca, &rsa_ca_len, &rsa_key, &rsa_key_len);

/* enc_oid is irrelevant for the ECC case: the recipient is rejected up
* front, before it is used. The RSA cert is the contrast that must still
* envelop. */
if (made_ecc == 0) {
ecc_rc = wolfcert_scep_envelop(ecc_ca, ecc_ca_len, payload,
sizeof(payload), AES128CBCb, &ecc_env,
NULL);
ecc_env_empty = (ecc_env.data == NULL);
}
if (made_rsa == 0) {
rsa_rc = wolfcert_scep_envelop(rsa_ca, rsa_ca_len, payload,
sizeof(payload), AES128CBCb, &rsa_env,
NULL);
rsa_env_ok = (rsa_env.data != NULL && rsa_env.len > 0);
}

/* Free everything before asserting so a failed REQUIRE cannot leak. */
wolfcert_buffer_free(&ecc_env);
wolfcert_buffer_free(&rsa_env);
free(ecc_ca);
free(rsa_ca);
free(rsa_key);

REQUIRE(made_ecc == 0);
REQUIRE(made_rsa == 0);
REQUIRE(ecc_rc == WOLFCERT_ERR_UNSUPPORTED);
REQUIRE(ecc_env_empty);
REQUIRE(rsa_rc == WOLFCERT_OK);
REQUIRE(rsa_env_ok);
return 0;
}
#endif /* HAVE_ECC */

/* Build a FAILURE CertRep signed with hash_oid and confirm it carries no
* pkcsPKIEnvelope and still round-trips through the parser - which must
* discover the signer's digest rather than assume one. */
Expand Down Expand Up @@ -962,6 +1090,10 @@ int main(void)
return 1;
if (test_signer_matches_any_bundle_cert())
return 1;
#ifdef HAVE_ECC
if (test_envelop_rejects_ecc_ra())
return 1;
#endif
wolfcert_cleanup();
printf("OK\n");
return 0;
Expand Down
Loading