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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions Playground/Http3/MutualTls/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,13 @@

Env.OverrideCert(ref certOverride, ref keyOverride);

// The CA that client certificates are checked against. This is what turns mTLS ON - leave it null
// The CA that client certificates are checked against. This is what turns mTLS ON - leave both null
// and the server verifies nothing about the client, exactly as the other h3 samples do.
//
// Either the bundle's path, or the bundle itself as PEM text for a host that keeps its CA in a
// secrets store rather than on disk. Set one, not both.
string? clientCaPath = Environment.GetEnvironmentVariable("PLAYGROUND_CLIENT_CA");
string? clientCaPem = Environment.GetEnvironmentVariable("PLAYGROUND_CLIENT_CA_PEM");

// Refuse a client that offers no certificate, during the handshake. Off, so a client without one
// still connects and the handler decides what it may see - which is the more useful default when
Expand All @@ -55,14 +59,16 @@

(string certPath, string keyPath) = QuicCert.Ensure(certOverride, keyOverride);

if (clientCaPath is null)
if (clientCaPath is null && clientCaPem is null)
{
Console.Error.WriteLine("set PLAYGROUND_CLIENT_CA to a PEM bundle of the CA that signs your client certificates.");
Console.Error.WriteLine("set PLAYGROUND_CLIENT_CA to a PEM bundle of the CA that signs your client "
+ "certificates, or PLAYGROUND_CLIENT_CA_PEM to that bundle as text.");
return 1;
}

using var engine = new QuicEngine(certPath, keyPath, cidLength: 8, alpn: ["h3"],
clientCaPemPath: clientCaPath, requireClientCertificate: requireClientCertificate);
clientCaPemPath: clientCaPath, requireClientCertificate: requireClientCertificate,
clientCaPem: clientCaPem);

var config = new ServerConfig
{
Expand Down Expand Up @@ -105,7 +111,7 @@
}

Console.WriteLine($"[http3-mtls] {config.ReactorCount} reactors on :{quicPort}, "
+ $"client CA {clientCaPath}, "
+ $"client CA {clientCaPath ?? "from PEM text"}, "
+ $"client certificate {(requireClientCertificate ? "REQUIRED" : "optional")}");

foreach (Thread thread in threads)
Expand Down
2 changes: 1 addition & 1 deletion src/clients/ioxide.file/ioxide.file.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.file</RootNamespace>

<PackageId>ioxide.file</PackageId>
<Version>0.4.187</Version>
<Version>0.5.192</Version>
<Authors>MDA2AV</Authors>
<Description>File serving for the ioxide io_uring runtime: immutable asset snapshots with baked responses, pooled positional ring reads, atomic reloads.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/clients/ioxide.httpclient/ioxide.httpclient.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.httpclient</RootNamespace>

<PackageId>ioxide.httpclient</PackageId>
<Version>0.4.187</Version>
<Version>0.5.192</Version>
<Authors>MDA2AV</Authors>
<Description>The ring-native HTTP/1.1 client for the ioxide io_uring runtime - the upstream leg between a proxy and an origin. Connections are opened on the reactor thread that will use them, so a request never crosses a thread on its way out or back, and every response resumes the awaiting handler inline on its own reactor. Includes client-side TLS (SNI, ALPN, certificate verification and client certificates for mutual TLS) for https:// origins. Depends on ioxide core alone: no protocol package, no native asset.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/clients/ioxide.pg/ioxide.pg.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.pg</RootNamespace>

<PackageId>ioxide.pg</PackageId>
<Version>0.4.187</Version>
<Version>0.5.192</Version>
<Authors>MDA2AV</Authors>
<Description>Postgres driver for the ioxide io_uring runtime: pooled ring-native connections per reactor, ring-native connect and handshake, inline completion resume.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/clients/ioxide.redis/ioxide.redis.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.redis</RootNamespace>

<PackageId>ioxide.redis</PackageId>
<Version>0.4.187</Version>
<Version>0.5.192</Version>
<Authors>MDA2AV</Authors>
<Description>Redis client for the ioxide io_uring runtime: pooled ring-native connections per reactor, full RESP2 protocol, a generic command API plus typed helpers (strings, keys, hashes, lists, sets, sorted sets, pub/sub, transactions, scripting), and pipelining. Inline completion resume.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/ioxide/ioxide.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide</RootNamespace>

