diff --git a/src/clients/ioxide.httpclient/Tls/OpenSsl.cs b/src/clients/ioxide.httpclient/Tls/OpenSsl.cs
index 6a03a66b..653e0d0e 100644
--- a/src/clients/ioxide.httpclient/Tls/OpenSsl.cs
+++ b/src/clients/ioxide.httpclient/Tls/OpenSsl.cs
@@ -31,6 +31,7 @@ internal static unsafe partial class OpenSsl
public const int SSL_VERIFY_NONE = 0x00;
public const int SSL_VERIFY_PEER = 0x01;
public const long X509_V_OK = 0;
+ public const uint X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS = 0x4;
// --- context ---------------------------------------------------------------------------------
@@ -131,6 +132,9 @@ public static unsafe int Write(nint ssl, byte* buf, int num, out int error)
[LibraryImport(Ssl, StringMarshalling = StringMarshalling.Utf8)]
public static partial int SSL_set1_host(nint ssl, string hostname);
+ /// How 's name is matched: X509_CHECK_FLAG_* bits.
+ [LibraryImport(Ssl)] public static partial void SSL_set_hostflags(nint ssl, uint flags);
+
[LibraryImport(Ssl)] public static partial long SSL_get_verify_result(nint ssl);
[LibraryImport(Ssl)] public static partial void SSL_get0_alpn_selected(nint ssl, byte** data, uint* length);
diff --git a/src/clients/ioxide.httpclient/Tls/TlsClientStream.cs b/src/clients/ioxide.httpclient/Tls/TlsClientStream.cs
index e9278cd3..b5da6943 100644
--- a/src/clients/ioxide.httpclient/Tls/TlsClientStream.cs
+++ b/src/clients/ioxide.httpclient/Tls/TlsClientStream.cs
@@ -138,6 +138,9 @@ private static void ConfigureNames(nint ssl, TlsClientOptions options, string se
{
throw new IOException($"SSL_set1_host('{serverName}'): {OpenSsl.LastError()}");
}
+
+ // 'ww*.example.com' must not match 'www.example.com': a partial wildcard names a prefix, not a host.
+ OpenSsl.SSL_set_hostflags(ssl, OpenSsl.X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS);
}
private async ValueTask RunHandshakeAsync(TlsClientOptions options, string serverName)
diff --git a/tests/Ioxide.Tests.Http/TlsClientVerificationTests.cs b/tests/Ioxide.Tests.Http/TlsClientVerificationTests.cs
index 2a7d8646..de53b482 100644
--- a/tests/Ioxide.Tests.Http/TlsClientVerificationTests.cs
+++ b/tests/Ioxide.Tests.Http/TlsClientVerificationTests.cs
@@ -34,12 +34,12 @@ public static void Register(Runner runner)
}
///
- /// Wildcard matching, which the client never spells out: it calls SSL_set1_host and inherits
- /// whatever OpenSSL's default hostflags mean this month.
+ /// Wildcard matching, which the client spells out rather than inheriting whatever OpenSSL's
+ /// default hostflags mean this month: SSL_set1_host with X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS.
///
private static void RegisterWildcards(Runner runner)
{
- runner.Pending("tls client: a partial wildcard must not match (ww*.example.com is not www.example.com)", () =>
+ runner.Test("tls client: a partial wildcard must not match (ww*.example.com is not www.example.com)", () =>
{
// A certificate issued for 'ww*.example.com' is a certificate for a PREFIX, and the
// holder of one for 'ww*' can speak for www, wwx and every other name that starts the
@@ -55,15 +55,13 @@ private static void RegisterWildcards(Runner runner)
$"a partial wildcard should not have authenticated www.example.com, got: {body}");
Assert.True(body.Contains("certificate verify failed"),
$"should be refused as a verification failure, got: {body}");
- }, "SSL_set1_host runs with OpenSSL's default hostflags: X509_CHECK_FLAG_NO_PARTIAL_WILDCARDS "
- + "is never set, so 'ww*' matches 'www'. curl sets it, and SslStream and Python refuse "
- + "partial wildcards outright");
+ });
runner.Test("tls client: control - a full wildcard matches one label below it", () =>
{
// The control for the case above, and the reason it is a control: the ONLY difference
// is the two characters in front of the star. A '*.example.com' certificate is
- // ordinary and has to keep working, so the pending test above cannot be satisfied by
+ // ordinary and has to keep working, so the test above cannot be satisfied by
// refusing wildcards altogether.
using Authority ca = Authority.Mint();
using X509Certificate2 leaf = ca.Leaf("CN=wildcard origin", dns: ["*.example.com"]);
@@ -75,7 +73,7 @@ private static void RegisterWildcards(Runner runner)
runner.Test("tls client: a wildcard does not match across a label", () =>
{
// '*.example.com' covers one label, not a subtree - so it must not authenticate
- // a.b.example.com. Pinned because the obvious wrong fix for the pending case above is
+ // a.b.example.com. Pinned because the obvious wrong fix for the partial case above is
// to reach for the hostflags argument and pass the wrong constant:
// X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS is right next to the one that is wanted and
// widens matching instead of narrowing it.