diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/RevocationTests/CertificateAuthority.cs b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs similarity index 78% rename from src/libraries/System.Security.Cryptography.X509Certificates/tests/RevocationTests/CertificateAuthority.cs rename to src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs index 2f2666ebeeb86c..1cb4e8a290f1df 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/RevocationTests/CertificateAuthority.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/CertificateAuthority.cs @@ -7,7 +7,7 @@ using System.Linq; using Xunit; -namespace System.Security.Cryptography.X509Certificates.Tests.RevocationTests +namespace System.Security.Cryptography.X509Certificates.Tests.Common { // This class represents only a portion of what is required to be a proper Certificate Authority. // @@ -15,6 +15,29 @@ namespace System.Security.Cryptography.X509Certificates.Tests.RevocationTests // without understanding all of the portions of proper CA management that you're skipping. // // At minimum, read the current baseline requirements of the CA/Browser Forum. + + [Flags] + public enum PkiOptions + { + None = 0, + + IssuerRevocationViaCrl = 1 << 0, + IssuerRevocationViaOcsp = 1 << 1, + EndEntityRevocationViaCrl = 1 << 2, + EndEntityRevocationViaOcsp = 1 << 3, + + CrlEverywhere = IssuerRevocationViaCrl | EndEntityRevocationViaCrl, + OcspEverywhere = IssuerRevocationViaOcsp | EndEntityRevocationViaOcsp, + AllIssuerRevocation = IssuerRevocationViaCrl | IssuerRevocationViaOcsp, + AllEndEntityRevocation = EndEntityRevocationViaCrl | EndEntityRevocationViaOcsp, + AllRevocation = CrlEverywhere | OcspEverywhere, + + IssuerAuthorityHasDesignatedOcspResponder = 1 << 16, + RootAuthorityHasDesignatedOcspResponder = 1 << 17, + NoIssuerCertDistributionUri = 1 << 18, + NoRootCertDistributionUri = 1 << 18, + } + internal sealed class CertificateAuthority : IDisposable { private static readonly Asn1Tag s_context0 = new Asn1Tag(TagClass.ContextSpecific, 0); @@ -35,7 +58,7 @@ internal sealed class CertificateAuthority : IDisposable private static readonly X509KeyUsageExtension s_eeKeyUsage = new X509KeyUsageExtension( - X509KeyUsageFlags.DigitalSignature, + X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DataEncipherment, critical: false); private static readonly X509EnhancedKeyUsageExtension s_ocspResponderEku = @@ -46,6 +69,14 @@ internal sealed class CertificateAuthority : IDisposable }, critical: false); + private static readonly X509EnhancedKeyUsageExtension s_tlsServerEku = + new X509EnhancedKeyUsageExtension( + new OidCollection + { + new Oid("1.3.6.1.5.5.7.3.1", null) + }, + false); + private static readonly X509EnhancedKeyUsageExtension s_tlsClientEku = new X509EnhancedKeyUsageExtension( new OidCollection @@ -137,7 +168,7 @@ internal X509Certificate2 CreateSubordinateCA( ekuExtension: null); } - internal X509Certificate2 CreateEndEntity(string subject, RSA publicKey) + internal X509Certificate2 CreateEndEntity(string subject, RSA publicKey, X509Extension altName) { return CreateCertificate( subject, @@ -145,7 +176,8 @@ internal X509Certificate2 CreateEndEntity(string subject, RSA publicKey) TimeSpan.FromSeconds(2), s_eeConstraints, s_eeKeyUsage, - s_tlsClientEku); + s_tlsServerEku, + altName: altName); } internal X509Certificate2 CreateOcspSigner(string subject, RSA publicKey) @@ -219,7 +251,8 @@ private X509Certificate2 CreateCertificate( X509BasicConstraintsExtension basicConstraints, X509KeyUsageExtension keyUsage, X509EnhancedKeyUsageExtension ekuExtension, - bool ocspResponder = false) + bool ocspResponder = false, + X509Extension altName = null) { if (_cdpExtension == null && CdpUri != null) { @@ -262,6 +295,11 @@ private X509Certificate2 CreateCertificate( request.CertificateExtensions.Add(ekuExtension); } + if (altName != null) + { + request.CertificateExtensions.Add(altName); + } + byte[] serial = new byte[sizeof(long)]; RandomNumberGenerator.Fill(serial); @@ -793,5 +831,130 @@ private enum CertStatus OK, Revoked, } + + internal static void BuildPrivatePki( + PkiOptions pkiOptions, + out RevocationResponder responder, + out CertificateAuthority rootAuthority, + out CertificateAuthority intermediateAuthority, + out X509Certificate2 endEntityCert, + string testName = null, + bool registerAuthorities = true, + bool pkiOptionsInSubject = false, + string subjectName = null) + { + bool rootDistributionViaHttp = !pkiOptions.HasFlag(PkiOptions.NoRootCertDistributionUri); + bool issuerRevocationViaCrl = pkiOptions.HasFlag(PkiOptions.IssuerRevocationViaCrl); + bool issuerRevocationViaOcsp = pkiOptions.HasFlag(PkiOptions.IssuerRevocationViaOcsp); + bool issuerDistributionViaHttp = !pkiOptions.HasFlag(PkiOptions.NoIssuerCertDistributionUri); + bool endEntityRevocationViaCrl = pkiOptions.HasFlag(PkiOptions.EndEntityRevocationViaCrl); + bool endEntityRevocationViaOcsp = pkiOptions.HasFlag(PkiOptions.EndEntityRevocationViaOcsp); + + Assert.True( + issuerRevocationViaCrl || issuerRevocationViaOcsp || + endEntityRevocationViaCrl || endEntityRevocationViaOcsp, + "At least one revocation mode is enabled"); + + // All keys created in this method are smaller than recommended, + // but they only live for a few seconds (at most), + // and never communicate out of process. + const int KeySize = 1024; + + using (RSA rootKey = RSA.Create(KeySize)) + using (RSA intermedKey = RSA.Create(KeySize)) + using (RSA eeKey = RSA.Create(KeySize)) + { + var rootReq = new CertificateRequest( + BuildSubject("A Revocation Test Root", testName, pkiOptions, pkiOptionsInSubject), + rootKey, + HashAlgorithmName.SHA256, + RSASignaturePadding.Pkcs1); + + X509BasicConstraintsExtension caConstraints = + new X509BasicConstraintsExtension(true, false, 0, true); + + rootReq.CertificateExtensions.Add(caConstraints); + var rootSkid = new X509SubjectKeyIdentifierExtension(rootReq.PublicKey, false); + rootReq.CertificateExtensions.Add( + rootSkid); + + DateTimeOffset start = DateTimeOffset.UtcNow; + DateTimeOffset end = start.AddMonths(3); + + // Don't dispose this, it's being transferred to the CertificateAuthority + X509Certificate2 rootCert = rootReq.CreateSelfSigned(start.AddDays(-2), end.AddDays(2)); + responder = RevocationResponder.CreateAndListen(); + + string certUrl = $"{responder.UriPrefix}cert/{rootSkid.SubjectKeyIdentifier}.cer"; + string cdpUrl = $"{responder.UriPrefix}crl/{rootSkid.SubjectKeyIdentifier}.crl"; + string ocspUrl = $"{responder.UriPrefix}ocsp/{rootSkid.SubjectKeyIdentifier}"; + + rootAuthority = new CertificateAuthority( + rootCert, + rootDistributionViaHttp ? certUrl : null, + issuerRevocationViaCrl ? cdpUrl : null, + issuerRevocationViaOcsp ? ocspUrl : null); + + // Don't dispose this, it's being transferred to the CertificateAuthority + X509Certificate2 intermedCert; + + { + X509Certificate2 intermedPub = rootAuthority.CreateSubordinateCA( + BuildSubject("A Revocation Test CA", testName, pkiOptions, pkiOptionsInSubject), + intermedKey); + + intermedCert = intermedPub.CopyWithPrivateKey(intermedKey); + intermedPub.Dispose(); + } + + X509SubjectKeyIdentifierExtension intermedSkid = + intermedCert.Extensions.OfType().Single(); + + certUrl = $"{responder.UriPrefix}cert/{intermedSkid.SubjectKeyIdentifier}.cer"; + cdpUrl = $"{responder.UriPrefix}crl/{intermedSkid.SubjectKeyIdentifier}.crl"; + ocspUrl = $"{responder.UriPrefix}ocsp/{intermedSkid.SubjectKeyIdentifier}"; + + intermediateAuthority = new CertificateAuthority( + intermedCert, + issuerDistributionViaHttp ? certUrl : null, + endEntityRevocationViaCrl ? cdpUrl : null, + endEntityRevocationViaOcsp ? ocspUrl : null); + + X509Extension altName = null; + + if (!String.IsNullOrEmpty(subjectName)) + { + SubjectAlternativeNameBuilder builder = new SubjectAlternativeNameBuilder(); + builder.AddDnsName(subjectName); + altName = builder.Build(); + } + + endEntityCert = intermediateAuthority.CreateEndEntity( + BuildSubject(subjectName ?? "A Revocation Test Cert", testName, pkiOptions, pkiOptionsInSubject), + eeKey, + altName); + endEntityCert = endEntityCert.CopyWithPrivateKey(eeKey); + } + + if (registerAuthorities) + { + responder.AddCertificateAuthority(rootAuthority); + responder.AddCertificateAuthority(intermediateAuthority); + } + } + + private static string BuildSubject( + string cn, + string testName, + PkiOptions pkiOptions, + bool includePkiOptions) + { + if (includePkiOptions) + { + return $"CN=\"{cn}\", O=\"{testName}\", OU=\"{pkiOptions}\""; + } + + return $"CN=\"{cn}\", O=\"{testName}\""; + } } } diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/RevocationTests/RevocationResponder.cs b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/RevocationResponder.cs similarity index 98% rename from src/libraries/System.Security.Cryptography.X509Certificates/tests/RevocationTests/RevocationResponder.cs rename to src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/RevocationResponder.cs index e3de12320d7923..7dae6711cf995d 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/RevocationTests/RevocationResponder.cs +++ b/src/libraries/Common/tests/System/Security/Cryptography/X509Certificates/RevocationResponder.cs @@ -9,7 +9,7 @@ using System.Threading.Tasks; using System.Web; -namespace System.Security.Cryptography.X509Certificates.Tests.RevocationTests +namespace System.Security.Cryptography.X509Certificates.Tests.Common { internal sealed class RevocationResponder : IDisposable { @@ -292,7 +292,7 @@ private static void DecodeOcspRequest( if (!versionReader.TryReadInt32(out int version) || version != 0) { - throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding); + throw new CryptographicException("ASN1 corrupted data"); } versionReader.ThrowIfNotEmpty(); diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs index 70cb742ca0b173..291d5a343d5cdf 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs @@ -1195,7 +1195,6 @@ private static TlsAlertMessage GetAlertMessageFromChain(X509Chain chain) return TlsAlertMessage.CertificateUnknown; } - Debug.Fail("GetAlertMessageFromChain was called but none of the chain elements had errors."); return TlsAlertMessage.BadCertificate; } diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs index e68ed5a3d4f650..0a0481280aac47 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs @@ -17,6 +17,14 @@ namespace System.Net.Security.Tests public class SslStreamNetworkStreamTest { + private readonly X509Certificate2 _serverCert; + private readonly X509CertificateCollection _serverChain; + + public SslStreamNetworkStreamTest() + { + (_serverCert, _serverChain) = TestHelper.GenerateCertificates("localhost"); + } + [Fact] public async Task SslStream_SendReceiveOverNetworkStream_Ok() { @@ -261,6 +269,69 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout( } } + [Fact] + [PlatformSpecific(TestPlatforms.AnyUnix)] + public async Task SslStream_UntrustedCaWithCustomCallback_OK() + { + var options = new SslClientAuthenticationOptions() { TargetHost = "localhost" }; + options.RemoteCertificateValidationCallback = + (sender, certificate, chain, sslPolicyErrors) => + { + chain.ChainPolicy.ExtraStore.AddRange(_serverChain); + chain.ChainPolicy.CustomTrustStore.Add(_serverChain[_serverChain.Count -1]); + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + + bool result = chain.Build((X509Certificate2)certificate); + Assert.True(result); + + return result; + }; + + (Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams(); + using (clientStream) + using (serverStream) + using (SslStream client = new SslStream(clientStream)) + using (SslStream server = new SslStream(serverStream)) + { + Task t1 = client.AuthenticateAsClientAsync(options, default); + Task t2 = server.AuthenticateAsServerAsync(_serverCert); + + await TestConfiguration.WhenAllOrAnyFailedWithTimeout(t1, t2); + } + } + + [Fact] + [PlatformSpecific(TestPlatforms.AnyUnix)] + public async Task SslStream_UntrustedCaWithCustomCallback_Throws() + { + var options = new SslClientAuthenticationOptions() { TargetHost = "localhost" }; + options.RemoteCertificateValidationCallback = + (sender, certificate, chain, sslPolicyErrors) => + { + chain.ChainPolicy.ExtraStore.AddRange(_serverChain); + chain.ChainPolicy.CustomTrustStore.Add(_serverChain[_serverChain.Count -1]); + chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; + // This should work and we should be able to trust the chain. + Assert.True(chain.Build((X509Certificate2)certificate)); + // Reject it in custom callback to simulate for example pinning. + return false; + }; + + (Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams(); + using (clientStream) + using (serverStream) + using (SslStream client = new SslStream(clientStream)) + using (SslStream server = new SslStream(serverStream)) + { + Task t1 = client.AuthenticateAsClientAsync(options, default); + Task t2 = server.AuthenticateAsServerAsync(_serverCert); + + await Assert.ThrowsAsync(() => t1); + // Server side should finish since we run custom callback after handshake is done. + await t2; + } + } + private static bool ValidateServerCertificate( object sender, X509Certificate retrievedServerPublicCertificate, diff --git a/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj b/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj index 1e55f308659933..61887033897ed0 100644 --- a/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj +++ b/src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj @@ -61,6 +61,10 @@ Link="Common\System\Net\VirtualNetwork\VirtualNetwork.cs" /> + + ().Single(); - - certUrl = $"{responder.UriPrefix}cert/{intermedSkid.SubjectKeyIdentifier}.cer"; - cdpUrl = $"{responder.UriPrefix}crl/{intermedSkid.SubjectKeyIdentifier}.crl"; - ocspUrl = $"{responder.UriPrefix}ocsp/{intermedSkid.SubjectKeyIdentifier}"; - - intermediateAuthority = new CertificateAuthority( - intermedCert, - issuerDistributionViaHttp ? certUrl : null, - endEntityRevocationViaCrl ? cdpUrl : null, - endEntityRevocationViaOcsp ? ocspUrl : null); - - endEntityCert = intermediateAuthority.CreateEndEntity( - BuildSubject("A Revocation Test Cert", testName, pkiOptions, pkiOptionsInSubject), - eeKey); - } - - if (registerAuthorities) - { - responder.AddCertificateAuthority(rootAuthority); - responder.AddCertificateAuthority(intermediateAuthority); - } + CertificateAuthority.BuildPrivatePki(pkiOptions, out responder, out rootAuthority, out intermediateAuthority, out endEntityCert, testName, registerAuthorities, pkiOptionsInSubject); } private static string BuildSubject( diff --git a/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj b/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj index c5cd7e39fe0082..ebf9abffd3421c 100644 --- a/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj +++ b/src/libraries/System.Security.Cryptography.X509Certificates/tests/System.Security.Cryptography.X509Certificates.Tests.csproj @@ -55,9 +55,11 @@ - + - +