<PackageId>ioxide</PackageId>
<Version>0.4.187</Version>
<Version>0.5.192</Version>
<Authors>MDA2AV</Authors>
<Description>A shared-nothing io_uring runtime for .NET: one ring per reactor thread, inline completions, zero native dependencies. The engine - reactor, connection, and the IRingHost client seam. Includes TLS termination: the OpenSSL handshake driven over the ring, then kernel TLS (kTLS) transmit offload, so handlers keep writing plaintext. TLS needs OpenSSL 3 and the Linux tls module; nothing else does, and neither is loaded unless you use it.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.http2/ioxide.http2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.http2</RootNamespace>

<PackageId>ioxide.http2</PackageId>
<Version>0.4.187</Version>
<Version>0.5.192</Version>
<Authors>MDA2AV</Authors>
<Description>Pure-C# HTTP/2 for the ioxide io_uring runtime: framing, HPACK (static and dynamic tables, Huffman) and flow control, with zero native code. Serves h2c with prior knowledge and h2 over TLS by ALPN, buffered or streamed in either direction.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.http3/ioxide.http3.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.http3</RootNamespace>

<PackageId>ioxide.http3</PackageId>
<Version>0.4.187</Version>
<Version>0.5.192</Version>
<Authors>MDA2AV</Authors>
<Description>Pure C# HTTP/3 for the ioxide io_uring runtime: frame parsing, QPACK (static table + Huffman) and request dispatch with zero native dependencies. Rides any QuicConnection via its stream read surface - engine-agnostic, drop-in alternative to ioxide.nghttp3.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.nghttp2/ioxide.nghttp2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.nghttp2</RootNamespace>

<PackageId>ioxide.nghttp2</PackageId>
<Version>0.4.187</Version>
<Version>0.5.192</Version>
<Authors>MDA2AV</Authors>
<Description>HTTP/2 for the ioxide io_uring runtime: framing, HPACK and flow control from nghttp2, statically linked behind a small shim with no external dependencies beyond libc. Serves HTTP/2 over any TcpConnection - h2c with prior knowledge, or h2 over TLS via ALPN. nghttp2 is sans-I/O, so ioxide keeps the ring and the loop.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.nghttp3/ioxide.nghttp3.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.nghttp3</RootNamespace>

<PackageId>ioxide.nghttp3</PackageId>
<Version>0.4.187</Version>
<Version>0.5.192</Version>
<Authors>MDA2AV</Authors>
<Description>HTTP/3 layer for the ioxide io_uring runtime: nghttp3 (H3 + QPACK) bundled as a single self-contained native library with no external dependencies. Rides any QuicConnection via its stream read surface - engine-agnostic, no ioxide.ngtcp2 dependency.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
24 changes: 19 additions & 5 deletions src/protocols/ioxide.ngtcp2/Engine/QuicEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,35 @@ public sealed unsafe class QuicEngine : IDisposable
/// accept whichever protocol the client offers first (the pre-H3 permissive behavior).
/// </summary>
/// <param name="clientCaPemPath">
/// PEM bundle that client certificates are validated against - mutual TLS. Null (the default)
/// leaves it off and the handshake is exactly what it was.
/// PEM bundle that client certificates are validated against, as a path - mutual TLS. Null (the
/// default), with <paramref name="clientCaPem"/> also null, leaves it off and the handshake is
/// exactly what it was.
///
/// QUIC settles client authentication during the handshake and RFC 9001 section 4.4 forbids
/// doing it afterwards, so this is a property of the whole connection: there is no asking for a
/// certificate later because a request happened to reach a protected route.
/// </param>
/// <param name="clientCaPem">
/// The same trust anchors as PEM text - the in-memory alternative to
/// <paramref name="clientCaPemPath"/>, for a host that carries its CA bundle as data rather than
/// as a file. Set at most one of the two.
/// </param>
/// <param name="requireClientCertificate">
/// With a CA configured, whether a client offering no certificate is refused during the
/// handshake. False lets it connect unauthenticated and leaves the decision to the application,
/// which can read <see cref="QuicEngineConnection.PeerSubject"/>.
/// </param>
public QuicEngine(string certPemPath, string keyPemPath, uint cidLength = 8, string[]? alpn = null,
long maxSendRetentionBytes = 16L << 20,
string? clientCaPemPath = null, bool requireClientCertificate = false)
string? clientCaPemPath = null, bool requireClientCertificate = false,
string? clientCaPem = null)
{
if (clientCaPemPath is not null && clientCaPem is not null)
{
throw new ArgumentException(
"At most one client CA source: set clientCaPemPath or clientCaPem, not both.", nameof(clientCaPem));
}

CidLength = cidLength;
// Clamp to a floor: the pump overshoots the high-water by at most one egress chunk (16 KiB),
// so a cap below that would wedge a response mid-flight. 256 KiB gives comfortable headroom.
Expand All @@ -77,13 +90,14 @@ public QuicEngine(string certPemPath, string keyPemPath, uint cidLength = 8, str
{
_engine = Ngtcp2.iq_engine_new_mtls(certPemPath, keyPemPath, (nuint)cidLength,
alpnWire.Length > 0 ? pAlpn : null, (nuint)alpnWire.Length,
clientCaPemPath, requireClientCertificate ? 1 : 0, callbacks);
clientCaPemPath, clientCaPem, requireClientCertificate ? 1 : 0, callbacks);
}
if (_engine == 0)
{
throw new InvalidOperationException(
$"ioxide.ngtcp2: engine init failed (cert '{certPemPath}', key '{keyPemPath}'"
+ (clientCaPemPath is null ? ")" : $", client CA '{clientCaPemPath}')"));
+ (clientCaPemPath is not null ? $", client CA '{clientCaPemPath}')"
: clientCaPem is not null ? ", client CA from PEM text)" : ")"));
}
}

