Skip to content

Commit fad3fc9

Browse files
authored
add two SslStream tests for custom validation (#38182)
* add two SslStream tests for custom validation * use BuildPrivatePki * feedback from review * disable on windows
1 parent 9f3e08e commit fad3fc9

File tree

9 files changed

+281
-112
lines changed

9 files changed

+281
-112
lines changed
Lines changed: 168 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,37 @@
77
using System.Linq;
88
using Xunit;
99

10-
namespace System.Security.Cryptography.X509Certificates.Tests.RevocationTests
10+
namespace System.Security.Cryptography.X509Certificates.Tests.Common
1111
{
1212
// This class represents only a portion of what is required to be a proper Certificate Authority.
1313
//
1414
// Please do not use it as the basis for any real Public/Private Key Infrastructure (PKI) system
1515
// without understanding all of the portions of proper CA management that you're skipping.
1616
//
1717
// At minimum, read the current baseline requirements of the CA/Browser Forum.
18+
19+
[Flags]
20+
public enum PkiOptions
21+
{
22+
None = 0,
23+
24+
IssuerRevocationViaCrl = 1 << 0,
25+
IssuerRevocationViaOcsp = 1 << 1,
26+
EndEntityRevocationViaCrl = 1 << 2,
27+
EndEntityRevocationViaOcsp = 1 << 3,
28+
29+
CrlEverywhere = IssuerRevocationViaCrl | EndEntityRevocationViaCrl,
30+
OcspEverywhere = IssuerRevocationViaOcsp | EndEntityRevocationViaOcsp,
31+
AllIssuerRevocation = IssuerRevocationViaCrl | IssuerRevocationViaOcsp,
32+
AllEndEntityRevocation = EndEntityRevocationViaCrl | EndEntityRevocationViaOcsp,
33+
AllRevocation = CrlEverywhere | OcspEverywhere,
34+
35+
IssuerAuthorityHasDesignatedOcspResponder = 1 << 16,
36+
RootAuthorityHasDesignatedOcspResponder = 1 << 17,
37+
NoIssuerCertDistributionUri = 1 << 18,
38+
NoRootCertDistributionUri = 1 << 18,
39+
}
40+
1841
internal sealed class CertificateAuthority : IDisposable
1942
{
2043
private static readonly Asn1Tag s_context0 = new Asn1Tag(TagClass.ContextSpecific, 0);
@@ -35,7 +58,7 @@ internal sealed class CertificateAuthority : IDisposable
3558

3659
private static readonly X509KeyUsageExtension s_eeKeyUsage =
3760
new X509KeyUsageExtension(
38-
X509KeyUsageFlags.DigitalSignature,
61+
X509KeyUsageFlags.DigitalSignature | X509KeyUsageFlags.KeyEncipherment | X509KeyUsageFlags.DataEncipherment,
3962
critical: false);
4063

4164
private static readonly X509EnhancedKeyUsageExtension s_ocspResponderEku =
@@ -46,6 +69,14 @@ internal sealed class CertificateAuthority : IDisposable
4669
},
4770
critical: false);
4871

72+
private static readonly X509EnhancedKeyUsageExtension s_tlsServerEku =
73+
new X509EnhancedKeyUsageExtension(
74+
new OidCollection
75+
{
76+
new Oid("1.3.6.1.5.5.7.3.1", null)
77+
},
78+
false);
79+
4980
private static readonly X509EnhancedKeyUsageExtension s_tlsClientEku =
5081
new X509EnhancedKeyUsageExtension(
5182
new OidCollection
@@ -137,15 +168,16 @@ internal X509Certificate2 CreateSubordinateCA(
137168
ekuExtension: null);
138169
}
139170

140-
internal X509Certificate2 CreateEndEntity(string subject, RSA publicKey)
171+
internal X509Certificate2 CreateEndEntity(string subject, RSA publicKey, X509Extension altName)
141172
{
142173
return CreateCertificate(
143174
subject,
144175
publicKey,
145176
TimeSpan.FromSeconds(2),
146177
s_eeConstraints,
147178
s_eeKeyUsage,
148-
s_tlsClientEku);
179+
s_tlsServerEku,
180+
altName: altName);
149181
}
150182

