Skip to content
Open
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
3 changes: 3 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ configure time if the installed wolfSSL lacks any of `HAVE_PKCS7`,
`WOLFSSL_KEY_GEN`, `WOLF_CRYPTO_CB`, `WOLFSSL_BASE64_ENCODE`,
`WOLFSSL_ALT_NAMES`, or `WOLFSSL_CERT_NAME_ALL`, or if it was built with
`NO_AES` / `NO_SHA256`, or if it provides neither TLS 1.2 nor TLS 1.3.
With SCEP enabled it also needs AES-128-CBC encrypt and decrypt
(`NO_AES_128`, `NO_AES_CBC` or `NO_AES_DECRYPT` hard-fail), since RFC 8894
makes it mandatory-to-implement.
It also link-probes the `WOLFSSL_ASN_API` helpers it calls, which a shared
libwolfssl exports only under one of `WOLFSSL_PUBLIC_ASN` (the lean choice),
`OPENSSL_EXTRA`, `OPENSSL_EXTRA_X509_SMALL` or `WOLFSSL_TEST_CERT`; a static
Expand Down
17 changes: 17 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,23 @@ else()
"the rsa:* key type will return WOLFCERT_ERR_UNSUPPORTED.")
endif()

# AES-128-CBC: mandatory-to-implement for SCEP (RFC 8894 section 2.9).
if(WOLFCERT_ENABLE_SCEP)
unset(_have_aes128cbc CACHE)
check_c_source_compiles("${_wc_wolfssl_prologue}
#if !defined(WOLFSSL_AES_128) || !defined(HAVE_AES_CBC) || !defined(HAVE_AES_DECRYPT)
#error noaes128cbc
#endif
int main(void) { return 0; }
" _have_aes128cbc)
if(NOT _have_aes128cbc)
message(FATAL_ERROR
"wolfCert SCEP requires AES-128-CBC (RFC 8894). Rebuild wolfSSL "
"without NO_AES_128, NO_AES_CBC or NO_AES_DECRYPT, or set "
"-DWOLFCERT_ENABLE_SCEP=OFF for an EST-only build.")
endif()
endif()

# TLS: the HTTPS transport needs at least TLS 1.2 or TLS 1.3; the endpoints pin
# their floor to whichever lower version is available.
unset(_have_tls12 CACHE)
Expand Down
1 change: 1 addition & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ test_scep_roundtrip_SOURCES = tests/integration/test_scep_roundtrip.c
test_scep_roundtrip_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/src
test_scep_roundtrip_LDADD = libwolfcert.la $(WOLFSSL_LIBS) -lpthread
test_scep_poll_roundtrip_SOURCES = tests/integration/test_scep_poll_roundtrip.c
test_scep_poll_roundtrip_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/src
test_scep_poll_roundtrip_LDADD = libwolfcert.la $(WOLFSSL_LIBS) -lpthread
test_scep_async_roundtrip_SOURCES = tests/integration/test_scep_async_roundtrip.c
test_scep_async_roundtrip_LDADD = libwolfcert.la $(WOLFSSL_LIBS) -lpthread
Expand Down
26 changes: 22 additions & 4 deletions cli/wolfcert_client.c
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,8 @@ typedef struct {
const char* url;
const char* trust_file;
const char* user;
const char* pass;
const char* challenge;
char* pass;
char* challenge;
const char* client_cert_file;
const char* client_key_file;
const char* key_type;
Expand Down Expand Up @@ -227,8 +227,24 @@ static int opt_append(const char*** arr, size_t* len, size_t* cap,
return 0;
}

/* Copy a secret option value, then wipe it from argv so ps cannot show it. */
static int opt_secret(char** dst, char* arg)
{
if (*dst != NULL)
free_secret(*dst, strlen(*dst));

*dst = strdup(arg);
wc_ForceZero(arg, (word32)strlen(arg));

return *dst == NULL ? -1 : 0;
}

static void opts_free(Opts* opts)
{
if (opts->pass != NULL)
free_secret(opts->pass, strlen(opts->pass));
if (opts->challenge != NULL)
free_secret(opts->challenge, strlen(opts->challenge));
free(opts->san_dns);
free(opts->san_ip);
free(opts->san_uri);
Expand Down Expand Up @@ -292,10 +308,12 @@ static int parse_common(int argc, char** argv, Opts* opts)
opts->user = optarg;
break;
case 'P':
opts->pass = optarg;
if (opt_secret(&opts->pass, optarg) != 0)
return -1;
break;
case 'X':
opts->challenge = optarg;
if (opt_secret(&opts->challenge, optarg) != 0)
return -1;
break;
case 'M':
opts->client_cert_file = optarg;
Expand Down
49 changes: 40 additions & 9 deletions cli/wolfcert_server.c
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,15 @@ static void print_usage(FILE* out)
{
fprintf(out,
"wolfcert-server %s\n"
"Usage: wolfcert-server --proto est|scep [--listen HOST:PORT]\n"
"Usage: wolfcert-server --proto est|scep [--listen ADDR:PORT]\n"
" [--basic USER:PASS] [--challenge PASS]\n"
" [--tls-cert PEM --tls-key PEM [--tls-client-ca PEM]]\n"
" [--scep-require-approval] [--scep-enable-next-ca]\n"
" [--scep-enable-get-cert]\n"
"\n"
"Options:\n"
" --proto est|scep Protocol to serve (required)\n"
" --listen HOST:PORT Bind address (default 0.0.0.0:8080)\n"
" --listen ADDR:PORT Numeric IPv4 bind address (default 0.0.0.0:8080)\n"
" --basic USER:PASS Require HTTP Basic auth (EST enroll)\n"
" --challenge PASS Require this SCEP challengePassword in the CSR\n"
" --tls-cert PEMFILE Terminate TLS with this server certificate (PEM);\n"
Expand All @@ -88,7 +88,8 @@ static void print_usage(FILE* out)
" TLS 1.3 post-handshake auth (RFC 8446 section 4.6.2): initial\n"
" handshake is anonymous, client cert is requested when\n"
" EST /simpleenroll is hit on the kept-alive connection.\n"
" Requires --tls-cert/-key and --tls-client-ca.\n"
" Requires --proto est, --tls-cert/-key and\n"
" --tls-client-ca.\n"
" --csrattrs-file PATH Serve this DER-encoded CsrAttrs blob (RFC 7030 section 4.5.2)\n"
" from GET /.well-known/est/csrattrs; without this the\n"
" server answers 204 No Content.\n"
Expand Down Expand Up @@ -120,14 +121,27 @@ static int parse_listen(const char* arg, char** host, uint16_t* port)
return 0;
}

static int parse_basic(const char* arg, char** user, char** pass)
static void free_secret(char* s)
{
const char* colon = strchr(arg, ':');
if (s != NULL) {
wc_ForceZero(s, (word32)strlen(s));
free(s);
}
}

static int parse_basic(char* arg, char** user, char** pass)
{
char* colon = strchr(arg, ':');
if (colon == NULL)
return -1;

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.

wc_ForceZero(colon + 1, (word32)strlen(colon + 1));
if (*user == NULL || *pass == NULL)
return -2;

return 0;
}
Expand Down Expand Up @@ -203,6 +217,7 @@ int main(int argc, char** argv)
uint8_t* csr_attrs_blob = NULL;
size_t csr_attrs_blob_len = 0;
int est_require_csr_attrs = 0;
int basic_rc;
int c;

while ((c = getopt_long(argc, argv, "", opts, NULL)) != -1) {
Expand All @@ -217,13 +232,24 @@ int main(int argc, char** argv)
}
break;
case 'b':
if (parse_basic(optarg, &user, &pass) != 0) {
basic_rc = parse_basic(optarg, &user, &pass);
if (basic_rc == -2) {
fprintf(stderr, "out of memory copying --basic\n");
return 1;
}
if (basic_rc != 0) {
fprintf(stderr, "invalid --basic (expected USER:PASS)\n");
return 1;
}
break;
case 'X':
challenge = optarg;
free_secret(challenge);
challenge = strdup(optarg);
wc_ForceZero(optarg, (word32)strlen(optarg));
if (challenge == NULL) {
fprintf(stderr, "out of memory copying --challenge\n");
return 1;
}
break;
case 'C':
tls_cert = slurp(optarg, &tls_cert_len);
Expand Down Expand Up @@ -339,7 +365,8 @@ int main(int argc, char** argv)
free(csr_attrs_blob);
free(host);
free(user);
free(pass);
free_secret(pass);
free_secret(challenge);
free(tls_cert);
free(tls_key);
free(tls_ca);
Expand Down Expand Up @@ -376,7 +403,10 @@ int main(int argc, char** argv)

int rc = wolfcert_server_start(&cfg, &g_server);
if (rc != WOLFCERT_OK) {
const char* m = wolfcert_last_error_message();
fprintf(stderr, "wolfcert-server: start failed (%s)\n", wolfcert_strerror(rc));
if (m != NULL && *m != '\0')
fprintf(stderr, "wolfcert-server: %s\n", m);
goto out;
}

Expand All @@ -396,7 +426,8 @@ int main(int argc, char** argv)
g_server = NULL;
free(host);
free(user);
free(pass);
free_secret(pass);
free_secret(challenge);
free(tls_cert);
free(tls_key);
free(tls_ca);
Expand Down
14 changes: 14 additions & 0 deletions configure.ac
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,20 @@ AS_IF([test "x$have_rsa" = "xno"],
RSA and the rsa:* key type will return WOLFCERT_ERR_UNSUPPORTED.])])])
AM_CONDITIONAL([WOLFCERT_HAVE_RSA], [test "x$have_rsa" = "xyes"])