Expand Down
13 changes: 8 additions & 5 deletions src/protocols/ioxide.ngtcp2/Interop/Ngtcp2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,16 +35,19 @@ internal struct Callbacks
nuint cidLen, byte* alpn, nuint alpnLen, Callbacks cbs);

/// <summary>
/// Engine with client-certificate verification. <paramref name="clientCaPemPath"/> is the bundle
/// client certificates are validated against; null leaves mTLS off and the handshake unchanged.
/// <paramref name="requireClientCert"/> decides whether a client offering none is refused
/// outright or merely arrives unauthenticated.
/// Engine with client-certificate verification. Client certificates are validated against
/// <paramref name="clientCaPemPath"/>, a bundle on disk, or <paramref name="clientCaPem"/>, the
/// same bundle as PEM text - pass at most one. Both null leaves mTLS off and the handshake
/// unchanged. <paramref name="requireClientCert"/> decides whether a client offering none is
/// refused outright or merely arrives unauthenticated.
/// </summary>
[DllImport(Lib)] internal static extern nint iq_engine_new_mtls(
[MarshalAs(UnmanagedType.LPUTF8Str)] string certPemPath,
[MarshalAs(UnmanagedType.LPUTF8Str)] string keyPemPath,
nuint cidLen, byte* alpn, nuint alpnLen,
[MarshalAs(UnmanagedType.LPUTF8Str)] string? clientCaPemPath, int requireClientCert,
[MarshalAs(UnmanagedType.LPUTF8Str)] string? clientCaPemPath,
[MarshalAs(UnmanagedType.LPUTF8Str)] string? clientCaPem,
int requireClientCert,
Callbacks cbs);

/// <summary>The verified client identity, or 0 written when the peer offered none.</summary>
Expand Down
2 changes: 1 addition & 1 deletion src/protocols/ioxide.ngtcp2/ioxide.ngtcp2.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.ngtcp2</RootNamespace>

<PackageId>ioxide.ngtcp2</PackageId>
<Version>0.4.187</Version>
<Version>0.5.192</Version>
<Authors>MDA2AV</Authors>
<Description>QUIC engine for the ioxide io_uring runtime: ngtcp2 + picotls bundled as a single self-contained native library (only system dependency: libcrypto.so.3 / OpenSSL 3.x). Plugs into the reactor's QUIC transport via QuicConnection. Server side; engine bindings in progress.</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
63 changes: 52 additions & 11 deletions src/protocols/ioxide.ngtcp2/native/ioxide_ngtcp2_shim.c
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
#include <openssl/bio.h>
#include <openssl/pem.h>
#include <openssl/evp.h>
#include <openssl/err.h>

/* ---- callback table into C# ------------------------------------------------------------- */

Expand Down Expand Up @@ -331,24 +332,58 @@ static int iq_cb_get_new_connection_id_noreport(ngtcp2_conn *conn, ngtcp2_cid *c

iq_engine *iq_engine_new_mtls(const char *cert_pem_path, const char *key_pem_path,
size_t cidlen, const uint8_t *alpn, size_t alpn_len,
const char *client_ca_pem_path, int require_client_cert,
iq_callbacks cbs);
const char *client_ca_pem_path, const char *client_ca_pem,
int require_client_cert, iq_callbacks cbs);

