From 0b65f53782d33954f6cc9b5e1985dd80c68834db Mon Sep 17 00:00:00 2001 From: Yosuke Shimizu Date: Mon, 24 Aug 2026 09:27:19 +0900 Subject: [PATCH] port: add a wolfCert transport over wolfIP sockets - src/port/wolfcert_io.c implements WolfCertTransport's connect, read, write and disconnect over wolfIP_sock_*. wolfCert_Init_wolfIP() and wolfCert_Cleanup_wolfIP() take and release a slot in a static context pool holding the stack and a millisecond clock. - connect() resolves through a bounded dotted-quad parser or nslookup(), then pumps wolfIP_poll() until the session is established; disconnect() pumps until the close completes. read() and write() clamp len to INT_MAX, report -WOLFIP_EAGAIN as WANT_READ/WANT_WRITE when timeout_ms is 0 and otherwise pump within a budget, and map -1 to CONN_CLOSED and -WOLFIP_EINVAL to BAD_ARG; read() also maps 0 to CONN_CLOSED. - wolfip.h includes and declares both entry points under WOLFCERT_WOLFIP. - unit_shared.c adds scripted wolfIP_sock_*, wolfIP_poll and nslookup mocks over new mocks/wolfcert headers; unit.c registers a wolfCert-IO tcase of 32 tests from unit_tests_wolfcert.c, which the Makefile adds to UNIT_TEST_SRCS alongside wolfcert_io.c. - docs/wolfcert_howto.md documents the integration, listed in docs/API.md and README.md. --- Makefile | 4 +- README.md | 1 + docs/API.md | 1 + docs/wolfcert_howto.md | 187 ++++++ src/port/wolfcert_io.c | 373 +++++++++++ src/test/unit/mocks/wolfcert/errors.h | 27 + src/test/unit/mocks/wolfcert/types.h | 36 ++ src/test/unit/unit.c | 38 +- src/test/unit/unit_shared.c | 216 +++++++ src/test/unit/unit_tests_wolfcert.c | 860 ++++++++++++++++++++++++++ wolfip.h | 11 + 11 files changed, 1752 insertions(+), 2 deletions(-) create mode 100644 docs/wolfcert_howto.md create mode 100644 src/port/wolfcert_io.c create mode 100644 src/test/unit/mocks/wolfcert/errors.h create mode 100644 src/test/unit/mocks/wolfcert/types.h create mode 100644 src/test/unit/unit_tests_wolfcert.c diff --git a/Makefile b/Makefile index 915e8d89..eaefc648 100644 --- a/Makefile +++ b/Makefile @@ -978,7 +978,9 @@ UNIT_TEST_SRCS:=src/test/unit/unit.c \ src/test/unit/unit_tests_arp_regression.c \ src/test/unit/unit_tests_dns_edges.c \ src/test/unit/unit_tests_misc_edges.c \ - src/test/unit/unit_tests_vlan.c + src/test/unit/unit_tests_vlan.c \ + src/test/unit/unit_tests_wolfcert.c \ + src/port/wolfcert_io.c unit: build/test/unit diff --git a/README.md b/README.md index d97be9e1..502368f5 100644 --- a/README.md +++ b/README.md @@ -191,6 +191,7 @@ This port follows the same model as the POSIX wrapper: Module how-tos: - [TLS over wolfIP](docs/tls_howto.md): running wolfSSL/TLS on wolfIP sockets, the I/O-callback bridge, and non-blocking handshakes +- [Certificate enrolment (wolfCert)](docs/wolfcert_howto.md): running wolfCert's EST/SCEP client on wolfIP sockets, the transport vtable, and name resolution - [HTTP/HTTPS server](docs/http_server_howto.md): the `src/http/` server module, handler registration, and enabling HTTPS - [IPsec ESP](docs/ipsec_esp_howto.md): securing traffic with ESP transport mode, SA setup, and Linux `ip xfrm` interop - [wolfGuard (FIPS WireGuard)](docs/wolfguard_howto.md): the in-stack WireGuard tunnel, peer/key setup, and kernel interop diff --git a/docs/API.md b/docs/API.md index c7179d21..bb6d35b5 100644 --- a/docs/API.md +++ b/docs/API.md @@ -27,6 +27,7 @@ The core socket and stack APIs are documented below. Optional modules and features have dedicated getting-started guides: - [TLS over wolfIP](tls_howto.md) — running wolfSSL/TLS on wolfIP sockets (`WOLFSSL_WOLFIP`), the I/O-callback bridge, and non-blocking handshakes. +- [Certificate enrolment (wolfCert)](wolfcert_howto.md) — running wolfCert's EST/SCEP client on wolfIP sockets (`WOLFCERT_WOLFIP`), the transport vtable, and name resolution. - [HTTP/HTTPS server](http_server_howto.md) — the `src/http/` server module (`WOLFIP_ENABLE_HTTP`), handler registration, and enabling HTTPS via a `WOLFSSL_CTX`. - [IPsec ESP how-to](ipsec_esp_howto.md) — build with `WOLFIP_ESP`, install Security Associations, and interoperate with Linux `ip xfrm`. - [wolfGuard (FIPS WireGuard)](wolfguard_howto.md) — the in-stack WireGuard tunnel (`WOLFGUARD`), peer/key setup, and kernel interop. diff --git a/docs/wolfcert_howto.md b/docs/wolfcert_howto.md new file mode 100644 index 00000000..1a939a75 --- /dev/null +++ b/docs/wolfcert_howto.md @@ -0,0 +1,187 @@ +# Certificate enrolment (wolfCert) How-To + +wolfCert is wolfSSL's certificate enrolment library: it speaks EST (RFC 7030) +and SCEP (RFC 8894) to a CA and hands back an issued certificate. This guide +covers running it on wolfIP, so a device with no BSD sockets can enrol. + +It is a getting-started document, not a reference manual. The authoritative +glue is `src/port/wolfcert_io.c` (declared in `wolfip.h` under +`WOLFCERT_WOLFIP`). The wolfCert API itself — `wolfcert_est_simple_enroll()`, +`wolfcert_scep_*`, key and CSR generation — is documented by wolfCert; this +guide only covers the wolfIP integration points. + +## Table of Contents + +- [1. What the integration provides](#1-what-the-integration-provides) +- [2. Building with wolfCert support](#2-building-with-wolfcert-support) +- [3. Registering the transport](#3-registering-the-transport) +- [4. The transport callbacks](#4-the-transport-callbacks) +- [5. Name resolution](#5-name-resolution) +- [6. Timeouts and the poll loop](#6-timeouts-and-the-poll-loop) +- [7. Troubleshooting](#7-troubleshooting) + +--- + +## 1. What the integration provides + +wolfCert opens its own connections, so it exposes a `WolfCertTransport` vtable +— `connect`, `read`, `write`, `disconnect` — that a stack without BSD sockets +fills in. `src/port/wolfcert_io.c` is wolfIP's implementation of that vtable, +in the same spirit as `src/port/wolfssl_io.c` for wolfSSL. + +One transport carries every protocol wolfCert speaks: + +| Deployment | Covered | +|---|---| +| EST over HTTPS | yes | +| SCEP over HTTPS | yes | +| SCEP over plain HTTP | yes | + +TLS records travel through the same `read`/`write` as plain HTTP, so the rows +above are all carried by wolfIP end to end and the glue needs no TLS code of +its own. + +`src/port/wolfssl_io.c` and `WOLFSSL_WOLFIP` are a different integration — +running wolfSSL directly on wolfIP sockets, see +[TLS over wolfIP](tls_howto.md). They are not needed here, and an application +can use both. + +## 2. Building with wolfCert support + +The integration is gated by **`WOLFCERT_WOLFIP`** and lives in one source +file, `src/port/wolfcert_io.c`, which you compile in and link against +`-lwolfcert` (and `-lwolfssl`, which wolfCert requires). + +1. Build and install wolfCert first. +2. Compile `src/port/wolfcert_io.c` together with your application. +3. Add `-DWOLFCERT_WOLFIP` to the wolfIP/application `CFLAGS`, so the + declarations in `wolfip.h` are exposed. +4. Link with `-lwolfcert -lwolfssl`. + +On a device with no sockets and no filesystem, wolfCert can also drop its own +POSIX transport and file store; see wolfCert's `docs/EMBEDDED.md` for those +build options. + +When `WOLFCERT_WOLFIP` is defined, `wolfip.h` declares the two entry points: + +```c +void *wolfCert_Init_wolfIP(WolfCertTransport *t, struct wolfIP *stack, + uint64_t (*now_ms)(void)); +void wolfCert_Cleanup_wolfIP(void *context); +``` + +One compile-time knob, `MAX_WOLFCERT_CTX` (default 2, in +`src/port/wolfcert_io.c`), sizes the static context pool. + +## 3. Registering the transport + +`wolfCert_Init_wolfIP()` opens nothing. It fills in a transport you own and +returns a context handle for the matching cleanup call: + +```c +static WolfCertTransport wc_transport; +static void *wc_io; + +static uint64_t my_now_ms(void) +{ + return board_get_tick(); /* the clock you already feed wolfIP_poll() */ +} + +wc_io = wolfCert_Init_wolfIP(&wc_transport, ipstack, my_now_ms); +if (wc_io == NULL) + return -1; /* bad arguments, or the pool is full */ + +cfg.transport = wc_transport; /* WolfCertServerCfg, WolfCertHttpSessionCfg + * or WolfCertHttpRequest */ +``` + +Call `wolfCert_Cleanup_wolfIP(wc_io)` when you are done with the stack, to +release the pool slot. + +`now_ms` returns milliseconds and must advance. It is the only clock the +transport has: it drives `wolfIP_poll()` while a connect or a blocking +transfer is in progress, and it bounds every timeout. + +## 4. The transport callbacks + +wolfCert calls these; your application does not. `read` and `write` map +wolfIP's return codes like this: + +| `wolfIP_sock_recv`/`send` returns | Reported as | +|---|---| +| `> 0` | the byte count — short transfers are passed through | +| `0` (receive only) | `WOLFCERT_ERR_CONN_CLOSED` — the peer closed | +| `-1` | `WOLFCERT_ERR_CONN_CLOSED` — the socket is no longer established | +| `-WOLFIP_EAGAIN` | `WANT_READ`/`WANT_WRITE`, or poll and retry when blocking | +| `-WOLFIP_EINVAL` | `WOLFCERT_ERR_BAD_ARG` | +| anything else | `WOLFCERT_ERR_IO` | + +`connect` does not use this mapping. It reports `WOLFCERT_ERR_BAD_ARG` for a +host or port it will not accept, `WOLFCERT_ERR_NOT_FOUND` when a name resolves +to no address, and `WOLFCERT_ERR_IO` for every other failure. + +The connection handle is the wolfIP descriptor, handed back as an opaque +pointer. `disconnect` closes it and polls until the FIN handshake finishes, +since `wolfIP_sock_close()` reports `-WOLFIP_EAGAIN` until then. + +## 5. Name resolution + +`connect` accepts either a dotted quad or a hostname. A dotted quad is parsed +locally; a hostname goes to `nslookup()` and the answer is awaited inside +`connect`. + +**Only one hostname lookup runs at a time, even if you created several +transport contexts.** `nslookup()`'s callback carries no user pointer, so the +transport collects the answer in a single slot. That matches wolfIP's own +one-query-per-stack limit, and `connect` runs to completion before returning, +so lookups cannot overlap. + +Two behaviours of the resolver shape what a failed lookup costs here — a name +that does not exist is never reported, only timed out, and an abandoned query +clears on its own schedule rather than being cancelled. Both are described in +[DHCP & DNS clients](dhcp_dns_howto.md); the practical effect is that a failed +resolution spends the connect budget, and retrying at once can spend part of +the next one. + +Prefer keeping the hostname in the URL over an IP literal, so the server +certificate is verified against the name. + +## 6. Timeouts and the poll loop + +`connect`, and any blocking `read`/`write`, drive `wolfIP_poll()` themselves — +nothing else runs during that call. Two defaults bound them when the caller +sets no timeout, both overridable at compile time: + +| Macro | Default | Applies to | +|---|---|---| +| `WOLFCERT_WOLFIP_CONNECT_TIMEOUT_MS` | 30000 | `connect`, including resolution | +| `WOLFCERT_WOLFIP_IO_TIMEOUT_MS` | 30000 | blocking `read`/`write`, and `disconnect` | + +**Set `cfg.timeout_ms` explicitly on an MCU.** A value comfortably inside your +watchdog period — a few seconds — is the right choice, because the application +is stalled for the whole of a connect. A refused connection costs the full +budget too: wolfIP reports a reset socket the same way it reports a handshake +still in progress, so the transport cannot fail early on an RST. + +Where wolfCert offers a non-blocking mode, prefer it: `read` and `write` then +return immediately instead of polling internally, leaving your own loop to +pace the stack, as in [TLS over wolfIP](tls_howto.md) section 8. The connect +is synchronous either way. + +## 7. Troubleshooting + +**Link errors on `wolfCert_Init_wolfIP`.** `src/port/wolfcert_io.c` was not +compiled, or `-DWOLFCERT_WOLFIP` was not passed. See section 2. + +**`wolfCert_Init_wolfIP()` returns NULL.** A NULL argument, or more transports +than `MAX_WOLFCERT_CTX`. + +**Requests fail immediately with a bad-argument error.** The config carries no +transport, or the URL's host is empty. + +**Connects always take the full timeout.** The peer is unreachable or +refusing. Check the route and that `now_ms` actually advances — a clock that +never moves leaves the transport polling with no deadline. + +**EST enrolment fails with a TLS error.** EST requires the client to +authenticate the server; check wolfCert's trust-anchor settings. diff --git a/src/port/wolfcert_io.c b/src/port/wolfcert_io.c new file mode 100644 index 00000000..98f9f486 --- /dev/null +++ b/src/port/wolfcert_io.c @@ -0,0 +1,373 @@ +/* wolfcert_io.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfIP TCP/IP stack. + * + * wolfIP is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfIP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + * + * wolfIP <-> wolfCert glue: a WolfCertTransport over wolfIP sockets. + * Carries TLS records and plain HTTP alike, so it holds no TLS code of its + * own - wolfCert bridges wolfSSL's CBIO onto these same callbacks. + */ +#include "wolfip.h" +#include +#include +#include +#include + +#ifndef MAX_WOLFCERT_CTX + #define MAX_WOLFCERT_CTX 2 +#endif + +/* Cap on a blocking transfer; wolfIP has no timeout of its own. */ +#ifndef WOLFCERT_WOLFIP_IO_TIMEOUT_MS + #define WOLFCERT_WOLFIP_IO_TIMEOUT_MS 30000 +#endif + +/* Cap on a connect the caller left unbounded; wolfIP has none of its own. */ +#ifndef WOLFCERT_WOLFIP_CONNECT_TIMEOUT_MS + #define WOLFCERT_WOLFIP_CONNECT_TIMEOUT_MS 30000 +#endif + +/* nslookup() reports this while another query is in flight. */ +#define WOLFCERT_WOLFIP_DNS_BUSY (-16) + +/* Per-transport state; a connection is just its wolfIP descriptor. */ +struct wolfcert_io_ctx { + struct wolfIP *stack; + uint64_t (*now_ms)(void); + int in_use; +}; + +static struct wolfcert_io_ctx io_ctxs[MAX_WOLFCERT_CTX]; + +/* nslookup()'s callback carries no user pointer, so the answer lands here. + * One slot serves every context: only one resolution runs at a time. */ +static uint32_t dns_result_ip; +static int dns_result_ready; + +/* Claim a free slot; stack and now_ms are filled in by the caller. */ +static struct wolfcert_io_ctx *io_ctx_alloc(void) +{ + int i; + + for (i = 0; i < MAX_WOLFCERT_CTX; i++) { + if (io_ctxs[i].in_use == 0) { + io_ctxs[i].in_use = 1; + return &io_ctxs[i]; + } + } + return NULL; +} + +static void io_ctx_free(struct wolfcert_io_ctx *c) +{ + if (c != NULL) { + c->stack = NULL; + c->now_ms = NULL; + c->in_use = 0; + } +} + +/* Elapsed against timeout_ms, or against dflt when the caller gave none. */ +static int deadline_expired(struct wolfcert_io_ctx *c, uint64_t start, + int timeout_ms, uint64_t dflt) +{ + uint64_t budget = (timeout_ms > 0) ? (uint64_t)timeout_ms : dflt; + + return ((c->now_ms() - start) >= budget); +} + +/* Decide whether the host is a dotted quad or a name for the resolver. + * Rejects anything atoip4() would silently turn into a wrong address. */ +static int is_ipv4_literal(const char *host) +{ + int octet = 0; + int digits = 0; + int dots = 0; + int i; + + for (i = 0; host[i] != '\0'; i++) { + if (host[i] == '.') { + if (digits == 0) + return 0; + octet = 0; + digits = 0; + dots++; + } + else if ((host[i] >= '0') && (host[i] <= '9')) { + octet = (octet * 10) + (host[i] - '0'); + digits++; + if ((digits > 3) || (octet > 255)) + return 0; + } + else { + return 0; + } + } + return ((dots == 3) && (digits > 0)); +} + +static void dns_result_cb(uint32_t ip) +{ + dns_result_ip = ip; + dns_result_ready = 1; +} + +static int resolve_host(struct wolfcert_io_ctx *c, const char *host, + uint64_t start, int timeout_ms, ip4 *out) +{ + uint16_t id = 0; + int rc = -1; + + if (is_ipv4_literal(host)) { + *out = atoip4(host); + if (*out == 0) + return WOLFCERT_ERR_BAD_ARG; + return WOLFCERT_OK; + } + + for (;;) { + rc = nslookup(c->stack, host, &id, dns_result_cb); + if (rc == 0) + break; + /* Both codes are transient; the query is not armed either way. */ + if ((rc != WOLFCERT_WOLFIP_DNS_BUSY) && (rc != -WOLFIP_EAGAIN)) + return WOLFCERT_ERR_IO; + if (deadline_expired(c, start, timeout_ms, + WOLFCERT_WOLFIP_CONNECT_TIMEOUT_MS)) + return WOLFCERT_ERR_IO; + (void)wolfIP_poll(c->stack, c->now_ms()); + } + + /* Clear once our own query is armed */ + dns_result_ip = 0; + dns_result_ready = 0; + + for (;;) { + if (dns_result_ready != 0) { + if (dns_result_ip == 0) + return WOLFCERT_ERR_NOT_FOUND; + *out = (ip4)dns_result_ip; + return WOLFCERT_OK; + } + if (deadline_expired(c, start, timeout_ms, + WOLFCERT_WOLFIP_CONNECT_TIMEOUT_MS)) + return WOLFCERT_ERR_IO; + (void)wolfIP_poll(c->stack, c->now_ms()); + } +} + +/* -WOLFIP_EAGAIN is the only retryable code; a bare -1 is a dead socket and + * reporting it as would-block would spin the caller forever. */ +static int map_io_error(int rc, int want) +{ + if (rc == -WOLFIP_EAGAIN) + return want; + if (rc == -1) + return WOLFCERT_ERR_CONN_CLOSED; + if (rc == -WOLFIP_EINVAL) + return WOLFCERT_ERR_BAD_ARG; + return WOLFCERT_ERR_IO; +} + +static int wolfcert_wolfip_connect(void *ctx, const char *host, int port, + int timeout_ms, void **conn) +{ + struct wolfcert_io_ctx *c = (struct wolfcert_io_ctx *)ctx; + struct wolfIP_sockaddr_in addr; + uint64_t start; + ip4 ip = 0; + int ret; + int fd; + int rc; + + if ((c == NULL) || (host == NULL) || (host[0] == '\0') || (conn == NULL)) + return WOLFCERT_ERR_BAD_ARG; + if ((port <= 0) || (port > 65535)) + return WOLFCERT_ERR_BAD_ARG; + + start = c->now_ms(); + + ret = resolve_host(c, host, start, timeout_ms, &ip); + if (ret != WOLFCERT_OK) + return ret; + + fd = wolfIP_sock_socket(c->stack, AF_INET, IPSTACK_SOCK_STREAM, 0); + if (fd < 0) + return WOLFCERT_ERR_IO; + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_port = ee16((uint16_t)port); + addr.sin_addr.s_addr = ee32(ip); + + /* Re-issue with the same address each tick until it reports success. */ + ret = WOLFCERT_ERR_IO; + for (;;) { + rc = wolfIP_sock_connect(c->stack, fd, + (struct wolfIP_sockaddr *)&addr, + sizeof(addr)); + if (rc == 0) { + ret = WOLFCERT_OK; + break; + } + if (rc != -WOLFIP_EAGAIN) { + ret = WOLFCERT_ERR_IO; + break; + } + if (deadline_expired(c, start, timeout_ms, + WOLFCERT_WOLFIP_CONNECT_TIMEOUT_MS)) { + ret = WOLFCERT_ERR_IO; + break; + } + (void)wolfIP_poll(c->stack, c->now_ms()); + } + + if (ret != WOLFCERT_OK) { + (void)wolfIP_sock_close(c->stack, fd); + return ret; + } + + *conn = (void *)(intptr_t)fd; + return WOLFCERT_OK; +} + +static int wolfcert_wolfip_read(void *ctx, void *conn, uint8_t *buf, + size_t len, int timeout_ms) +{ + struct wolfcert_io_ctx *c = (struct wolfcert_io_ctx *)ctx; + int fd = (int)(intptr_t)conn; + uint64_t start; + int mapped; + int rc; + + if ((c == NULL) || (buf == NULL) || (len == 0)) + return WOLFCERT_ERR_BAD_ARG; + if (len > (size_t)INT_MAX) + len = (size_t)INT_MAX; + + start = c->now_ms(); + + for (;;) { + rc = wolfIP_sock_recv(c->stack, fd, buf, len, 0); + if (rc > 0) + return rc; + if (rc == 0) + return WOLFCERT_ERR_CONN_CLOSED; + + mapped = map_io_error(rc, WOLFCERT_ERR_WANT_READ); + if (mapped != WOLFCERT_ERR_WANT_READ) + return mapped; + + /* Only a zero timeout reports would-block; the rest pump. */ + if (timeout_ms == 0) + return WOLFCERT_ERR_WANT_READ; + (void)wolfIP_poll(c->stack, c->now_ms()); + if (deadline_expired(c, start, timeout_ms, + WOLFCERT_WOLFIP_IO_TIMEOUT_MS)) + return WOLFCERT_ERR_IO; + } +} + +static int wolfcert_wolfip_write(void *ctx, void *conn, const uint8_t *buf, + size_t len, int timeout_ms) +{ + struct wolfcert_io_ctx *c = (struct wolfcert_io_ctx *)ctx; + int fd = (int)(intptr_t)conn; + uint64_t start; + int mapped; + int rc; + + if ((c == NULL) || (buf == NULL) || (len == 0)) + return WOLFCERT_ERR_BAD_ARG; + if (len > (size_t)INT_MAX) + len = (size_t)INT_MAX; + + start = c->now_ms(); + + for (;;) { + rc = wolfIP_sock_send(c->stack, fd, buf, len, 0); + if (rc > 0) + return rc; + mapped = map_io_error(rc, WOLFCERT_ERR_WANT_WRITE); + if (mapped != WOLFCERT_ERR_WANT_WRITE) + return mapped; + + if (timeout_ms == 0) + return WOLFCERT_ERR_WANT_WRITE; + (void)wolfIP_poll(c->stack, c->now_ms()); + if (deadline_expired(c, start, timeout_ms, + WOLFCERT_WOLFIP_IO_TIMEOUT_MS)) + return WOLFCERT_ERR_IO; + } +} + +/* Closing an established socket starts a FIN handshake and reports + * -WOLFIP_EAGAIN until it finishes. Drive to complete the handshake. */ +static int wolfcert_wolfip_disconnect(void *ctx, void *conn) +{ + struct wolfcert_io_ctx *c = (struct wolfcert_io_ctx *)ctx; + uint64_t start; + int rc; + + if (c == NULL) + return WOLFCERT_ERR_BAD_ARG; + + start = c->now_ms(); + + for (;;) { + rc = wolfIP_sock_close(c->stack, (int)(intptr_t)conn); + if (rc == 0) + return WOLFCERT_OK; + if (rc != -WOLFIP_EAGAIN) + return WOLFCERT_ERR_IO; + if (deadline_expired(c, start, 0, WOLFCERT_WOLFIP_IO_TIMEOUT_MS)) + return WOLFCERT_ERR_IO; + (void)wolfIP_poll(c->stack, c->now_ms()); + } +} + +/* Opens nothing: fills t, and returns the context */ +void *wolfCert_Init_wolfIP(WolfCertTransport *t, struct wolfIP *stack, + uint64_t (*now_ms)(void)) +{ + struct wolfcert_io_ctx *c; + + if ((t == NULL) || (stack == NULL) || (now_ms == NULL)) + return NULL; + + c = io_ctx_alloc(); + if (c == NULL) + return NULL; + + c->stack = stack; + c->now_ms = now_ms; + + t->connect = wolfcert_wolfip_connect; + t->read = wolfcert_wolfip_read; + t->write = wolfcert_wolfip_write; + t->disconnect = wolfcert_wolfip_disconnect; + t->ctx = c; + + return c; +} + +/* Releases the context slot; closes no socket. */ +void wolfCert_Cleanup_wolfIP(void *context) +{ + io_ctx_free((struct wolfcert_io_ctx *)context); +} diff --git a/src/test/unit/mocks/wolfcert/errors.h b/src/test/unit/mocks/wolfcert/errors.h new file mode 100644 index 00000000..669b2762 --- /dev/null +++ b/src/test/unit/mocks/wolfcert/errors.h @@ -0,0 +1,27 @@ +/* Mock wolfcert/errors.h for unit tests. + * Only the codes src/port/wolfcert_io.c uses; values match wolfCert's. + */ +#ifndef WOLFCERT_ERRORS_H +#define WOLFCERT_ERRORS_H + +#ifdef __cplusplus +extern "C" { +#endif + +enum { + WOLFCERT_OK = 0, + WOLFCERT_ERR_GENERIC = -1, + WOLFCERT_ERR_BAD_ARG = -2, + WOLFCERT_ERR_MEMORY = -3, + WOLFCERT_ERR_IO = -4, + WOLFCERT_ERR_NOT_FOUND = -11, + WOLFCERT_ERR_WANT_READ = -14, + WOLFCERT_ERR_WANT_WRITE = -15, + WOLFCERT_ERR_CONN_CLOSED = -16 +}; + +#ifdef __cplusplus +} +#endif + +#endif /* WOLFCERT_ERRORS_H */ diff --git a/src/test/unit/mocks/wolfcert/types.h b/src/test/unit/mocks/wolfcert/types.h new file mode 100644 index 00000000..a10f60fe --- /dev/null +++ b/src/test/unit/mocks/wolfcert/types.h @@ -0,0 +1,36 @@ +/* Mock wolfcert/types.h for unit tests. + * Only the transport vtable src/port/wolfcert_io.c implements; the real + * header needs wolfcert/options.h, which a wolfIP-only build cannot generate. + */ +#ifndef WOLFCERT_TYPES_H +#define WOLFCERT_TYPES_H + +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Pluggable transport, carrying TLS records and plain HTTP alike. */ +typedef struct WolfCertTransport { + /* Return WOLFCERT_OK with the handle stored in *conn, else a negative + * WOLFCERT_ERR_*. *conn is opaque and never NULL-tested, so 0 is valid. */ + int (*connect)(void* ctx, const char* host, int port, + int timeout_ms, void** conn); + /* Bytes moved, or a negative WOLFCERT_ERR_*; never 0 (orderly close is + * CONN_CLOSED). */ + int (*read)(void* ctx, void* conn, uint8_t* buf, size_t len, + int timeout_ms); + int (*write)(void* ctx, void* conn, const uint8_t* buf, size_t len, + int timeout_ms); + /* Runs exactly once per successful connect, error paths included. */ + int (*disconnect)(void* ctx, void* conn); + void* ctx; /* transport-wide, e.g. the stack instance */ +} WolfCertTransport; + +#ifdef __cplusplus +} +#endif + +#endif /* WOLFCERT_TYPES_H */ diff --git a/src/test/unit/unit.c b/src/test/unit/unit.c index 70b5f099..0c89c09e 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -38,17 +38,19 @@ #include "unit_tests_dns_edges.c" #include "unit_tests_misc_edges.c" #include "unit_tests_vlan.c" +#include "unit_tests_wolfcert.c" Suite *wolf_suite(void) { Suite *s; - TCase *tc_core, *tc_proto, *tc_utils, *tc_wolfssl; + TCase *tc_core, *tc_proto, *tc_utils, *tc_wolfssl, *tc_wolfcert; s = suite_create("wolfIP"); tc_core = tcase_create("Core"); tc_utils = tcase_create("Utils"); tc_proto = tcase_create("Protocols"); tc_wolfssl = tcase_create("wolfSSL-IO"); + tc_wolfcert = tcase_create("wolfCert-IO"); tcase_add_test(tc_core, test_fifo_init); @@ -1065,6 +1067,39 @@ Suite *wolf_suite(void) tcase_add_test(tc_wolfssl, test_wolfssh_io_send_behaviors); tcase_add_test(tc_wolfssl, test_wolfssh_io_recv_behaviors); + tcase_add_test(tc_wolfcert, test_wolfcert_io_init_populates_vtable); + tcase_add_test(tc_wolfcert, test_wolfcert_io_init_rejects_bad_args); + tcase_add_test(tc_wolfcert, test_wolfcert_io_init_pool_exhaustion); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_ip_literal); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_retries_eagain); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_deadline_closes_socket); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_hard_error_closes_socket); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_default_timeout_bounds_spin); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_resolves_name); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_rejects_bad_args); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_dns_busy_then_resolves); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_dns_busy_hits_deadline); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_ignores_stale_dns_answer); + tcase_add_test(tc_wolfcert, test_wolfcert_io_write_blocking_deadline); + tcase_add_test(tc_wolfcert, test_wolfcert_io_write_blocking_pumps); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_socket_failure); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_dns_send_eagain_retries); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_dns_hard_error); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_dns_answer_is_zero); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_dns_never_answers); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_unusable_ip_literal); + tcase_add_test(tc_wolfcert, test_wolfcert_io_read_nonblocking_maps_eagain); + tcase_add_test(tc_wolfcert, test_wolfcert_io_read_blocking_pumps_instead_of_want_read); + tcase_add_test(tc_wolfcert, test_wolfcert_io_read_maps_close_and_reset); + tcase_add_test(tc_wolfcert, test_wolfcert_io_read_blocking_deadline); + tcase_add_test(tc_wolfcert, test_wolfcert_io_write_short_write_and_eagain); + tcase_add_test(tc_wolfcert, test_wolfcert_io_rw_reject_bad_args); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_rejects_out_of_range_octets); + tcase_add_test(tc_wolfcert, test_wolfcert_io_disconnect_closes_once); + tcase_add_test(tc_wolfcert, test_wolfcert_io_disconnect_completes_async_close); + tcase_add_test(tc_wolfcert, test_wolfcert_io_disconnect_deadline); + tcase_add_test(tc_wolfcert, test_wolfcert_io_handle_survives_round_trip); + /* Branch-coverage tests backported from the trimmed wolfIP suite. */ tcase_add_test(tc_core, test_socket_from_fd_invalid_inputs); tcase_add_test(tc_core, test_can_read_write_icmp_socket); @@ -1765,6 +1800,7 @@ Suite *wolf_suite(void) suite_add_tcase(s, tc_utils); suite_add_tcase(s, tc_proto); suite_add_tcase(s, tc_wolfssl); + suite_add_tcase(s, tc_wolfcert); return s; } diff --git a/src/test/unit/unit_shared.c b/src/test/unit/unit_shared.c index a6c51c09..da7f63d8 100644 --- a/src/test/unit/unit_shared.c +++ b/src/test/unit/unit_shared.c @@ -327,6 +327,222 @@ void *wolfSSH_GetIOReadCtx(WOLFSSH *ssh) #undef io_desc_alloc #undef io_desc_free +/* wolfCert IO glue mocks. The transport drives the stack itself, so it needs + * socket, connect, close and poll on top of the recv/send pair above. */ +#include + +static int wc_socket_ret; +static int wc_socket_calls; +static int wc_connect_steps[8]; +static int wc_connect_steps_len; +static int wc_connect_step; +static int wc_recv_steps[8]; +static int wc_recv_steps_len; +static int wc_recv_step; +static int wc_send_steps[8]; +static int wc_send_steps_len; +static int wc_send_step; +static uint32_t wc_connect_last_ip; +static uint16_t wc_connect_last_port; +static size_t wc_recv_last_len; +static size_t wc_send_last_len; +static int wc_close_calls; +static int wc_close_ret; +static int wc_close_eagain_left; +static int wc_poll_calls; +static int wc_nslookup_ret; +static int wc_nslookup_steps[8]; +static int wc_nslookup_steps_len; +static int wc_nslookup_step; +static uint32_t wc_nslookup_ip; +static int wc_nslookup_answer; +static void (*wc_dns_cb)(uint32_t ip); +static int wc_dns_pending; +static uint32_t wc_dns_ips[4]; +static int wc_dns_ips_len; +static int wc_dns_ip_idx; +static uint64_t wc_fake_now; +static uint64_t wc_now_step_ms; + +static int wc_next_step(const int *steps, int len, int *cursor, int dflt) +{ + if (len <= 0) + return dflt; + if (*cursor < len) + return steps[(*cursor)++]; + return steps[len - 1]; +} + +static uint64_t test_wc_now_ms(void) +{ + return wc_fake_now; +} + +static int test_wc_sock_socket(struct wolfIP *s, int domain, int type, int proto) +{ + (void)s; + (void)domain; + (void)type; + (void)proto; + wc_socket_calls++; + return wc_socket_ret; +} + +static int test_wc_sock_connect(struct wolfIP *s, int fd, + const struct wolfIP_sockaddr *addr, + socklen_t addrlen) +{ + const struct wolfIP_sockaddr_in *sin; + + (void)s; + (void)fd; + (void)addrlen; + if (addr != NULL) { + sin = (const struct wolfIP_sockaddr_in *)addr; + /* Read it back the way wolfIP_sock_connect does, so a dropped or + * doubled ee16/ee32 shows up here. */ + wc_connect_last_ip = ee32(sin->sin_addr.s_addr); + wc_connect_last_port = ee16(sin->sin_port); + } + return wc_next_step(wc_connect_steps, wc_connect_steps_len, + &wc_connect_step, 0); +} + +static int test_wc_sock_recv(struct wolfIP *s, int fd, void *buf, size_t len, + int flags) +{ + int step; + + (void)s; + (void)fd; + (void)flags; + wc_recv_last_len = len; + step = wc_next_step(wc_recv_steps, wc_recv_steps_len, &wc_recv_step, 0); + if ((step > 0) && (buf != NULL)) { + if ((size_t)step > len) + step = (int)len; + memset(buf, 'x', (size_t)step); + } + return step; +} + +static int test_wc_sock_send(struct wolfIP *s, int fd, const void *buf, + size_t len, int flags) +{ + int step; + + (void)s; + (void)fd; + (void)buf; + (void)flags; + wc_send_last_len = len; + step = wc_next_step(wc_send_steps, wc_send_steps_len, &wc_send_step, 0); + if ((step > 0) && ((size_t)step > len)) + step = (int)len; + return step; +} + +static int test_wc_sock_close(struct wolfIP *s, int fd) +{ + (void)s; + (void)fd; + wc_close_calls++; + if (wc_close_eagain_left > 0) { + wc_close_eagain_left--; + return -WOLFIP_EAGAIN; + } + return wc_close_ret; +} + +static int test_wc_poll(struct wolfIP *s, uint64_t now) +{ + (void)s; + (void)now; + wc_poll_calls++; + /* Advancing here is what lets a deadline expire inside a pump loop. */ + wc_fake_now += wc_now_step_ms; + if ((wc_dns_pending != 0) && (wc_nslookup_answer != 0) && + (wc_dns_cb != NULL)) { + uint32_t ip = wc_nslookup_ip; + if (wc_dns_ips_len > 0) { + ip = wc_dns_ips[wc_dns_ip_idx]; + if (wc_dns_ip_idx < (wc_dns_ips_len - 1)) + wc_dns_ip_idx++; + } + wc_dns_pending = 0; + wc_dns_cb(ip); + } + return 0; +} + +static int test_wc_nslookup(struct wolfIP *s, const char *name, uint16_t *id, + void (*cb)(uint32_t ip)) +{ + int ret; + + (void)s; + (void)name; + if (id != NULL) + *id = 1; + ret = wc_next_step(wc_nslookup_steps, wc_nslookup_steps_len, + &wc_nslookup_step, wc_nslookup_ret); + if (ret == 0) { + wc_dns_cb = cb; + wc_dns_pending = 1; + } + return ret; +} + +#define wolfIP_sock_socket test_wc_sock_socket +#define wolfIP_sock_connect test_wc_sock_connect +#define wolfIP_sock_recv test_wc_sock_recv +#define wolfIP_sock_send test_wc_sock_send +#define wolfIP_sock_close test_wc_sock_close +#define wolfIP_poll test_wc_poll +#define nslookup test_wc_nslookup +#include "../../port/wolfcert_io.c" +#undef wolfIP_sock_socket +#undef wolfIP_sock_connect +#undef wolfIP_sock_recv +#undef wolfIP_sock_send +#undef wolfIP_sock_close +#undef wolfIP_poll +#undef nslookup + +static void reset_wolfcert_io_state(void) +{ + memset(io_ctxs, 0, sizeof(io_ctxs)); + wc_socket_ret = 0x100; + wc_socket_calls = 0; + wc_connect_steps_len = 0; + wc_connect_step = 0; + wc_recv_steps_len = 0; + wc_recv_step = 0; + wc_send_steps_len = 0; + wc_send_step = 0; + wc_connect_last_ip = 0; + wc_connect_last_port = 0; + wc_recv_last_len = 0; + wc_send_last_len = 0; + wc_close_calls = 0; + wc_close_ret = 0; + wc_close_eagain_left = 0; + wc_poll_calls = 0; + wc_nslookup_ret = 0; + wc_nslookup_steps_len = 0; + wc_nslookup_step = 0; + wc_nslookup_ip = 0x0A000001; + wc_nslookup_answer = 1; + wc_dns_cb = NULL; + wc_dns_pending = 0; + wc_dns_ips_len = 0; + wc_dns_ip_idx = 0; + wc_fake_now = 0; + wc_now_step_ms = 1; + dns_result_ip = 0; + dns_result_ready = 0; +} + static void reset_wolfssh_io_state(void) { memset(wolfssh_io_descs, 0, sizeof(wolfssh_io_descs)); diff --git a/src/test/unit/unit_tests_wolfcert.c b/src/test/unit/unit_tests_wolfcert.c new file mode 100644 index 00000000..a6389e63 --- /dev/null +++ b/src/test/unit/unit_tests_wolfcert.c @@ -0,0 +1,860 @@ +/* unit_tests_wolfcert.c + * + * Copyright (C) 2026 wolfSSL Inc. + * + * This file is part of wolfIP TCP/IP stack. + * + * wolfIP is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * wolfIP is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1335, USA + * + * Unit tests for the wolfIP <-> wolfCert transport glue. + */ + +/* A stand-in for the caller's stack pointer; the mocks never dereference it. */ +static struct wolfIP *wc_test_stack(void) +{ + static int dummy; + return (struct wolfIP *)&dummy; +} + +static void *wc_test_init(WolfCertTransport *t) +{ + return wolfCert_Init_wolfIP(t, wc_test_stack(), test_wc_now_ms); +} + +START_TEST(test_wolfcert_io_init_populates_vtable) +{ + WolfCertTransport t; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + + c = wc_test_init(&t); + ck_assert_ptr_ne(c, NULL); + ck_assert_ptr_eq(t.ctx, c); + ck_assert(t.connect != NULL); + ck_assert(t.read != NULL); + ck_assert(t.write != NULL); + ck_assert(t.disconnect != NULL); + /* Pure wiring: no socket is opened and the stack is not driven. */ + ck_assert_int_eq(wc_socket_calls, 0); + ck_assert_int_eq(wc_poll_calls, 0); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_init_rejects_bad_args) +{ + WolfCertTransport t; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + + ck_assert_ptr_eq(wolfCert_Init_wolfIP(NULL, wc_test_stack(), + test_wc_now_ms), NULL); + ck_assert_ptr_eq(wolfCert_Init_wolfIP(&t, NULL, test_wc_now_ms), NULL); + ck_assert_ptr_eq(wolfCert_Init_wolfIP(&t, wc_test_stack(), NULL), NULL); + wolfCert_Cleanup_wolfIP(NULL); +} +END_TEST + +START_TEST(test_wolfcert_io_init_pool_exhaustion) +{ + WolfCertTransport t[MAX_WOLFCERT_CTX + 1]; + void *c[MAX_WOLFCERT_CTX]; + int i; + + reset_wolfcert_io_state(); + memset(t, 0, sizeof(t)); + + for (i = 0; i < MAX_WOLFCERT_CTX; i++) { + c[i] = wc_test_init(&t[i]); + ck_assert_ptr_ne(c[i], NULL); + } + ck_assert_ptr_eq(wc_test_init(&t[MAX_WOLFCERT_CTX]), NULL); + + /* Cleanup returns the slot, so the next init succeeds again. */ + wolfCert_Cleanup_wolfIP(c[0]); + ck_assert_ptr_ne(wc_test_init(&t[MAX_WOLFCERT_CTX]), NULL); +} +END_TEST + +START_TEST(test_wolfcert_io_connect_ip_literal) +{ + WolfCertTransport t; + void *conn = NULL; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + wc_socket_ret = 0x105; + wc_connect_steps[0] = 0; + wc_connect_steps_len = 1; + + ck_assert_int_eq(t.connect(t.ctx, "10.0.0.1", 443, 5000, &conn), + WOLFCERT_OK); + ck_assert_int_eq((int)(intptr_t)conn, 0x105); + /* A dotted quad must not reach the resolver. */ + ck_assert_int_eq(dns_result_ready, 0); + /* ee16/ee32 are load-bearing: wolfIP_sock_connect reads the address back + * with ee32(), so a dropped or doubled swap sends us to a wrong host. */ + ck_assert_uint_eq(wc_connect_last_ip, 0x0A000001); + ck_assert_uint_eq(wc_connect_last_port, 443); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_connect_retries_eagain) +{ + WolfCertTransport t; + void *conn = NULL; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + wc_connect_steps[0] = -WOLFIP_EAGAIN; + wc_connect_steps[1] = -WOLFIP_EAGAIN; + wc_connect_steps[2] = 0; + wc_connect_steps_len = 3; + + ck_assert_int_eq(t.connect(t.ctx, "10.0.0.1", 443, 5000, &conn), + WOLFCERT_OK); + /* Each retry must drive the stack, or the handshake never completes. */ + ck_assert_int_eq(wc_poll_calls, 2); + ck_assert_int_eq(wc_close_calls, 0); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_connect_deadline_closes_socket) +{ + WolfCertTransport t; + void *conn = NULL; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + wc_connect_steps[0] = -WOLFIP_EAGAIN; + wc_connect_steps_len = 1; + wc_now_step_ms = 100; + + ck_assert_int_eq(t.connect(t.ctx, "10.0.0.1", 443, 250, &conn), + WOLFCERT_ERR_IO); + /* A failed connect owns its socket: wolfCert will not call disconnect. */ + ck_assert_int_eq(wc_close_calls, 1); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_connect_default_timeout_bounds_spin) +{ + WolfCertTransport t; + void *conn = NULL; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + /* A zero-initialised WolfCertServerCfg leaves timeout_ms at 0. wolfIP has + * no kernel to abandon an unanswered SYN, so the transport's own default + * must stop it well before the iteration backstop. */ + wc_connect_steps[0] = -WOLFIP_EAGAIN; + wc_connect_steps_len = 1; + wc_now_step_ms = 1000; + + ck_assert_int_eq(t.connect(t.ctx, "10.0.0.1", 443, 0, &conn), + WOLFCERT_ERR_IO); + ck_assert_int_eq(wc_close_calls, 1); + ck_assert_int_lt(wc_poll_calls, + (int)(WOLFCERT_WOLFIP_CONNECT_TIMEOUT_MS / 1000) + 2); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_connect_hard_error_closes_socket) +{ + WolfCertTransport t; + void *conn = NULL; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + wc_connect_steps[0] = -WOLFIP_EINVAL; + wc_connect_steps_len = 1; + + ck_assert_int_eq(t.connect(t.ctx, "10.0.0.1", 443, 5000, &conn), + WOLFCERT_ERR_IO); + ck_assert_int_eq(wc_close_calls, 1); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_connect_resolves_name) +{ + WolfCertTransport t; + void *conn = NULL; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + ck_assert_int_eq(t.connect(t.ctx, "est.example.com", 443, 5000, &conn), + WOLFCERT_OK); + ck_assert_int_eq(dns_result_ready, 1); + ck_assert_uint_eq(dns_result_ip, 0x0A000001); + ck_assert_uint_eq(wc_connect_last_ip, 0x0A000001); + ck_assert_uint_eq(wc_connect_last_port, 443); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +/* resolve_host's error branches. All of them precede socket creation, so a + * failure must leave no descriptor behind. */ +START_TEST(test_wolfcert_io_connect_dns_busy_then_resolves) +{ + WolfCertTransport t; + void *conn = NULL; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + /* wolfIP answers -16 while another query is in flight; that is a retry, + * not a failure. */ + wc_nslookup_steps[0] = -16; + wc_nslookup_steps[1] = -16; + wc_nslookup_steps[2] = 0; + wc_nslookup_steps_len = 3; + + ck_assert_int_eq(t.connect(t.ctx, "est.example.com", 443, 5000, &conn), + WOLFCERT_OK); + ck_assert_int_eq(dns_result_ready, 1); + ck_assert_int_gt(wc_poll_calls, 0); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +/* The one connect exit that returns before a socket exists: it must not + * close anything. */ +START_TEST(test_wolfcert_io_connect_socket_failure) +{ + WolfCertTransport t; + void *conn = NULL; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + wc_socket_ret = -1; + + ck_assert_int_eq(t.connect(t.ctx, "10.0.0.1", 443, 5000, &conn), + WOLFCERT_ERR_IO); + ck_assert_int_eq(wc_socket_calls, 1); + ck_assert_int_eq(wc_close_calls, 0); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +/* A full UDP tx FIFO makes nslookup() report -WOLFIP_EAGAIN after rolling the + * query back, so the next attempt can succeed. */ +START_TEST(test_wolfcert_io_connect_dns_send_eagain_retries) +{ + WolfCertTransport t; + void *conn = NULL; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + wc_nslookup_steps[0] = -WOLFIP_EAGAIN; + wc_nslookup_steps[1] = 0; + wc_nslookup_steps_len = 2; + + ck_assert_int_eq(t.connect(t.ctx, "est.example.com", 443, 5000, &conn), + WOLFCERT_OK); + ck_assert_int_eq(dns_result_ready, 1); + ck_assert_int_gt(wc_poll_calls, 0); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +/* An abandoned query can answer while we are still waiting to arm our own. + * That answer belongs to the previous host and must not become this one's + * address. */ +START_TEST(test_wolfcert_io_connect_ignores_stale_dns_answer) +{ + WolfCertTransport t; + void *conn = NULL; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + /* A query is already armed from a previous, timed-out connect. */ + wc_dns_pending = 1; + wc_dns_cb = dns_result_cb; + wc_dns_ips[0] = 0x0A0000FE; /* the stale answer, delivered first */ + wc_dns_ips[1] = 0x0A000001; /* ours, once we manage to arm it */ + wc_dns_ips_len = 2; + + wc_nslookup_steps[0] = -16; /* busy until the stale query clears */ + wc_nslookup_steps[1] = 0; + wc_nslookup_steps_len = 2; + + ck_assert_int_eq(t.connect(t.ctx, "est.example.com", 443, 5000, &conn), + WOLFCERT_OK); + ck_assert_uint_eq(wc_connect_last_ip, 0x0A000001); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +/* A blocking write retries through -WOLFIP_EAGAIN instead of reporting it. */ +START_TEST(test_wolfcert_io_write_blocking_pumps) +{ + WolfCertTransport t; + uint8_t buf[16]; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + memset(buf, 'a', sizeof(buf)); + c = wc_test_init(&t); + + wc_send_steps[0] = -WOLFIP_EAGAIN; + wc_send_steps[1] = -WOLFIP_EAGAIN; + wc_send_steps[2] = 6; + wc_send_steps_len = 3; + + ck_assert_int_eq(t.write(t.ctx, (void *)(intptr_t)0x100, buf, sizeof(buf), + -1), 6); + ck_assert_int_eq(wc_poll_calls, 2); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +/* A blocking write that never completes stops at the default budget. */ +START_TEST(test_wolfcert_io_write_blocking_deadline) +{ + WolfCertTransport t; + uint8_t buf[16]; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + memset(buf, 'a', sizeof(buf)); + c = wc_test_init(&t); + + wc_send_steps[0] = -WOLFIP_EAGAIN; + wc_send_steps_len = 1; + wc_now_step_ms = WOLFCERT_WOLFIP_IO_TIMEOUT_MS; + + ck_assert_int_eq(t.write(t.ctx, (void *)(intptr_t)0x100, buf, sizeof(buf), + -1), WOLFCERT_ERR_IO); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_connect_dns_busy_hits_deadline) +{ + WolfCertTransport t; + void *conn = NULL; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + wc_nslookup_ret = -16; + wc_now_step_ms = 100; + + ck_assert_int_eq(t.connect(t.ctx, "est.example.com", 443, 250, &conn), + WOLFCERT_ERR_IO); + ck_assert_int_eq(wc_socket_calls, 0); + ck_assert_int_eq(wc_close_calls, 0); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_connect_dns_hard_error) +{ + WolfCertTransport t; + void *conn = NULL; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + /* Anything other than the busy code is fatal and must not be retried. */ + wc_nslookup_ret = -22; + + ck_assert_int_eq(t.connect(t.ctx, "est.example.com", 443, 5000, &conn), + WOLFCERT_ERR_IO); + ck_assert_int_eq(wc_poll_calls, 0); + ck_assert_int_eq(wc_socket_calls, 0); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_connect_dns_answer_is_zero) +{ + WolfCertTransport t; + void *conn = NULL; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + /* An A record of 0.0.0.0 reaches the callback like any other; it must + * not be dialled. A name that does not exist never answers at all. */ + wc_nslookup_ip = 0; + + ck_assert_int_eq(t.connect(t.ctx, "est.example.com", 443, 5000, &conn), + WOLFCERT_ERR_NOT_FOUND); + ck_assert_int_eq(wc_socket_calls, 0); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_connect_dns_never_answers) +{ + WolfCertTransport t; + void *conn = NULL; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + /* Query accepted, callback never fires: the wait loop owns the deadline. */ + wc_nslookup_answer = 0; + wc_now_step_ms = 100; + + ck_assert_int_eq(t.connect(t.ctx, "est.example.com", 443, 250, &conn), + WOLFCERT_ERR_IO); + ck_assert_int_eq(dns_result_ready, 0); + ck_assert_int_eq(wc_socket_calls, 0); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_connect_unusable_ip_literal) +{ + WolfCertTransport t; + void *conn = NULL; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + /* Parses as a literal but yields no usable address; must not fall through + * to the resolver, and must not open a socket. */ + ck_assert_int_eq(t.connect(t.ctx, "0.0.0.0", 443, 5000, &conn), + WOLFCERT_ERR_BAD_ARG); + ck_assert_int_eq(dns_result_ready, 0); + ck_assert_int_eq(wc_socket_calls, 0); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_connect_rejects_bad_args) +{ + WolfCertTransport t; + void *conn = NULL; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + ck_assert_int_eq(t.connect(t.ctx, NULL, 443, 5000, &conn), + WOLFCERT_ERR_BAD_ARG); + ck_assert_int_eq(t.connect(t.ctx, "10.0.0.1", 443, 5000, NULL), + WOLFCERT_ERR_BAD_ARG); + ck_assert_int_eq(t.connect(t.ctx, "", 443, 5000, &conn), + WOLFCERT_ERR_BAD_ARG); + ck_assert_int_eq(t.connect(t.ctx, "10.0.0.1", 0, 5000, &conn), + WOLFCERT_ERR_BAD_ARG); + ck_assert_int_eq(t.connect(t.ctx, "10.0.0.1", 65536, 5000, &conn), + WOLFCERT_ERR_BAD_ARG); + ck_assert_int_eq(t.connect(NULL, "10.0.0.1", 443, 5000, &conn), + WOLFCERT_ERR_BAD_ARG); + ck_assert_int_eq(wc_socket_calls, 0); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_read_nonblocking_maps_eagain) +{ + WolfCertTransport t; + uint8_t buf[16]; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + wc_recv_steps[0] = -WOLFIP_EAGAIN; + wc_recv_steps_len = 1; + + ck_assert_int_eq(t.read(t.ctx, (void *)(intptr_t)0x100, buf, sizeof(buf), 0), + WOLFCERT_ERR_WANT_READ); + /* A non-blocking caller pumps the stack itself; the glue must not. */ + ck_assert_int_eq(wc_poll_calls, 0); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_read_blocking_pumps_instead_of_want_read) +{ + WolfCertTransport t; + uint8_t buf[16]; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + wc_recv_steps[0] = -WOLFIP_EAGAIN; + wc_recv_steps[1] = -WOLFIP_EAGAIN; + wc_recv_steps[2] = 4; + wc_recv_steps_len = 3; + + /* wolfCert's blocking path turns any non-positive return into a generic + * IO error, so WANT_READ here would break plain-HTTP SCEP. */ + ck_assert_int_eq(t.read(t.ctx, (void *)(intptr_t)0x100, buf, sizeof(buf), + -1), 4); + ck_assert_int_eq(wc_poll_calls, 2); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_read_maps_close_and_reset) +{ + WolfCertTransport t; + uint8_t buf[16]; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + /* An orderly close is a 0 from wolfIP; it must never reach wolfCert. */ + wc_recv_steps[0] = 0; + wc_recv_steps_len = 1; + ck_assert_int_eq(t.read(t.ctx, (void *)(intptr_t)0x100, buf, sizeof(buf), + -1), WOLFCERT_ERR_CONN_CLOSED); + + /* A bare -1 means the socket left ESTABLISHED/CLOSE_WAIT. Retrying it as + * would-block would spin the caller and starve wolfIP_poll(). */ + wc_recv_step = 0; + wc_recv_steps[0] = -1; + ck_assert_int_eq(t.read(t.ctx, (void *)(intptr_t)0x100, buf, sizeof(buf), + -1), WOLFCERT_ERR_CONN_CLOSED); + + /* A rejected argument is neither a close nor would-block. */ + wc_recv_step = 0; + wc_recv_steps[0] = -WOLFIP_EINVAL; + ck_assert_int_eq(t.read(t.ctx, (void *)(intptr_t)0x100, buf, sizeof(buf), + -1), WOLFCERT_ERR_BAD_ARG); + + /* A length past INT_MAX is clamped before it reaches the stack, whose + * transfer count is an int. */ + wc_recv_step = 0; + wc_recv_steps[0] = 4; + ck_assert_int_eq(t.read(t.ctx, (void *)(intptr_t)0x100, buf, + (size_t)INT_MAX + 1, -1), 4); + ck_assert_uint_eq((unsigned long long)wc_recv_last_len, + (unsigned long long)INT_MAX); + + /* Anything else is a plain IO failure. */ + wc_recv_step = 0; + wc_recv_steps[0] = -WOLFIP_ENOMEM; + ck_assert_int_eq(t.read(t.ctx, (void *)(intptr_t)0x100, buf, sizeof(buf), + -1), WOLFCERT_ERR_IO); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_read_blocking_deadline) +{ + WolfCertTransport t; + uint8_t buf[16]; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + wc_recv_steps[0] = -WOLFIP_EAGAIN; + wc_recv_steps_len = 1; + wc_now_step_ms = WOLFCERT_WOLFIP_IO_TIMEOUT_MS; + + ck_assert_int_eq(t.read(t.ctx, (void *)(intptr_t)0x100, buf, sizeof(buf), + -1), WOLFCERT_ERR_IO); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_write_short_write_and_eagain) +{ + WolfCertTransport t; + uint8_t buf[16]; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + memset(buf, 'a', sizeof(buf)); + c = wc_test_init(&t); + + /* A short write is returned verbatim; wolfCert loops on the remainder. */ + wc_send_steps[0] = 6; + wc_send_steps_len = 1; + ck_assert_int_eq(t.write(t.ctx, (void *)(intptr_t)0x100, buf, sizeof(buf), + -1), 6); + + wc_send_step = 0; + wc_send_steps[0] = -WOLFIP_EAGAIN; + ck_assert_int_eq(t.write(t.ctx, (void *)(intptr_t)0x100, buf, sizeof(buf), + 0), WOLFCERT_ERR_WANT_WRITE); + + wc_send_step = 0; + wc_send_steps[0] = -WOLFIP_EINVAL; + ck_assert_int_eq(t.write(t.ctx, (void *)(intptr_t)0x100, buf, sizeof(buf), + -1), WOLFCERT_ERR_BAD_ARG); + + wc_send_step = 0; + wc_send_steps[0] = -1; + ck_assert_int_eq(t.write(t.ctx, (void *)(intptr_t)0x100, buf, sizeof(buf), + -1), WOLFCERT_ERR_CONN_CLOSED); + + wc_send_step = 0; + wc_send_steps[0] = -WOLFIP_ENOMEM; + ck_assert_int_eq(t.write(t.ctx, (void *)(intptr_t)0x100, buf, sizeof(buf), + -1), WOLFCERT_ERR_IO); + + /* A length past INT_MAX is clamped before it reaches the stack. */ + wc_send_step = 0; + wc_send_steps[0] = 4; + ck_assert_int_eq(t.write(t.ctx, (void *)(intptr_t)0x100, buf, + (size_t)INT_MAX + 1, -1), 4); + ck_assert_uint_eq((unsigned long long)wc_send_last_len, + (unsigned long long)INT_MAX); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_rw_reject_bad_args) +{ + WolfCertTransport t; + uint8_t buf[16]; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + ck_assert_int_eq(t.read(t.ctx, (void *)(intptr_t)0x100, NULL, sizeof(buf), + 0), WOLFCERT_ERR_BAD_ARG); + ck_assert_int_eq(t.read(t.ctx, (void *)(intptr_t)0x100, buf, 0, 0), + WOLFCERT_ERR_BAD_ARG); + ck_assert_int_eq(t.read(NULL, (void *)(intptr_t)0x100, buf, sizeof(buf), 0), + WOLFCERT_ERR_BAD_ARG); + ck_assert_int_eq(t.write(t.ctx, (void *)(intptr_t)0x100, NULL, sizeof(buf), + 0), WOLFCERT_ERR_BAD_ARG); + ck_assert_int_eq(t.write(t.ctx, (void *)(intptr_t)0x100, buf, 0, 0), + WOLFCERT_ERR_BAD_ARG); + ck_assert_int_eq(t.write(NULL, (void *)(intptr_t)0x100, buf, sizeof(buf), + 0), WOLFCERT_ERR_BAD_ARG); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +/* An out-of-range octet is not a literal: it must reach the resolver rather + * than reaching atoip4(), which does not validate and yields a wrong address. */ +START_TEST(test_wolfcert_io_connect_rejects_out_of_range_octets) +{ + WolfCertTransport t; + void *conn = NULL; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + ck_assert_int_eq(t.connect(t.ctx, "300.1.1.1", 443, 5000, &conn), + WOLFCERT_OK); + ck_assert_int_eq(dns_result_ready, 1); + + dns_result_ready = 0; + ck_assert_int_eq(t.connect(t.ctx, "1.2.3.256", 443, 5000, &conn), + WOLFCERT_OK); + ck_assert_int_eq(dns_result_ready, 1); + + dns_result_ready = 0; + ck_assert_int_eq(t.connect(t.ctx, "1.2.3", 443, 5000, &conn), + WOLFCERT_OK); + ck_assert_int_eq(dns_result_ready, 1); + + dns_result_ready = 0; + ck_assert_int_eq(t.connect(t.ctx, "1..2.3", 443, 5000, &conn), + WOLFCERT_OK); + ck_assert_int_eq(dns_result_ready, 1); + + /* Four digits, but a value the octet bound would accept. */ + dns_result_ready = 0; + ck_assert_int_eq(t.connect(t.ctx, "0001.1.1.1", 443, 5000, &conn), + WOLFCERT_OK); + ck_assert_int_eq(dns_result_ready, 1); + + /* A valid literal still bypasses the resolver. */ + dns_result_ready = 0; + ck_assert_int_eq(t.connect(t.ctx, "10.0.0.1", 443, 5000, &conn), + WOLFCERT_OK); + ck_assert_int_eq(dns_result_ready, 0); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_disconnect_closes_once) +{ + WolfCertTransport t; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + ck_assert_int_eq(t.disconnect(t.ctx, (void *)(intptr_t)0x100), + WOLFCERT_OK); + ck_assert_int_eq(wc_close_calls, 1); + + wc_close_ret = -1; + ck_assert_int_eq(t.disconnect(t.ctx, (void *)(intptr_t)0x100), + WOLFCERT_ERR_IO); + ck_assert_int_eq(t.disconnect(NULL, (void *)(intptr_t)0x100), + WOLFCERT_ERR_BAD_ARG); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +/* Closing an established socket runs a FIN handshake, so close reports + * -WOLFIP_EAGAIN first. Nothing calls disconnect twice. */ +START_TEST(test_wolfcert_io_disconnect_completes_async_close) +{ + WolfCertTransport t; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + wc_close_eagain_left = 3; + + ck_assert_int_eq(t.disconnect(t.ctx, (void *)(intptr_t)0x100), + WOLFCERT_OK); + ck_assert_int_eq(wc_close_calls, 4); + ck_assert_int_gt(wc_poll_calls, 0); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +/* A FIN handshake that never completes must still return, or a stalled peer + * hangs the caller. */ +START_TEST(test_wolfcert_io_disconnect_deadline) +{ + WolfCertTransport t; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + wc_close_eagain_left = 1000000; + wc_now_step_ms = WOLFCERT_WOLFIP_IO_TIMEOUT_MS; + + ck_assert_int_eq(t.disconnect(t.ctx, (void *)(intptr_t)0x100), + WOLFCERT_ERR_IO); + ck_assert_int_lt(wc_close_calls, 5); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_handle_survives_round_trip) +{ + WolfCertTransport t; + void *conn = NULL; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + /* wolfIP marks every TCP descriptor, so the handle is opaque to wolfCert + * and must come back to wolfIP_sock_close() unchanged. */ + wc_socket_ret = 0x100 | 3; + ck_assert_int_eq(t.connect(t.ctx, "10.0.0.1", 8080, 0, &conn), + WOLFCERT_OK); + ck_assert_int_eq((int)(intptr_t)conn, 0x103); + ck_assert_int_eq(t.disconnect(t.ctx, conn), WOLFCERT_OK); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST diff --git a/wolfip.h b/wolfip.h index 44c32e6e..6f7ad90c 100644 --- a/wolfip.h +++ b/wolfip.h @@ -631,4 +631,15 @@ static inline void iptoa(ip4 ip, char *buf) #endif /* WOLFIP_ESP */ #endif /* WOLFSSL_WOLFIP */ +#ifdef WOLFCERT_WOLFIP + #include + + /* Fill in a caller-owned WolfCertTransport over wolfIP sockets; returns + * the context for wolfCert_Cleanup_wolfIP(), or NULL on failure. + * `now_ms` is the clock the application already feeds wolfIP_poll(). */ + void *wolfCert_Init_wolfIP(WolfCertTransport *t, struct wolfIP *stack, + uint64_t (*now_ms)(void)); + void wolfCert_Cleanup_wolfIP(void *context); +#endif /* WOLFCERT_WOLFIP */ + #endif /* !WOLFIP_H */