Skip to content

Commit

Permalink
Add X509 SAN extension and RFC6125 MatchesHostname (#72304)
Browse files Browse the repository at this point in the history
* Add X509 SAN extension and RFC6125 MatchesHostname

* Address feedback

* Make macOS happy

* Make macOS happier

* Small test enhancements

* Add new X.509 extensions into CryptoConfig tests

* Apply feedback

* Clarify the position on SRV-ID and URI-ID matching in API docs and tests
* Add some more IPv6 tests
* Delete a now-redundant test
* Change a dead if to an assert.
  • Loading branch information
bartonjs authored Jul 20, 2022
1 parent dcffb2e commit 0f8841d
Show file tree
Hide file tree
Showing 13 changed files with 1,592 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -148,5 +148,22 @@ internal static Oid GetSharedOrNewOid(ref AsnValueReader asnValueReader)
return null;
#endif
}

internal static bool ValueEquals(this Oid oid, Oid? other)
{
Debug.Assert(oid is not null);

if (ReferenceEquals(oid, other))
{
return true;
}

if (other is null)
{
return false;
}

return oid.Value is not null && oid.Value.Equals(other.Value);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Linq;
using System.Net;
using Test.Cryptography;
using Xunit;

Expand Down Expand Up @@ -75,7 +76,11 @@ public static void ReadExtensions()

Assert.Equal(expected, sans.RawData);

Assert.IsType<X509Extension>(sans);
// This SAN only contains an alternate DirectoryName entry, so both the DNSNames and
// IPAddresses enumerations being empty is correct.
X509SubjectAlternativeNameExtension rich = Assert.IsType<X509SubjectAlternativeNameExtension>(sans);
Assert.Equal(Enumerable.Empty<string>(), rich.EnumerateDnsNames());
Assert.Equal(Enumerable.Empty<IPAddress>(), rich.EnumerateIPAddresses());
}