151183
internal X509Certificate2 CreateOcspSigner(string subject, RSA publicKey)
@@ -219,7 +251,8 @@ private X509Certificate2 CreateCertificate(
219251
X509BasicConstraintsExtension basicConstraints,
220252
X509KeyUsageExtension keyUsage,
221253
X509EnhancedKeyUsageExtension ekuExtension,
222-
bool ocspResponder = false)
254+
bool ocspResponder = false,
255+
X509Extension altName = null)
223256
{
224257
if (_cdpExtension == null && CdpUri != null)
225258
{
@@ -262,6 +295,11 @@ private X509Certificate2 CreateCertificate(
262295
request.CertificateExtensions.Add(ekuExtension);
263296
}
264297

298+
if (altName != null)
299+
{
300+
request.CertificateExtensions.Add(altName);
301+
}
302+
265303
byte[] serial = new byte[sizeof(long)];
266304
RandomNumberGenerator.Fill(serial);
267305

@@ -793,5 +831,130 @@ private enum CertStatus
793831
OK,
794832
Revoked,
795833
}
834+
835+
internal static void BuildPrivatePki(
836+
PkiOptions pkiOptions,
837+
out RevocationResponder responder,
838+
out CertificateAuthority rootAuthority,
839+
out CertificateAuthority intermediateAuthority,
840+
out X509Certificate2 endEntityCert,
841+
string testName = null,
842+
bool registerAuthorities = true,
843+
bool pkiOptionsInSubject = false,
844+
string subjectName = null)
845+
{
846+
bool rootDistributionViaHttp = !pkiOptions.HasFlag(PkiOptions.NoRootCertDistributionUri);
847+
bool issuerRevocationViaCrl = pkiOptions.HasFlag(PkiOptions.IssuerRevocationViaCrl);
848+
bool issuerRevocationViaOcsp = pkiOptions.HasFlag(PkiOptions.IssuerRevocationViaOcsp);
849+
bool issuerDistributionViaHttp = !pkiOptions.HasFlag(PkiOptions.NoIssuerCertDistributionUri);
850+
bool endEntityRevocationViaCrl = pkiOptions.HasFlag(PkiOptions.EndEntityRevocationViaCrl);
851+
bool endEntityRevocationViaOcsp = pkiOptions.HasFlag(PkiOptions.EndEntityRevocationViaOcsp);
852+
853+
Assert.True(
854+
issuerRevocationViaCrl || issuerRevocationViaOcsp ||
855+
endEntityRevocationViaCrl || endEntityRevocationViaOcsp,
856+
"At least one revocation mode is enabled");
857+
858+
// All keys created in this method are smaller than recommended,
859+
// but they only live for a few seconds (at most),
860+
// and never communicate out of process.
861+
const int KeySize = 1024;
862+
863+
using (RSA rootKey = RSA.Create(KeySize))
864+
using (RSA intermedKey = RSA.Create(KeySize))
865+
using (RSA eeKey = RSA.Create(KeySize))
866+
{
867+
var rootReq = new CertificateRequest(
868+
BuildSubject("A Revocation Test Root", testName, pkiOptions, pkiOptionsInSubject),
869+
rootKey,
870+
HashAlgorithmName.SHA256,
871+
RSASignaturePadding.Pkcs1);
872+
873+
X509BasicConstraintsExtension caConstraints =
874+
new X509BasicConstraintsExtension(true, false, 0, true);
875+
876+
rootReq.CertificateExtensions.Add(caConstraints);
877+
var rootSkid = new X509SubjectKeyIdentifierExtension(rootReq.PublicKey, false);
878+
rootReq.CertificateExtensions.Add(
879+
rootSkid);
880+
881+
DateTimeOffset start = DateTimeOffset.UtcNow;
882+
DateTimeOffset end = start.AddMonths(3);
883+
884+
// Don't dispose this, it's being transferred to the CertificateAuthority
885+
X509Certificate2 rootCert = rootReq.CreateSelfSigned(start.AddDays(-2), end.AddDays(2));
886+
responder = RevocationResponder.CreateAndListen();
887+
888+
string certUrl = $"{responder.UriPrefix}cert/{rootSkid.SubjectKeyIdentifier}.cer";
889+
string cdpUrl = $"{responder.UriPrefix}crl/{rootSkid.SubjectKeyIdentifier}.crl";
890+
string ocspUrl = $"{responder.UriPrefix}ocsp/{rootSkid.SubjectKeyIdentifier}";
891+
892+
rootAuthority = new CertificateAuthority(
893+
rootCert,
894+
rootDistributionViaHttp ? certUrl : null,
895+
issuerRevocationViaCrl ? cdpUrl : null,
896+
issuerRevocationViaOcsp ? ocspUrl : null);
897+
898+
// Don't dispose this, it's being transferred to the CertificateAuthority
899+
X509Certificate2 intermedCert;
900+
901+
{
902+
X509Certificate2 intermedPub = rootAuthority.CreateSubordinateCA(
903+
BuildSubject("A Revocation Test CA", testName, pkiOptions, pkiOptionsInSubject),
904+
intermedKey);
905+
906+
intermedCert = intermedPub.CopyWithPrivateKey(intermedKey);
907+
intermedPub.Dispose();
908+
}
909+
910+
X509SubjectKeyIdentifierExtension intermedSkid =
911+
intermedCert.Extensions.OfType<X509SubjectKeyIdentifierExtension>().Single();
912+
913+
certUrl = $"{responder.UriPrefix}cert/{intermedSkid.SubjectKeyIdentifier}.cer";
914+
cdpUrl = $"{responder.UriPrefix}crl/{intermedSkid.SubjectKeyIdentifier}.crl";
915+
ocspUrl = $"{responder.UriPrefix}ocsp/{intermedSkid.SubjectKeyIdentifier}";
916+
917+
intermediateAuthority = new CertificateAuthority(
918+
intermedCert,
919+
issuerDistributionViaHttp ? certUrl : null,
920+
endEntityRevocationViaCrl ? cdpUrl : null,
921+
endEntityRevocationViaOcsp ? ocspUrl : null);
922+
923+
X509Extension altName = null;
924+
925+
if (!String.IsNullOrEmpty(subjectName))
926+
{
927+
SubjectAlternativeNameBuilder builder = new SubjectAlternativeNameBuilder();
928+
builder.AddDnsName(subjectName);
929+
altName = builder.Build();
930+
}
931+
932+
endEntityCert = intermediateAuthority.CreateEndEntity(
933+
BuildSubject(subjectName ?? "A Revocation Test Cert", testName, pkiOptions, pkiOptionsInSubject),
934+
eeKey,
935+
altName);
936+
endEntityCert = endEntityCert.CopyWithPrivateKey(eeKey);
937+
}
938+
939+
if (registerAuthorities)
940+
{
941+
responder.AddCertificateAuthority(rootAuthority);
942+
responder.AddCertificateAuthority(intermediateAuthority);
943+
}
944+
}
945+
946+
private static string BuildSubject(
947+
string cn,
948+
string testName,
949+
PkiOptions pkiOptions,
950+
bool includePkiOptions)
951+
{
952+
if (includePkiOptions)
953+
{
954+
return $"CN=\"{cn}\", O=\"{testName}\", OU=\"{pkiOptions}\"";
955+
}
956+
957+
return $"CN=\"{cn}\", O=\"{testName}\"";
958+
}
796959
}
797960
}
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
using System.Threading.Tasks;
1010
using System.Web;
1111

