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
8 changes: 4 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,16 @@ configure time if the installed wolfSSL lacks any of `HAVE_PKCS7`,
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
one links them regardless. The test server's post-handshake-auth mode
additionally needs `KEEP_PEER_CERT` and `WOLFSSL_HAVE_TLS_UNIQUE`; without
them it returns `WOLFCERT_ERR_UNSUPPORTED`. The OpenSSL compatibility layer
one links them regardless. The OpenSSL compatibility layer
itself is not required. With ML-DSA enabled it additionally needs
`WOLFSSL_MLDSA_CHECK_KEY` (`wc_MlDsaKey_CheckKey()`), which reloading an
ML-DSA CA from a store calls -- checked when `src/key_algs.c` compiles,
since only `dilithium.h` resolves that macro. `--enable-mldsa` gives it by
default; it is lost only if wolfSSL is built with
`WOLFSSL_DILITHIUM_NO_CHECK_KEY` or `WOLFSSL_MLDSA_VERIFY_ONLY`.

The in-tree test server needs `KEEP_PEER_CERT` for post-handshake auth and `/simplereenroll`, and `WOLFSSL_HAVE_TLS_UNIQUE` for post-handshake auth; `README.md` has the details below its configure line.

**Key algorithms are gated** by `WOLFCERT_HAVE_<ALG>` (RSA, ECC,
ED25519, ED448, MLDSA). RSA, ECC, Ed25519, Ed448 and ML-DSA are each
*optional* (absent => warning, that key type returns
Expand Down Expand Up @@ -107,7 +107,7 @@ After a build with `-DWOLFCERT_ENABLE_CLI=ON` (the default):

```sh
build/wolfcert-server --proto est --listen 127.0.0.1:8443 \
--tls-cert server.crt --tls-key server.key
--tls-cert server.crt --tls-key server.key --est-allow-anonymous
build/wolfcert-client enroll --proto est \
--url https://127.0.0.1:8443/.well-known/est --trust server.crt \
--key-type ecc:256 --subject "CN=dev" \
Expand Down
1 change: 1 addition & 0 deletions Makefile.am
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ test_csr_LDADD = libwolfcert.la $(WOLFSSL_LIBS)
test_store_SOURCES = tests/unit/test_store.c
test_store_LDADD = libwolfcert.la $(WOLFSSL_LIBS)
test_transport_SOURCES = tests/unit/test_transport.c
test_transport_CPPFLAGS = $(AM_CPPFLAGS) -I$(top_srcdir)/src
test_transport_LDADD = libwolfcert.la $(WOLFSSL_LIBS)

test_net_SOURCES = tests/unit/test_net.c
Expand Down
11 changes: 5 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,7 @@ optional key types wolfCert picks up when available:
-DWOLFSSL_PUBLIC_ASN -DWOLFSSL_HAVE_TLS_UNIQUE"
```

`-DKEEP_PEER_CERT` and `-DWOLFSSL_HAVE_TLS_UNIQUE` are needed only by the test
server's post-handshake-auth mode (`wolfcert-server --tls-post-handshake-auth`).
Without them that mode returns `WOLFCERT_ERR_UNSUPPORTED`, and
`est_pha_roundtrip` / `est_async_roundtrip` skip. `--enable-opensslextra`
gives `KEEP_PEER_CERT` but not `WOLFSSL_HAVE_TLS_UNIQUE`.
`-DKEEP_PEER_CERT` and `-DWOLFSSL_HAVE_TLS_UNIQUE` are needed only by the test server. Its post-handshake-auth mode (`wolfcert-server --tls-post-handshake-auth`) needs both, and its `/simplereenroll` needs `KEEP_PEER_CERT` to compare the CSR with the certificate being renewed. Without them that mode returns `WOLFCERT_ERR_UNSUPPORTED`, an mTLS server answers `/simplereenroll` with 500, `est_pha_roundtrip` / `est_async_roundtrip` skip, and `est_mtls_roundtrip` skips its reenroll cases. `--enable-opensslextra` gives `KEEP_PEER_CERT` but not `WOLFSSL_HAVE_TLS_UNIQUE`.

### Header-based configuration (no build system)

Expand All @@ -93,8 +89,11 @@ Start the bundled test server (issues from an auto-generated local CA):
```sh
# EST is TLS-only (RFC 7030), so it needs a server identity for the
# listen address; SCEP authenticates at the pkiMessage layer instead.
# EST also needs --basic USER:PASS, --tls-client-ca PEM, or
# --est-allow-anonymous to issue to any client; /simplereenroll
# also needs --tls-client-ca and the KEEP_PEER_CERT build above.
./wolfcert-server --proto est --listen 127.0.0.1:8443 \
--tls-cert server.crt --tls-key server.key
--tls-cert server.crt --tls-key server.key --est-allow-anonymous
./wolfcert-server --proto scep --listen 127.0.0.1:8088
```

Expand Down
25 changes: 21 additions & 4 deletions cli/wolfcert_server.c
Original file line number Diff line number Diff line change
Expand Up @@ -62,17 +62,21 @@ static void print_usage(FILE* out)
" [--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"
" [--scep-enable-get-cert] [--est-allow-anonymous]\n"
"\n"
"Options:\n"
" --proto est|scep Protocol to serve (required)\n"
" --listen HOST:PORT Bind address (default 0.0.0.0:8080)\n"
" --basic USER:PASS Require HTTP Basic auth (EST enroll)\n"
" --basic USER:PASS Require HTTP Basic auth (EST enroll); both non-empty\n"
" --challenge PASS Require this SCEP challengePassword in the CSR\n"
" --tls-cert PEMFILE Terminate TLS with this server certificate (PEM);\n"
" required for --proto est (RFC 7030)\n"
" --tls-key PEMFILE Private key for --tls-cert (PEM)\n"
" --tls-client-ca PEMFILE Require mutual TLS; verify clients against this CA\n"
" --est-allow-anonymous Issue EST certificates to any client; --proto est\n"
" needs this, --basic or --tls-client-ca.\n"
" /simplereenroll also needs --tls-client-ca and a\n"
" KEEP_PEER_CERT wolfSSL\n"
" --scep-require-approval Defer SCEP PKCSReq/RenewalReq (pkiStatus=PENDING); issue\n"
" on first GetCertInitial with the same transactionID\n"
" --scep-enable-next-ca Advertise + answer GetNextCACert (RFC 8894 section 4.7),\n"
Expand Down Expand Up @@ -123,7 +127,7 @@ static int parse_listen(const char* arg, char** host, uint16_t* port)
static int parse_basic(const char* arg, char** user, char** pass)
{
const char* colon = strchr(arg, ':');
if (colon == NULL)
if (colon == NULL || colon == arg || colon[1] == '\0')
return -1;

*user = strndup(arg, (size_t)(colon - arg));
Expand Down Expand Up @@ -177,6 +181,7 @@ int main(int argc, char** argv)
{ "tls-post-handshake-auth", no_argument, NULL, 'H' },
{ "csrattrs-file", required_argument, NULL, 'F' },
{ "est-require-csrattrs", no_argument, NULL, 'Q' },
{ "est-allow-anonymous", no_argument, NULL, 'Y' },
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, 'V' },
{ 0 }
Expand All @@ -203,6 +208,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 est_allow_anonymous = 0;
int c;

while ((c = getopt_long(argc, argv, "", opts, NULL)) != -1) {
Expand All @@ -218,7 +224,7 @@ int main(int argc, char** argv)
break;
case 'b':
if (parse_basic(optarg, &user, &pass) != 0) {
fprintf(stderr, "invalid --basic (expected USER:PASS)\n");
fprintf(stderr, "invalid --basic (expected non-empty USER:PASS)\n");
return 1;
}
break;
Expand Down Expand Up @@ -274,6 +280,9 @@ int main(int argc, char** argv)
case 'Q':
est_require_csr_attrs = 1;
break;
case 'Y':
est_allow_anonymous = 1;
break;
case 'V':
printf("wolfcert-server %s\n", wolfcert_version_string());
return 0;
Expand Down Expand Up @@ -310,6 +319,13 @@ int main(int argc, char** argv)
return 1;
}

if (sel == WOLFCERT_PROTO_EST && user == NULL && tls_ca == NULL &&
!est_allow_anonymous) {
fprintf(stderr, "wolfcert-server: --proto est requires --basic, "
"--tls-client-ca or --est-allow-anonymous\n");
return 1;
}

if (host == NULL)
host = strdup("0.0.0.0");

Expand Down Expand Up @@ -372,6 +388,7 @@ int main(int argc, char** argv)
.csr_attributes_der = csr_attrs_blob,
.csr_attributes_len = csr_attrs_blob_len,
.est_require_csr_attributes = est_require_csr_attrs,
.est_allow_anonymous_enroll = est_allow_anonymous,
};