{
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,254 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Net;
using Xunit;

namespace System.Security.Cryptography.X509Certificates.Tests.ExtensionsTests
{
public static class SubjectAlternativeNameTests
{
[Fact]
public static void DefaultConstructor()
{
X509SubjectAlternativeNameExtension ext = new X509SubjectAlternativeNameExtension();
Assert.Empty(ext.RawData);
Assert.Equal("2.5.29.17", ext.Oid.Value);
Assert.Empty(ext.EnumerateDnsNames());
Assert.Empty(ext.EnumerateIPAddresses());
Assert.False(ext.Critical, "ext.Critical");
}

[Fact]
public static void ArrayCtorRejectsNull()
{
Assert.Throws<ArgumentNullException>(
"rawData",
() => new X509SubjectAlternativeNameExtension((byte[])null));
}

[Theory]
[InlineData(LoadMode.CopyFrom)]
[InlineData(LoadMode.Array)]
[InlineData(LoadMode.Span)]
public static void EnumerateDnsNames(LoadMode loadMode)
{
SubjectAlternativeNameBuilder builder = new SubjectAlternativeNameBuilder();
builder.AddDnsName("foo");
builder.AddIpAddress(IPAddress.Loopback);
builder.AddUserPrincipalName("[email protected]");
builder.AddIpAddress(IPAddress.IPv6Loopback);
builder.AddDnsName("*.foo");
X509Extension built = builder.Build(true);

X509SubjectAlternativeNameExtension ext;

switch (loadMode)
{
case LoadMode.CopyFrom:
ext = new X509SubjectAlternativeNameExtension();
ext.CopyFrom(built);
break;
case LoadMode.Array:
ext = new X509SubjectAlternativeNameExtension(built.RawData);
break;
case LoadMode.Span:
byte[] tmp = new byte[built.RawData.Length + 2];
built.RawData.AsSpan().CopyTo(tmp.AsSpan(1));
ext = new X509SubjectAlternativeNameExtension(tmp.AsSpan()[1..^1]);
tmp.AsSpan().Clear();
break;
default:
throw new ArgumentOutOfRangeException(nameof(loadMode), loadMode, "Unexpected mode");
}

Assert.Equal(new[] { "foo", "*.foo" }, ext.EnumerateDnsNames());
}

[Theory]
[InlineData(LoadMode.CopyFrom)]
[InlineData(LoadMode.Array)]
[InlineData(LoadMode.Span)]
public static void EnumerateIPAddresses(LoadMode loadMode)
{
SubjectAlternativeNameBuilder builder = new SubjectAlternativeNameBuilder();
builder.AddDnsName("foo");
builder.AddIpAddress(IPAddress.Loopback);
builder.AddUserPrincipalName("[email protected]");
builder.AddIpAddress(IPAddress.IPv6Loopback);
builder.AddDnsName("*.foo");
X509Extension built = builder.Build(true);

X509SubjectAlternativeNameExtension ext;

switch (loadMode)
{
case LoadMode.CopyFrom:
ext = new X509SubjectAlternativeNameExtension();
ext.CopyFrom(built);
break;
case LoadMode.Array:
ext = new X509SubjectAlternativeNameExtension(built.RawData);
break;
case LoadMode.Span:
byte[] tmp = new byte[built.RawData.Length + 2];
built.RawData.AsSpan().CopyTo(tmp.AsSpan(1));
ext = new X509SubjectAlternativeNameExtension(tmp.AsSpan()[1..^1]);
tmp.AsSpan().Clear();
break;
default:
throw new ArgumentOutOfRangeException(nameof(loadMode), loadMode, "Unexpected mode");
}

Assert.Equal(new[] { IPAddress.Loopback, IPAddress.IPv6Loopback }, ext.EnumerateIPAddresses());
}

[Theory]
[InlineData(LoadMode.CopyFrom)]
[InlineData(LoadMode.Array)]
[InlineData(LoadMode.Span)]
public static void CopyFromAfterLoaded(LoadMode originalLoadMode)
{
SubjectAlternativeNameBuilder builder = new SubjectAlternativeNameBuilder();
builder.AddDnsName("foo");
builder.AddIpAddress(IPAddress.Loopback);
builder.AddUserPrincipalName("[email protected]");
builder.AddIpAddress(IPAddress.IPv6Loopback);
builder.AddDnsName("*.foo");
X509Extension built = builder.Build(true);

X509SubjectAlternativeNameExtension ext;

switch (originalLoadMode)
{
case LoadMode.CopyFrom:
ext = new X509SubjectAlternativeNameExtension();
ext.CopyFrom(built);
break;
case LoadMode.Array:
ext = new X509SubjectAlternativeNameExtension(built.RawData, critical: true);
break;
case LoadMode.Span:
byte[] tmp = new byte[built.RawData.Length + 2];
built.RawData.AsSpan().CopyTo(tmp.AsSpan(1));
ext = new X509SubjectAlternativeNameExtension(tmp.AsSpan()[1..^1], critical: true);
tmp.AsSpan().Clear();
break;
default:
throw new ArgumentOutOfRangeException(nameof(originalLoadMode), originalLoadMode, "Unexpected mode");
}

Assert.True(ext.Critical, "ext.Critical");
Assert.Equal(new[] { "foo", "*.foo" }, ext.EnumerateDnsNames());
Assert.Equal(new[] { IPAddress.Loopback, IPAddress.IPv6Loopback }, ext.EnumerateIPAddresses());

builder = new SubjectAlternativeNameBuilder();
builder.AddDnsName("a");
builder.AddDnsName("b");
builder.AddDnsName("c");
builder.AddIpAddress(IPAddress.IPv6Loopback);
ext.CopyFrom(builder.Build());

Assert.False(ext.Critical, "ext.Critical");
Assert.Equal(new[] { "a", "b", "c" }, ext.EnumerateDnsNames());
Assert.Equal(new[] { IPAddress.IPv6Loopback }, ext.EnumerateIPAddresses());
}

[Theory]
[InlineData(LoadMode.CopyFrom)]
[InlineData(LoadMode.Array)]
[InlineData(LoadMode.Span)]
public static void VerifyDecodeFailureBehavior(LoadMode loadMode)
{
byte[] invalidEncoding = { 0x05, 0x00 };

VerifyDecodeFailure(invalidEncoding, loadMode);
}

[Theory]
[InlineData(LoadMode.CopyFrom)]
[InlineData(LoadMode.Array)]
[InlineData(LoadMode.Span)]
public static void VerifyInvalidIPAddressBehavior(LoadMode loadMode)
{
// dNSName: foo
// iPAddress: 127.0.0.1
// iPAddress: 7F 00 00 01 00 (127.0.0.1 with a trailing 0)
// UPN: [email protected]
// iPAddress: ::1
// dNSName: *.foo
byte[] invalidEncoding =
{
0x30, 0x4D, 0x82, 0x03, 0x66, 0x6F, 0x6F, 0x87, 0x04, 0x7F, 0x00, 0x00, 0x01, 0x87, 0x05, 0x7F,
0x00, 0x00, 0x01, 0x00, 0xA0, 0x20, 0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x14,
0x02, 0x03, 0xA0, 0x12, 0x0C, 0x10, 0x75, 0x73, 0x65, 0x72, 0x40, 0x73, 0x6F, 0x6D, 0x65, 0x2E,
0x64, 0x6F, 0x6D, 0x61, 0x69, 0x6E, 0x87, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x82, 0x05, 0x2A, 0x2E, 0x66, 0x6F, 0x6F,
};

VerifyDecodeFailure(invalidEncoding, loadMode);
}

[Theory]
[InlineData(LoadMode.CopyFrom)]
[InlineData(LoadMode.Array)]
[InlineData(LoadMode.Span)]
public static void VerifyInvalidDnsNameBehavior(LoadMode loadMode)
{
// dNSName: foo
// iPAddress: 127.0.0.1
// UPN: [email protected]
// dNSName: 86 6F 6F ("foo" with the f changed from 66 to 86)
// iPAddress: ::1
// dNSName: *.foo
byte[] invalidEncoding =
{
0x30, 0x4B, 0x82, 0x03, 0x66, 0x6F, 0x6F, 0x87, 0x04, 0x7F, 0x00, 0x00, 0x01, 0xA0, 0x20, 0x06,
0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x82, 0x37, 0x14, 0x02, 0x03, 0xA0, 0x12, 0x0C, 0x10, 0x75,
0x73, 0x65, 0x72, 0x40, 0x73, 0x6F, 0x6D, 0x65, 0x2E, 0x64, 0x6F, 0x6D, 0x61, 0x69, 0x6E, 0x82,
0x03, 0x86, 0x6F, 0x6F, 0x87, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x82, 0x05, 0x2A, 0x2E, 0x66, 0x6F, 0x6F,
};

VerifyDecodeFailure(invalidEncoding, loadMode);
}

private static void VerifyDecodeFailure(byte[] invalidEncoding, LoadMode loadMode)
{
switch (loadMode)
{
case LoadMode.CopyFrom:
X509Extension untyped = new X509Extension("0.0", invalidEncoding, true);
X509SubjectAlternativeNameExtension ext = new X509SubjectAlternativeNameExtension();

// The pattern for X509Extension is that CopyFrom doesn't validate data,
// and it blindly accepts the incoming OID. The semantic properties then throw late.
ext.CopyFrom(untyped);
Assert.True(ext.Critical);
Assert.Equal("0.0", ext.Oid.Value);
AssertExtensions.SequenceEqual(invalidEncoding, ext.RawData);
Assert.Throws<CryptographicException>(ext.EnumerateDnsNames);
Assert.Throws<CryptographicException>(ext.EnumerateIPAddresses);
break;
case LoadMode.Array:
// The ctors don't need to be so forgiving, through.
Assert.Throws<CryptographicException>(
() => new X509SubjectAlternativeNameExtension(invalidEncoding));
break;
case LoadMode.Span:
Assert.Throws<CryptographicException>(
() => new X509SubjectAlternativeNameExtension(new ReadOnlySpan<byte>(invalidEncoding)));
break;
default:
throw new ArgumentOutOfRangeException(nameof(loadMode), loadMode, "Unexpected mode");
}
}

public enum LoadMode
{
CopyFrom,
Array,
Span,
}
}
}
Loading

0 comments on commit 0f8841d

Please sign in to comment.