# AES-128-CBC: mandatory-to-implement for SCEP (RFC 8894 section 2.9).
AS_IF([test "x$enable_scep" = "xyes"],
[AC_MSG_CHECKING([whether wolfSSL provides AES-128-CBC])
AC_COMPILE_IFELSE(
[AC_LANG_PROGRAM(WOLFCERT_WOLFSSL_PROLOGUE,
[[#if !defined(WOLFSSL_AES_128) || !defined(HAVE_AES_CBC) || !defined(HAVE_AES_DECRYPT)
#error noaes128cbc
#endif]])],
[AC_MSG_RESULT([yes])],
[AC_MSG_RESULT([no])
AC_MSG_ERROR([wolfCert SCEP requires AES-128-CBC (RFC 8894). Rebuild
wolfSSL without NO_AES_128, NO_AES_CBC or NO_AES_DECRYPT, or configure
with --disable-scep.])])])

# TLS: the HTTPS transport needs at least TLS 1.2 or TLS 1.3; the endpoints pin
# their floor to whichever lower version is available.
AC_MSG_CHECKING([whether wolfSSL provides TLS 1.2])
Expand Down
6 changes: 4 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,8 +164,10 @@ what enables the post-handshake-auth bootstrap below.

**Test server PHA mode.** `WolfCertServerCfgSrv.tls_post_handshake_auth`
turns the in-tree server into the other end of shape 3. It needs
`tls_client_ca_pem`; without it `wolfcert_server_start()` returns
`WOLFCERT_ERR_BAD_ARG`. The CTX gets `WOLFSSL_VERIFY_POST_HANDSHAKE` and not
`tls_client_ca_pem` and the EST protocol; without either
`wolfcert_server_start()` returns `WOLFCERT_ERR_BAD_ARG`, since no other
protocol requests the deferred certificate. The CTX gets
`WOLFSSL_VERIFY_POST_HANDSHAKE` and not
`FAIL_IF_NO_PEER_CERT`, so a TLS 1.3 handshake stays anonymous and a TLS 1.2
client can still fetch `/cacerts`. A TLS 1.2 client is asked for its cert
during the handshake, since TLS 1.2 has no PHA. On the first `/simpleenroll`
Expand Down
5 changes: 3 additions & 2 deletions docs/EMBEDDED.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ never `#define ... 0`, because the code tests presence with `#ifdef`.**
result at compile time, so a contradictory or incomplete config fails with a
clear `#error` rather than a confusing downstream error. It enforces the same
rules the configure step does: at least one key algorithm, SCEP requires RSA
(RFC 8894), and the wolfSSL feature set wolfCert depends on (PKCS#7, cert
gen/req/ext, key gen, CryptoCb, base64 encode, alt names,
and AES-128-CBC (both RFC 8894, so `NO_AES_128`, `NO_AES_CBC` and
`NO_AES_DECRYPT` rule SCEP out), and the wolfSSL feature set wolfCert depends
on (PKCS#7, cert gen/req/ext, key gen, CryptoCb, base64 encode, alt names,
`WOLFSSL_CERT_NAME_ALL`, AES, SHA-256, and TLS 1.2 or 1.3). The header you copy
documents the matching wolfSSL configure flags.

Expand Down
3 changes: 2 additions & 1 deletion examples/user_settings.h.example
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
* value of 0 still counts as enabled.
* - At least one key algorithm must be enabled.
* - SCEP is RSA-only (RFC 8894): enabling WOLFCERT_HAVE_SCEP requires
* WOLFCERT_HAVE_RSA.
* WOLFCERT_HAVE_RSA, and a wolfSSL with AES-128-CBC encrypt and decrypt
* (no NO_AES_128, NO_AES_CBC or NO_AES_DECRYPT).
*
* The enabled set must also be backed by the wolfSSL you link against. wolfCert
* requires a wolfSSL (>= 5.9.4) built with at least:
Expand Down
4 changes: 3 additions & 1 deletion scripts/ci/assert-configure-fails.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
# of this gate.
#
# Scope: only misconfigurations that a *buildable* wolfSSL can express are
# covered here (RSA-off-with-SCEP, PKCS7 missing, ASN helpers not exported).
# covered here (RSA-off-with-SCEP, PKCS7 missing, ASN helpers not exported,
# AES-128 off with SCEP).
# wolfSSL's own configure refuses to drop AES / SHA-256 / all TLS / all key
# algorithms, so wolfCert's compile-time #error guards for those
# (check_config.h) can't be fed by a real wolfSSL build and are not exercised
Expand Down Expand Up @@ -56,6 +57,7 @@ CASE_TABLE=(
"no-rsa-scep:neg-no-rsa:::SCEP is RSA-only"
"no-pkcs7:neg-no-pkcs7:::HAVE_PKCS7|missing a required feature"
"no-public-asn:neg-no-public-asn:::does not export its ASN helpers"
"no-aes128-scep:neg-no-aes128:::requires AES-128-CBC"
)

lookup_case() {
Expand Down
6 changes: 5 additions & 1 deletion scripts/ci/build-wolfssl.sh
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ KNOWN_CONFIGS=(
# wolfSSL's own configure refuses to drop AES/SHA-256/all-TLS/all-key-algs
# (those are cascade-required), so wolfCert's compile-time #error guards for
# them in check_config.h cannot be fed by a real wolfSSL build.
neg-no-rsa neg-no-pkcs7 neg-no-public-asn
neg-no-rsa neg-no-pkcs7 neg-no-public-asn neg-no-aes128
)

# Emit the configure argument list (one per line) for a config name.
Expand Down Expand Up @@ -167,6 +167,10 @@ resolve_flags() {
# No ASN-export macro -> "does not export its ASN helpers".
_base_flags
printf '%s\n' 'CPPFLAGS=-DWOLFSSL_ALT_NAMES -DWOLFSSL_CERT_NAME_ALL -DKEEP_PEER_CERT -DWOLFSSL_HAVE_TLS_UNIQUE' ;;
neg-no-aes128)
# NO_AES_128 with SCEP still requested -> "requires AES-128-CBC".
_base_flags
printf '%s\n' 'CPPFLAGS=-DWOLFSSL_ALT_NAMES -DWOLFSSL_CERT_NAME_ALL -DKEEP_PEER_CERT -DWOLFSSL_HAVE_TLS_UNIQUE -DWOLFSSL_PUBLIC_ASN -DNO_AES_128' ;;
*)
echo "ERROR: unknown wolfSSL config '$cfg'." >&2
echo " Known: ${KNOWN_CONFIGS[*]}" >&2
Expand Down
14 changes: 14 additions & 0 deletions src/internal.c
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,20 @@ char* wolfcert_strdup(const char* s, void* heap)
return r;
}

int wolfcert_is_printable_string(const uint8_t* s, size_t len)
{
size_t i;

for (i = 0; i < len; i++) {
uint8_t c = s[i];
if ((c < 'A' || c > 'Z') && (c < 'a' || c > 'z') &&
(c < '0' || c > '9') &&
(c == 0 || strchr(" '()+,-./:=?", c) == NULL))
return 0;
}
return 1;
}

const char* wolfcert_last_error_message(void)
{
return g_err.message;
Expand Down
6 changes: 5 additions & 1 deletion src/internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@ typedef struct {

struct WolfCertServer {
WolfCertServerCfgSrv cfg;
char* cfg_bind_host;
char* cfg_challenge;
char* cfg_basic_user;
char* cfg_basic_pass;
Expand Down Expand Up @@ -396,6 +395,8 @@ WOLFCERT_TEST_VIS size_t wolfcert_oid_to_dotted(const uint8_t* oid, size_t oid_l
char* out, size_t out_cap);

/* SCEP pkiMessage helpers. */
#define SCEP_NONCE_SZ 16

typedef struct {
const uint8_t* transaction_id;
size_t transaction_id_len;
Expand Down Expand Up @@ -507,6 +508,9 @@ WOLFCERT_TEST_VIS int wolfcert_scep_build_next_ca_response(
int wolfcert_extract_spki(const uint8_t* der, size_t len, int is_csr,
uint8_t** out_spki, size_t* out_len, void* heap);

/* 1 if every byte is in the X.680 PrintableString repertoire, else 0. */
int wolfcert_is_printable_string(const uint8_t* s, size_t len);

/* RFC 8894: a CertRep must be signed by the CA or its RA. Confirm the response
* signer certificate shares a public key with some certificate in the trusted
* GetCACert bundle (one or more concatenated DER certs). Returns WOLFCERT_OK on
Expand Down
Loading
Loading