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
4 changes: 4 additions & 0 deletions src/clients/ioxide.httpclient/Tls/OpenSsl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 ---------------------------------------------------------------------------------

Expand Down Expand Up @@ -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);

/// <summary>How <see cref="SSL_set1_host"/>'s name is matched: X509_CHECK_FLAG_* bits.</summary>
[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);

Expand Down
3 changes: 3 additions & 0 deletions src/clients/ioxide.httpclient/Tls/TlsClientStream.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
14 changes: 6 additions & 8 deletions tests/Ioxide.Tests.Http/TlsClientVerificationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,12 @@ public static void Register(Runner runner)
}

/// <summary>
/// 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.
/// </summary>
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
Expand All @@ -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"]);
Expand All @@ -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.
Expand Down
Loading