12-
namespace System.Security.Cryptography.X509Certificates.Tests.RevocationTests
12+
namespace System.Security.Cryptography.X509Certificates.Tests.Common
1313
{
1414
internal sealed class RevocationResponder : IDisposable
1515
{
@@ -292,7 +292,7 @@ private static void DecodeOcspRequest(
292292

293293
if (!versionReader.TryReadInt32(out int version) || version != 0)
294294
{
295-
throw new CryptographicException(SR.Cryptography_Der_Invalid_Encoding);
295+
throw new CryptographicException("ASN1 corrupted data");
296296
}
297297

298298
versionReader.ThrowIfNotEmpty();

src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1195,7 +1195,6 @@ private static TlsAlertMessage GetAlertMessageFromChain(X509Chain chain)
11951195
return TlsAlertMessage.CertificateUnknown;
11961196
}
11971197

1198-
Debug.Fail("GetAlertMessageFromChain was called but none of the chain elements had errors.");
11991198
return TlsAlertMessage.BadCertificate;
12001199
}
12011200

src/libraries/System.Net.Security/tests/FunctionalTests/SslStreamNetworkStreamTest.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ namespace System.Net.Security.Tests
1717

1818
public class SslStreamNetworkStreamTest
1919
{
20+
private readonly X509Certificate2 _serverCert;
21+
private readonly X509CertificateCollection _serverChain;
22+
23+
public SslStreamNetworkStreamTest()
24+
{
25+
(_serverCert, _serverChain) = TestHelper.GenerateCertificates("localhost");
26+
}
27+
2028
[Fact]
2129
public async Task SslStream_SendReceiveOverNetworkStream_Ok()
2230
{
@@ -261,6 +269,69 @@ await TestConfiguration.WhenAllOrAnyFailedWithTimeout(
261269
}
262270
}
263271

272+
[Fact]
273+
[PlatformSpecific(TestPlatforms.AnyUnix)]
274+
public async Task SslStream_UntrustedCaWithCustomCallback_OK()
275+
{
276+
var options = new SslClientAuthenticationOptions() { TargetHost = "localhost" };
277+
options.RemoteCertificateValidationCallback =
278+
(sender, certificate, chain, sslPolicyErrors) =>
279+
{
280+
chain.ChainPolicy.ExtraStore.AddRange(_serverChain);
281+
chain.ChainPolicy.CustomTrustStore.Add(_serverChain[_serverChain.Count -1]);
282+
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
283+
284+
bool result = chain.Build((X509Certificate2)certificate);
285+
Assert.True(result);
286+
287+
return result;
288+
};
289+
290+
(Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams();
291+
using (clientStream)
292+
using (serverStream)
293+
using (SslStream client = new SslStream(clientStream))
294+
using (SslStream server = new SslStream(serverStream))
295+
{
296+
Task t1 = client.AuthenticateAsClientAsync(options, default);
297+
Task t2 = server.AuthenticateAsServerAsync(_serverCert);
298+
299+
await TestConfiguration.WhenAllOrAnyFailedWithTimeout(t1, t2);
300+
}
301+
}
302+
303+
[Fact]
304+
[PlatformSpecific(TestPlatforms.AnyUnix)]
305+
public async Task SslStream_UntrustedCaWithCustomCallback_Throws()
306+
{
307+
var options = new SslClientAuthenticationOptions() { TargetHost = "localhost" };
308+
options.RemoteCertificateValidationCallback =
309+
(sender, certificate, chain, sslPolicyErrors) =>
310+
{
311+
chain.ChainPolicy.ExtraStore.AddRange(_serverChain);
312+
chain.ChainPolicy.CustomTrustStore.Add(_serverChain[_serverChain.Count -1]);
313+
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
314+
// This should work and we should be able to trust the chain.
315+
Assert.True(chain.Build((X509Certificate2)certificate));
316+
// Reject it in custom callback to simulate for example pinning.
317+
return false;
318+
};
319+
320+
(Stream clientStream, Stream serverStream) = TestHelper.GetConnectedStreams();
321+
using (clientStream)
322+
using (serverStream)
323+
using (SslStream client = new SslStream(clientStream))
324+
using (SslStream server = new SslStream(serverStream))
325+
{
326+
Task t1 = client.AuthenticateAsClientAsync(options, default);
327+
Task t2 = server.AuthenticateAsServerAsync(_serverCert);
328+
329+
await Assert.ThrowsAsync<AuthenticationException>(() => t1);
330+
// Server side should finish since we run custom callback after handshake is done.
331+
await t2;
332+
}
333+
}
334+
264335
private static bool ValidateServerCertificate(
265336
object sender,
266337
X509Certificate retrievedServerPublicCertificate,

src/libraries/System.Net.Security/tests/FunctionalTests/System.Net.Security.Tests.csproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@
6161
Link="Common\System\Net\VirtualNetwork\VirtualNetwork.cs" />
6262
<Compile Include="$(CommonTestPath)System\Net\VirtualNetwork\VirtualNetworkStream.cs"
6363
Link="Common\System\Net\VirtualNetwork\VirtualNetworkStream.cs" />
64+
<Compile Include="$(CommonTestPath)System\Security\Cryptography\X509Certificates\CertificateAuthority.cs"
65+
Link="CommonTest\System\Security\Cryptography\509Certificates\CertificateAuthority.cs" />
66+
<Compile Include="$(CommonTestPath)System\Security\Cryptography\X509Certificates\RevocationResponder.cs"
67+
Link="CommonTest\System\Security\Cryptography\509Certificates\RevocationResponder.cs" />
6468
<Compile Include="$(CommonTestPath)System\Threading\Tasks\TaskTimeoutExtensions.cs"
6569
Link="Common\System\Threading\Tasks\TaskTimeoutExtensions.cs" />
6670
<Compile Include="$(CommonPath)System\Threading\Tasks\TaskToApm.cs"

0 commit comments

Comments
 (0)