int rc = wolfcert_server_start(&cfg, &g_server);
Expand Down
4 changes: 2 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ four endpoints a typical device needs:
| `GET /cacerts` | `wolfcert_est_get_cacerts` | CA chain as degenerate PKCS#7; decoded to PEM for you. |
| `GET /csrattrs` | `wolfcert_est_get_csr_attrs` | Raw body (empty on HTTP 204); decode with `wolfcert_est_parse_csr_attrs`. |
| `POST /simpleenroll` | `wolfcert_est_simple_enroll` | Body is base64-wrapped CSR DER; 200 returns the issued cert as PKCS#7. |
| `POST /simplereenroll` | `wolfcert_est_simple_reenroll` | Same, with the cert being renewed used as the implicit client identity. |
| `POST /simplereenroll` | `wolfcert_est_simple_reenroll` | Same, with the cert being renewed used as the implicit client identity. The test server's requirements for it are at `WolfCertServerCfgSrv.tls_client_ca_pem` in `wolfcert/server.h`. |

A typical flow is: `wolfcert_key_generate` → `wolfcert_csr_build` →
`wolfcert_est_simple_enroll` → persist the returned PEM. The keep-alive
Expand Down Expand Up @@ -172,7 +172,7 @@ during the handshake, since TLS 1.2 has no PHA. On the first `/simpleenroll`
or `/simplereenroll` without a peer cert, the server calls
`wolfSSL_request_certificate()` and waits (5 s, capped at the per-request
deadline) for the client's post-handshake Finished. Any TLS error, app data
first, or an empty Certificate gets a 401; if the request deadline ends the
first, or an empty Certificate gets a 403; if the request deadline ends the
wait first, the connection is dropped like any request that misses it. The
accept loop keeps the connection open across requests, so the anonymous
`/cacerts` and the authenticated enroll land on one connection. The mode
Expand Down
3 changes: 2 additions & 1 deletion examples/certs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ Two algorithm families, each a self-signed CA plus a server and a client leaf:

## Use with the CLIs

Server-side TLS (optionally mutual TLS with `--tls-client-ca`):
Server-side TLS with mutual TLS via `--tls-client-ca` (swap it for
`--est-allow-anonymous` to issue to any client):

```sh
build/wolfcert-server --proto est --listen 127.0.0.1:8443 \
Expand Down
2 changes: 1 addition & 1 deletion examples/certs/gen-certs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
#
# Mapping onto the CLIs:
# wolfcert-server --tls-cert <alg>/server-cert.pem --tls-key <alg>/server-key.pem \
# [--tls-client-ca <alg>/ca-cert.pem]
# --tls-client-ca <alg>/ca-cert.pem | --est-allow-anonymous
# wolfcert-client ... --trust <alg>/ca-cert.pem \
# [--client-cert <alg>/client-cert.pem --client-key <alg>/client-key.pem]
#
Expand Down
93 changes: 49 additions & 44 deletions src/ca_issue.c
Original file line number Diff line number Diff line change
Expand Up @@ -638,52 +638,59 @@ static void free_subject_pubkey(word32 keyOID, void* impl, void* heap)
}
}

/* Carry the subjectAltName from the parsed CSR into the issued cert. wolfSSL
* only transcribes the subject DN, and it also splits parsed alt names by type
* (rfc822Name lands in altEmailNames, not altNames), so we recombine the
* carriable lists -- DNS / URI / IP / registeredID in altNames and rfc822Name
* in altEmailNames -- into one list and flatten it into the GeneralNames
* SEQUENCE that Cert.altNames expects.
*
* directoryName and otherName cannot be faithfully re-encoded from a parsed
* cert via wc_FlattenAltNames (the parser strips the directoryName SEQUENCE
* wrapper, and wolfSSL's cert generator has no path to restore it). Rather
* than silently issue a cert missing a SAN entry the requester asked for, we
* reject such a CSR. The altDirNames / altOtherNamesRaw lists (and the
* rfc822Name split) only exist when wolfSSL keeps name-constraint state. */
static int flatten_csr_san(DecodedCert* dc, Cert* nc, void* heap)
/* 1 when every GeneralName in san is an rfc822Name, dNSName, URI, iPAddress or
* registeredID, the forms the test CA issues. */
static int san_types_issuable(const byte* san, word32 san_len)
{
DNS_entry* merged = NULL;
int rc = 0;

#ifndef IGNORE_NAME_CONSTRAINTS
if (dc->altDirNames != NULL || dc->altOtherNamesRaw != NULL)
return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "ca",
"CSR carries a directoryName/otherName SAN that cannot be issued");
#endif
word32 idx = 0;
word32 end;
int len = 0;
byte tag = 0;

if (GetASNTag(san, &idx, &tag, san_len) < 0 ||
tag != (ASN_SEQUENCE | ASN_CONSTRUCTED) ||
GetLength(san, &idx, &len, san_len) < 0)
return 0;

end = idx + (word32)len;
while (idx < end) {
if (GetASNTag(san, &idx, &tag, end) < 0 ||
GetLength(san, &idx, &len, end) < 0)
return 0;
if (tag != (ASN_CONTEXT_SPECIFIC | ASN_RFC822_TYPE) &&
tag != (ASN_CONTEXT_SPECIFIC | ASN_DNS_TYPE) &&
tag != (ASN_CONTEXT_SPECIFIC | ASN_URI_TYPE) &&
tag != (ASN_CONTEXT_SPECIFIC | ASN_IP_TYPE) &&
tag != (ASN_CONTEXT_SPECIFIC | ASN_RID_TYPE))
return 0;
idx += (word32)len;
}

const DNS_entry* srcs[] = {
dc->altNames,
#ifndef IGNORE_NAME_CONSTRAINTS
dc->altEmailNames,
#endif
};
return 1;
}

for (size_t i = 0; i < sizeof(srcs) / sizeof(srcs[0]) && rc == 0; ++i) {
for (const DNS_entry* e = srcs[i]; e != NULL && rc == 0; e = e->next)
rc = wc_SetDNSEntry(heap, e->name, e->len, e->type, &merged);
}
if (rc != 0) {
FreeAltNames(merged, heap);
return WOLFCERT_ERR_WC(rc, "ca", "SetDNSEntry(issue SAN)");
}
/* Copy the CSR's subjectAltName into the issued cert byte for byte. */
static int copy_csr_san(const DecodedCert* dc, Cert* nc)
{
const byte* san = NULL;
word32 san_len = 0;

/* Encode straight into nc->altNames / altNamesSz (0 when no SAN). */
rc = wc_SetAltNamesFromList(nc, merged);
if (wolfcert_find_san(dc, &san, &san_len) != WOLFCERT_OK)
return WOLFCERT_ERR(WOLFCERT_ERR_PARSE, "ca",
"CSR extensions do not parse");
if (san_len > sizeof(nc->altNames))
return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "ca",
"CSR SAN is %u bytes, limit %d", (unsigned)san_len,
(int)sizeof(nc->altNames));
if (san != NULL && !san_types_issuable(san, san_len))
return WOLFCERT_ERR(WOLFCERT_ERR_UNSUPPORTED, "ca",
"CSR carries an otherName, x400Address, directoryName or "
"ediPartyName SAN");

FreeAltNames(merged, heap);
if (rc != 0)
return WOLFCERT_ERR_WC(rc, "ca", "SetAltNamesFromList(issue)");
if (san != NULL)
memcpy(nc->altNames, san, san_len);
nc->altNamesSz = (int)san_len;
nc->altNamesCrit = dc->extSubjAltNameCrit;
return WOLFCERT_OK;
}

Expand Down Expand Up @@ -791,9 +798,7 @@ int wolfcert_ca_issue(WolfCertCa* ca,
nc->daysValid = 365;
nc->isCA = 0;

/* Carry the requested subjectAltName from the CSR into the issued
* cert. */
rc = flatten_csr_san(&dc, nc, heap);
rc = copy_csr_san(&dc, nc);
}

if (rc == 0) {
Expand Down
3 changes: 3 additions & 0 deletions src/client.c
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,9 @@ int wolfcert_client_reenroll(WolfCertClient* client, const WolfCertServerCfg* sr
if (srv == NULL || current_cert == NULL || current_key == NULL ||
meta == NULL || out_key == NULL || out_cert_pem == NULL)
return WOLFCERT_ERR_BAD_ARG;
if (wolfcert_csr_meta_sets_identity(meta))
return WOLFCERT_ERR(WOLFCERT_ERR_BAD_ARG, "client",
"a renewal keeps the certificate's subject and SAN");

WolfCertKey* nk = NULL;
int rc;
Expand Down
Loading
Loading