diff --git a/Makefile b/Makefile index 6a42f664..5f46bf7e 100644 --- a/Makefile +++ b/Makefile @@ -126,6 +126,7 @@ ifneq ($(CHECK_PKG_CFLAGS),) UNIT_CFLAGS+=$(CHECK_PKG_CFLAGS) endif UNIT_CFLAGS+=-Isrc/test/unit/mocks +UNIT_CFLAGS+=-DWOLFCERT_WOLFIP CPPCHECK=cppcheck CPPCHECK_FLAGS=--enable=warning,performance,portability,missingInclude \ @@ -979,7 +980,9 @@ UNIT_TEST_SRCS:=src/test/unit/unit.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_forwarding.c + src/test/unit/unit_tests_forwarding.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 cc2e493b..055e75c4 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..b921bf9d --- /dev/null +++ b/docs/wolfcert_howto.md @@ -0,0 +1,202 @@ +# 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); +``` + +`MAX_WOLFCERT_CTX` (default 2, in `src/port/wolfcert_io.c`) sizes the static +context pool; the two timeout knobs are in +[section 6](#6-timeouts-and-the-poll-loop). + +## 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 transport's only +clock and bounds every timeout, so one that never moves leaves the transport +waiting with no deadline. + +## 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 the resolver +answers 0.0.0.0, `WOLFCERT_ERR_CONN_CLOSED` when the peer refuses the +connection, and `WOLFCERT_ERR_IO` for every other failure, including a name +that never resolves. + +The connection handle packs the wolfIP descriptor with the socket's local port +and a fold of its peer address. Every call first checks that the socket still +matches it: once the socket was reset or handed to another connection, `read` +and `write` fail with `WOLFCERT_ERR_IO` and `disconnect` leaves it alone. +Otherwise `disconnect` closes it, and resets the connection instead when the +FIN cannot be queued. The check cannot tell ours from a new connection to the +same server that drew the same local port. + +## 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.** `connect` runs to completion before returning, so under +wolfIP's single-threaded model 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, +so your own loop does not run until they return. wolfIP's socket callbacks +still do — including your application's, from inside the transport's wait. +So do not call wolfCert from inside a wolfIP socket callback: these calls +would then run `wolfIP_poll()` inside itself. They poll back to back without +sleeping, so the CPU stays busy until they return. Two compile-time defaults +bound these calls: + +| Macro | Default | Applies to | +|---|---|---| +| `WOLFCERT_WOLFIP_CONNECT_TIMEOUT_MS` | 30000 | `connect`, including resolution | +| `WOLFCERT_WOLFIP_IO_TIMEOUT_MS` | 30000 | blocking `read`/`write` | + +**On an MCU, keep both inside your watchdog period** — a few seconds. +`cfg.timeout_ms` overrides the connect default; blocking reads and writes +always use `WOLFCERT_WOLFIP_IO_TIMEOUT_MS`. A refused connection fails at +once; an unreachable peer, which never answers, costs the full budget. + +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, section 8](tls_howto.md#8-non-blocking-handshakes-and-the-poll-loop). +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](#2-building-with-wolfcert-support). + +**`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 not answering, or the +name does not resolve; check the route and the DNS server. + +**Reads or writes fail with an I/O error.** The peer reset the connection, or +a blocking call waited longer than `WOLFCERT_WOLFIP_IO_TIMEOUT_MS`; raise it. + +**A connect never returns.** `now_ms` is not advancing; see +[section 3](#3-registering-the-transport). + +**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..12744862 --- /dev/null +++ b/src/port/wolfcert_io.c @@ -0,0 +1,416 @@ +/* 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. */ +#ifndef WOLFCERT_WOLFIP_IO_TIMEOUT_MS + #define WOLFCERT_WOLFIP_IO_TIMEOUT_MS 30000 +#endif + +/* Cap on a connect the caller left unbounded. */ +#ifndef WOLFCERT_WOLFIP_CONNECT_TIMEOUT_MS + #define WOLFCERT_WOLFIP_CONNECT_TIMEOUT_MS 30000 +#endif + +#define WOLFCERT_WOLFIP_DNS_BUSY (-16) + +/* State shared by every connection made through one transport. */ +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]; + +/* Where dns_result_cb() delivers the answer. 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 well-formed dotted quad or a name for the + * resolver. */ +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; + /* Retry on busy and on EAGAIN; any other failure is final. */ + 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()); + } +} + +/* Only -WOLFIP_EAGAIN is reported as would-block; everything else ends the + * transfer. */ +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; +} + +/* Pack the slot, the local port and an 8-bit fold of the peer address into + * one value. */ +static uintptr_t conn_handle(struct wolfcert_io_ctx *c, int fd) +{ + struct wolfIP_sockaddr_in local; + struct wolfIP_sockaddr_in peer; + socklen_t len = sizeof(local); + uint32_t ip; + uint16_t port; + uint8_t fold; + + memset(&local, 0, sizeof(local)); + memset(&peer, 0, sizeof(peer)); + if (wolfIP_sock_getsockname(c->stack, fd, (struct wolfIP_sockaddr *)&local, + &len) != 0) + return 0; + if (wolfIP_sock_getpeername(c->stack, fd, (struct wolfIP_sockaddr *)&peer, + &len) != 0) + return 0; + ip = ee32(peer.sin_addr.s_addr); + port = ee16(peer.sin_port); + fold = (uint8_t)(ip ^ (ip >> 8) ^ (ip >> 16) ^ (ip >> 24) ^ + port ^ (port >> 8)); + return ((uintptr_t)ee16(local.sin_port) << 16) | ((uintptr_t)fold << 8) | + (uintptr_t)SOCKET_UNMARK(fd); +} + +static int conn_fd(void *conn) +{ + return (int)(((uintptr_t)conn & 0xFFU) | MARK_TCP_SOCKET); +} + +/* A slot reset or handed to another socket no longer matches the handle, + * and a handle without a local port never names a live connection. */ +static int conn_is_ours(struct wolfcert_io_ctx *c, void *conn) +{ + if (((uintptr_t)conn >> 16) == 0) + return 0; + return (conn_handle(c, conn_fd(conn)) == (uintptr_t)conn); +} + +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; + uintptr_t handle = 0; + int owned = 1; + 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); + + ret = WOLFCERT_ERR_IO; + for (;;) { + rc = wolfIP_sock_connect(c->stack, fd, + (struct wolfIP_sockaddr *)&addr, sizeof(addr)); + if (handle == 0) + handle = conn_handle(c, fd); + if (rc != -WOLFIP_EAGAIN) + break; + if (deadline_expired(c, start, timeout_ms, + WOLFCERT_WOLFIP_CONNECT_TIMEOUT_MS)) + break; + (void)wolfIP_poll(c->stack, c->now_ms()); + if (!conn_is_ours(c, (void *)handle)) { + owned = 0; + ret = WOLFCERT_ERR_CONN_CLOSED; + break; + } + } + if ((rc == 0) && conn_is_ours(c, (void *)handle)) + ret = WOLFCERT_OK; + + /* Close only a socket that still matches the handle. */ + if (ret != WOLFCERT_OK) { + if (owned) + (void)wolfIP_sock_close(c->stack, fd); + return ret; + } + + *conn = (void *)handle; + 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 = conn_fd(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 (;;) { + if (!conn_is_ours(c, conn)) + return WOLFCERT_ERR_IO; + 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; + if (deadline_expired(c, start, timeout_ms, + WOLFCERT_WOLFIP_IO_TIMEOUT_MS)) + return WOLFCERT_ERR_IO; + (void)wolfIP_poll(c->stack, c->now_ms()); + } +} + +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 = conn_fd(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 (;;) { + if (!conn_is_ours(c, conn)) + return WOLFCERT_ERR_IO; + 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; + if (deadline_expired(c, start, timeout_ms, + WOLFCERT_WOLFIP_IO_TIMEOUT_MS)) + return WOLFCERT_ERR_IO; + (void)wolfIP_poll(c->stack, c->now_ms()); + } +} + +/* Start the close and leave the FIN handshake to the stack. */ +static int wolfcert_wolfip_disconnect(void *ctx, void *conn) +{ + struct wolfcert_io_ctx *c = (struct wolfcert_io_ctx *)ctx; + int fd = conn_fd(conn); + int rc; + + if (c == NULL) + return WOLFCERT_ERR_BAD_ARG; + if (!conn_is_ours(c, conn)) + return WOLFCERT_ERR_CONN_CLOSED; + + rc = wolfIP_sock_close(c->stack, fd); + /* The FIN could not be queued; reset the connection instead. */ + if ((rc == -WOLFIP_EAGAIN) && (wolfIP_sock_can_write(c->stack, fd) == 0)) + rc = wolfIP_sock_abort(c->stack, fd); + if ((rc == 0) || (rc == -WOLFIP_EAGAIN)) + return WOLFCERT_OK; + return WOLFCERT_ERR_IO; +} + +/* 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 25a6c90d..37929b60 100644 --- a/src/test/unit/unit.c +++ b/src/test/unit/unit.c @@ -39,17 +39,19 @@ #include "unit_tests_misc_edges.c" #include "unit_tests_vlan.c" #include "unit_tests_forwarding.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); @@ -1121,6 +1123,44 @@ 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_until_established); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_deadline_closes_socket); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_reset_is_not_rearmed); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_reused_slot_is_left_alone); + tcase_add_test(tc_wolfcert, test_wolfcert_io_connect_same_server_other_port_is_left_alone); + 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_empty_host); + 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); + tcase_add_test(tc_wolfcert, test_wolfcert_io_stale_handle_is_left_alone); + tcase_add_test(tc_wolfcert, test_wolfcert_io_read_stops_when_slot_changes); + tcase_add_test(tc_wolfcert, test_wolfcert_io_write_stops_when_slot_changes); + tcase_add_test(tc_wolfcert, test_wolfcert_io_read_blocking_deadline); + tcase_add_test(tc_wolfcert, test_wolfcert_io_rw_use_last_poll); + tcase_add_test(tc_wolfcert, test_wolfcert_io_write_short_write_and_eagain); + 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_leaves_fin_to_stack); + tcase_add_test(tc_wolfcert, test_wolfcert_io_disconnect_resets_unsent_fin); + /* 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); @@ -1854,6 +1894,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..d563b140 100644 --- a/src/test/unit/unit_shared.c +++ b/src/test/unit/unit_shared.c @@ -327,6 +327,300 @@ 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 int wc_connect_calls; +static int wc_peer_flip_at; +static uint32_t wc_peer_flip_ip; +static uint16_t wc_peer_flip_port; +static uint16_t wc_local_port; +static uint16_t wc_flip_local_port; +static int wc_close_calls; +static int wc_close_last_fd; +static int wc_close_ret; +static int wc_can_write_ret; +static int wc_abort_calls; +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; + wc_connect_calls++; + 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; + 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; + 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; +} + +/* Reports the peer connect() was given, until wc_peer_flip_at polls have run: + * then the slot reads as torn down (0.0.0.0:0) or as another caller's. */ +static int test_wc_sock_getpeername(struct wolfIP *s, int fd, + struct wolfIP_sockaddr *addr, + const socklen_t *addrlen) +{ + struct wolfIP_sockaddr_in *sin = (struct wolfIP_sockaddr_in *)addr; + uint32_t ip = wc_connect_last_ip; + uint16_t port = wc_connect_last_port; + + (void)s; + (void)fd; + (void)addrlen; + if ((wc_peer_flip_at > 0) && (wc_poll_calls >= wc_peer_flip_at)) { + ip = wc_peer_flip_ip; + port = wc_peer_flip_port; + } + sin->sin_family = AF_INET; + sin->sin_addr.s_addr = ee32(ip); + sin->sin_port = ee16(port); + return 0; +} + +/* Reports wc_local_port, or wc_flip_local_port once the peer has flipped. */ +static int test_wc_sock_getsockname(struct wolfIP *s, int fd, + struct wolfIP_sockaddr *addr, + const socklen_t *addrlen) +{ + struct wolfIP_sockaddr_in *sin = (struct wolfIP_sockaddr_in *)addr; + uint16_t port = wc_local_port; + + (void)s; + (void)fd; + (void)addrlen; + if ((wc_peer_flip_at > 0) && (wc_poll_calls >= wc_peer_flip_at)) + port = wc_flip_local_port; + sin->sin_family = AF_INET; + sin->sin_addr.s_addr = 0; + sin->sin_port = ee16(port); + return 0; +} + +static int test_wc_sock_close(struct wolfIP *s, int fd) +{ + (void)s; + wc_close_calls++; + wc_close_last_fd = fd; + return wc_close_ret; +} + +static int test_wc_sock_can_write(struct wolfIP *s, int fd) +{ + (void)s; + (void)fd; + return wc_can_write_ret; +} + +static int test_wc_sock_abort(struct wolfIP *s, int fd) +{ + (void)s; + (void)fd; + wc_abort_calls++; + return 0; +} + +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_sock_can_write test_wc_sock_can_write +#define wolfIP_sock_abort test_wc_sock_abort +#define wolfIP_sock_getpeername test_wc_sock_getpeername +#define wolfIP_sock_getsockname test_wc_sock_getsockname +#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_sock_can_write +#undef wolfIP_sock_abort +#undef wolfIP_sock_getpeername +#undef wolfIP_sock_getsockname +#undef wolfIP_poll +#undef nslookup + +/* The handle connect would return for fd, given the mocks' current state. */ +static void *wc_conn(void *ctx, int fd) +{ + return (void *)conn_handle((struct wolfcert_io_ctx *)ctx, fd); +} + +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_connect_calls = 0; + wc_peer_flip_at = 0; + wc_peer_flip_ip = 0; + wc_peer_flip_port = 0; + wc_local_port = 49731; + wc_flip_local_port = 0; + wc_close_calls = 0; + wc_close_last_fd = -1; + wc_close_ret = 0; + wc_can_write_ret = 1; + wc_abort_calls = 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..05cea77f --- /dev/null +++ b/src/test/unit/unit_tests_wolfcert.c @@ -0,0 +1,972 @@ +/* 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); + /* Local port 49731, peer fold 0xB1, slot 5. */ + ck_assert_uint_eq((unsigned long)(uintptr_t)conn, 0xC243B105UL); + /* A dotted quad must not reach the resolver. */ + ck_assert_int_eq(dns_result_ready, 0); + /* The stack receives the address and port we dialled. */ + ck_assert_uint_eq(wc_connect_last_ip, 0x0A000001); + ck_assert_uint_eq(wc_connect_last_port, 443); + + /* The handle decodes back to the same descriptor on disconnect. */ + ck_assert_int_eq(t.disconnect(t.ctx, conn), WOLFCERT_OK); + ck_assert_int_eq(wc_close_last_fd, 0x105); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +START_TEST(test_wolfcert_io_connect_retries_until_established) +{ + 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); + ck_assert_int_eq(wc_poll_calls, 2); + ck_assert_int_eq(wc_connect_calls, 3); + 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; the + * transport's own default must still bound the wait. */ + 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 + +/* The slot reads as torn down after the first poll: connect must stop without + * issuing the SYN again, and without closing it. */ +START_TEST(test_wolfcert_io_connect_reset_is_not_rearmed) +{ + 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_peer_flip_at = 1; /* the slot reads as 0.0.0.0:0 after it */ + + ck_assert_int_eq(t.connect(t.ctx, "10.0.0.1", 443, 5000, &conn), + WOLFCERT_ERR_CONN_CLOSED); + ck_assert_int_eq(wc_connect_calls, 1); + ck_assert_int_eq(wc_close_calls, 0); + ck_assert_int_lt(wc_poll_calls, 3); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +/* The slot reads as another caller's after the first poll: connect must + * neither drive it nor close it. */ +START_TEST(test_wolfcert_io_connect_reused_slot_is_left_alone) +{ + 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_peer_flip_at = 1; + wc_peer_flip_ip = 0x0A000063; + wc_peer_flip_port = 8080; + + ck_assert_int_eq(t.connect(t.ctx, "10.0.0.1", 443, 5000, &conn), + WOLFCERT_ERR_CONN_CLOSED); + ck_assert_int_eq(wc_connect_calls, 1); + ck_assert_int_eq(wc_close_calls, 0); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +/* The slot reads as a connection to the same server from another local port + * after the first poll: connect must neither drive it nor close it. */ +START_TEST(test_wolfcert_io_connect_same_server_other_port_is_left_alone) +{ + 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_peer_flip_at = 1; + wc_peer_flip_ip = 0x0A000001; + wc_peer_flip_port = 443; + wc_flip_local_port = 50000; + + ck_assert_int_eq(t.connect(t.ctx, "10.0.0.1", 443, 5000, &conn), + WOLFCERT_ERR_CONN_CLOSED); + ck_assert_int_eq(wc_connect_calls, 1); + ck_assert_int_eq(wc_close_calls, 0); + + 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, wc_conn(t.ctx, 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, wc_conn(t.ctx, 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 + +/* wolfCert's URL parser lets an empty host through. */ +START_TEST(test_wolfcert_io_connect_rejects_empty_host) +{ + 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, "", 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, wc_conn(t.ctx, 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, wc_conn(t.ctx, 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) +{ + WolfCertTransport t; + uint8_t buf[16]; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + /* 0 on a socket that still matches the handle is a close, never a 0. */ + wc_recv_steps[0] = 0; + wc_recv_steps_len = 1; + ck_assert_int_eq(t.read(t.ctx, wc_conn(t.ctx, 0x100), buf, sizeof(buf), + -1), WOLFCERT_ERR_CONN_CLOSED); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +/* A slot that was reset, or handed to another socket, no longer matches the + * handle: nothing is read, written or closed on it. */ +START_TEST(test_wolfcert_io_stale_handle_is_left_alone) +{ + WolfCertTransport t; + uint8_t buf[16]; + void *conn; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + wc_connect_last_ip = 0x0A000001; + wc_connect_last_port = 443; + conn = wc_conn(t.ctx, 0x100); + wc_recv_steps[0] = 4; + wc_recv_steps_len = 1; + wc_send_steps[0] = 4; + wc_send_steps_len = 1; + + /* Reset: the slot reports no address at all. */ + wc_local_port = 0; + wc_connect_last_ip = 0; + wc_connect_last_port = 0; + ck_assert_int_eq(t.read(t.ctx, conn, buf, sizeof(buf), -1), + WOLFCERT_ERR_IO); + + /* Reused for the same server from another local port. */ + wc_local_port = 50000; + wc_connect_last_ip = 0x0A000001; + wc_connect_last_port = 443; + ck_assert_int_eq(t.read(t.ctx, conn, buf, sizeof(buf), -1), + WOLFCERT_ERR_IO); + ck_assert_int_eq(t.write(t.ctx, conn, buf, 4, -1), WOLFCERT_ERR_IO); + + /* Reused for another server from the same local port. */ + wc_local_port = 49731; + wc_connect_last_ip = 0x0A000063; + ck_assert_int_eq(t.write(t.ctx, conn, buf, 4, -1), WOLFCERT_ERR_IO); + ck_assert_int_eq(t.disconnect(t.ctx, conn), WOLFCERT_ERR_CONN_CLOSED); + + ck_assert_int_eq(wc_recv_step, 0); + ck_assert_int_eq(wc_send_step, 0); + ck_assert_int_eq(wc_close_calls, 0); + ck_assert_int_eq(wc_abort_calls, 0); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +/* A slot that changes hands during the transport's own poll is caught before + * the next receive. */ +START_TEST(test_wolfcert_io_read_stops_when_slot_changes) +{ + WolfCertTransport t; + uint8_t buf[16]; + void *conn; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + conn = wc_conn(t.ctx, 0x100); + wc_recv_steps[0] = -WOLFIP_EAGAIN; + wc_recv_steps[1] = 4; + wc_recv_steps_len = 2; + wc_peer_flip_at = 1; + wc_flip_local_port = 50000; + + ck_assert_int_eq(t.read(t.ctx, conn, buf, sizeof(buf), -1), + WOLFCERT_ERR_IO); + ck_assert_int_eq(wc_recv_step, 1); + ck_assert_int_eq(wc_poll_calls, 1); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +/* The write counterpart of test_wolfcert_io_read_stops_when_slot_changes. */ +START_TEST(test_wolfcert_io_write_stops_when_slot_changes) +{ + WolfCertTransport t; + uint8_t buf[16]; + void *conn; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + memset(buf, 0, sizeof(buf)); + conn = wc_conn(t.ctx, 0x100); + wc_send_steps[0] = -WOLFIP_EAGAIN; + wc_send_steps[1] = 4; + wc_send_steps_len = 2; + wc_peer_flip_at = 1; + wc_flip_local_port = 50000; + + ck_assert_int_eq(t.write(t.ctx, conn, buf, 4, -1), WOLFCERT_ERR_IO); + ck_assert_int_eq(wc_send_step, 1); + ck_assert_int_eq(wc_poll_calls, 1); + + 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, wc_conn(t.ctx, 0x100), buf, sizeof(buf), + -1), WOLFCERT_ERR_IO); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +/* Data that lands in the poll which crosses the deadline is still taken. */ +START_TEST(test_wolfcert_io_rw_use_last_poll) +{ + WolfCertTransport t; + uint8_t buf[16]; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + wc_now_step_ms = WOLFCERT_WOLFIP_IO_TIMEOUT_MS; + wc_recv_steps[0] = -WOLFIP_EAGAIN; + wc_recv_steps[1] = 4; + wc_recv_steps_len = 2; + ck_assert_int_eq(t.read(t.ctx, wc_conn(t.ctx, 0x100), buf, sizeof(buf), + -1), 4); + + wc_send_steps[0] = -WOLFIP_EAGAIN; + wc_send_steps[1] = 4; + wc_send_steps_len = 2; + ck_assert_int_eq(t.write(t.ctx, wc_conn(t.ctx, 0x100), buf, 4, -1), 4); + ck_assert_int_eq(wc_poll_calls, 2); + + 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, wc_conn(t.ctx, 0x100), buf, sizeof(buf), + -1), 6); + + wc_send_step = 0; + wc_send_steps[0] = -WOLFIP_EAGAIN; + ck_assert_int_eq(t.write(t.ctx, wc_conn(t.ctx, 0x100), buf, sizeof(buf), + 0), WOLFCERT_ERR_WANT_WRITE); + 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); + + 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, wc_conn(t.ctx, 0x100)), + WOLFCERT_OK); + ck_assert_int_eq(wc_close_calls, 1); + ck_assert_int_eq(wc_poll_calls, 0); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +/* A close in progress that left the socket writable is left to the stack. */ +START_TEST(test_wolfcert_io_disconnect_leaves_fin_to_stack) +{ + WolfCertTransport t; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + wc_close_ret = -WOLFIP_EAGAIN; + + ck_assert_int_eq(t.disconnect(t.ctx, wc_conn(t.ctx, 0x100)), + WOLFCERT_OK); + ck_assert_int_eq(wc_close_calls, 1); + ck_assert_int_eq(wc_abort_calls, 0); + ck_assert_int_eq(wc_poll_calls, 0); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + +/* A close in progress that left the socket unwritable is reset instead. */ +START_TEST(test_wolfcert_io_disconnect_resets_unsent_fin) +{ + WolfCertTransport t; + void *c; + + reset_wolfcert_io_state(); + memset(&t, 0, sizeof(t)); + c = wc_test_init(&t); + + wc_close_ret = -WOLFIP_EAGAIN; + wc_can_write_ret = 0; + + ck_assert_int_eq(t.disconnect(t.ctx, wc_conn(t.ctx, 0x100)), + WOLFCERT_OK); + ck_assert_int_eq(wc_close_calls, 1); + ck_assert_int_eq(wc_abort_calls, 1); + ck_assert_int_eq(wc_poll_calls, 0); + + wolfCert_Cleanup_wolfIP(c); +} +END_TEST + diff --git a/wolfip.h b/wolfip.h index 41c9f778..2611c7a7 100644 --- a/wolfip.h +++ b/wolfip.h @@ -632,4 +632,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 */