/* The original five-argument form: no client certificates, exactly as before. */
iq_engine *iq_engine_new(const char *cert_pem_path, const char *key_pem_path,
size_t cidlen, const uint8_t *alpn, size_t alpn_len,
iq_callbacks cbs)
{
return iq_engine_new_mtls(cert_pem_path, key_pem_path, cidlen, alpn, alpn_len, NULL, 0, cbs);
return iq_engine_new_mtls(cert_pem_path, key_pem_path, cidlen, alpn, alpn_len, NULL, NULL, 0, cbs);
}

/* With mTLS: client_ca_pem_path is the bundle client certificates are validated against, and
* require_client_cert decides whether a client that offers none is refused outright or merely
* unauthenticated. A NULL bundle leaves both off and the handshake is byte-for-byte what it was. */
/* Trust anchors from PEM text rather than a file, for a host that carries its CA bundle as data.
* Returns how many certificates were added, 0 if the text held none usable.
*
* X509_STORE_load_locations cannot read memory, so the blocks are parsed here and added one at a
* time - the same anchors, reached the other way. A duplicate is not an error: add_cert refuses it
* and the store already holds one, so it counts. */
static int iq_store_add_pem(X509_STORE *store, const char *pem)
{
BIO *bio = BIO_new_mem_buf(pem, -1);
if (bio == NULL) {
return 0;
}

int added = 0;
X509 *cert;

while ((cert = PEM_read_bio_X509(bio, NULL, NULL, NULL)) != NULL) {
if (X509_STORE_add_cert(store, cert) == 1) {
added++;
} else if (ERR_GET_REASON(ERR_peek_last_error()) == X509_R_CERT_ALREADY_IN_HASH_TABLE) {
added++;
}
X509_free(cert);
}

/* The loop ends by design on PEM_R_NO_START_LINE once the last block is consumed, and any
* duplicate above left its own. Neither is a failure, so the queue must not outlive this. */
ERR_clear_error();

BIO_free(bio);
return added;
}

/* With mTLS: the client certificates are validated against client_ca_pem_path, a bundle on disk, or
* client_ca_pem, the same bundle as PEM text - at most one, and require_client_cert decides whether
* a client that offers none is refused outright or merely arrives unauthenticated. Both NULL leaves
* mTLS off and the handshake is byte-for-byte what it was. */
iq_engine *iq_engine_new_mtls(const char *cert_pem_path, const char *key_pem_path,
size_t cidlen, const uint8_t *alpn, size_t alpn_len,
const char *client_ca_pem_path, int require_client_cert,
iq_callbacks cbs)
const char *client_ca_pem_path, const char *client_ca_pem,
int require_client_cert, iq_callbacks cbs)
{
iq_engine *e = calloc(1, sizeof(*e));
if (e == NULL) {
Expand Down Expand Up @@ -407,14 +442,20 @@ iq_engine *iq_engine_new_mtls(const char *cert_pem_path, const char *key_pem_pat

/* AFTER configure_server_context, deliberately: it sets its own fields on the context, and
anything mTLS puts there first is not guaranteed to survive it. */
if (client_ca_pem_path != NULL) {
if (client_ca_pem_path != NULL || client_ca_pem != NULL) {
X509_STORE *store = X509_STORE_new();
if (store == NULL) {
fprintf(stderr, "[ioxide.ngtcp2] failed to allocate the client CA store\n");
goto fail;
}
if (X509_STORE_load_locations(store, client_ca_pem_path, NULL) != 1) {
fprintf(stderr, "[ioxide.ngtcp2] failed to load client CA bundle from %s\n", client_ca_pem_path);
if (client_ca_pem_path != NULL) {
if (X509_STORE_load_locations(store, client_ca_pem_path, NULL) != 1) {
fprintf(stderr, "[ioxide.ngtcp2] failed to load client CA bundle from %s\n", client_ca_pem_path);
X509_STORE_free(store);
goto fail;
}
} else if (iq_store_add_pem(store, client_ca_pem) == 0) {
fprintf(stderr, "[ioxide.ngtcp2] the client CA PEM text held no usable certificate\n");
X509_STORE_free(store);
goto fail;
}
Expand Down
Binary file not shown.
2 changes: 1 addition & 1 deletion src/serving/ioxide.Kestrel/ioxide.Kestrel.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
<RootNamespace>ioxide.Kestrel</RootNamespace>

<PackageId>ioxide.Kestrel</PackageId>
<Version>0.4.187</Version>
<Version>0.5.192</Version>
<Authors>MDA2AV</Authors>
<Description>ASP.NET Core Kestrel transport backed by the ioxide io_uring runtime: one reactor (ring) per core, SO_REUSEPORT load-balanced, with Kestrel's HTTP request loop pinned to the reactor thread. Drop-in via UseIoxide().</Description>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
Expand Down
Loading
Loading