diff options
author | Hao Kung <HaoK@users.noreply.github.com> | 2019-06-01 08:49:40 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2019-06-01 08:49:40 +0300 |
commit | b75b892eac51be8b2f0eb9dc9b47537fc02001c9 (patch) | |
tree | fa47b9a608102bd82e71e97fe0b0e039f32a26b2 | |
parent | 4dde8b9461ed9b79030e644363dab628d681e05f (diff) |
Add CertificateAuthentication (#9756)
44 files changed, 2178 insertions, 1 deletions
diff --git a/eng/ProjectReferences.props b/eng/ProjectReferences.props index fa3ee5688b..9df79e4053 100644 --- a/eng/ProjectReferences.props +++ b/eng/ProjectReferences.props @@ -57,6 +57,7 @@ <ProjectReferenceProvider Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions" ProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Abstractions\src\Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.csproj" RefProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Abstractions\ref\Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.csproj" /> <ProjectReferenceProvider Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv" ProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Libuv\src\Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.csproj" RefProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Libuv\ref\Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.csproj" /> <ProjectReferenceProvider Include="Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets" ProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Sockets\src\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj" RefProjectPath="$(RepoRoot)src\Servers\Kestrel\Transport.Sockets\ref\Microsoft.AspNetCore.Server.Kestrel.Transport.Sockets.csproj" /> + <ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication.Certificate" ProjectPath="$(RepoRoot)src\Security\Authentication\Certificate\src\Microsoft.AspNetCore.Authentication.Certificate.csproj" RefProjectPath="$(RepoRoot)src\Security\Authentication\Certificate\ref\Microsoft.AspNetCore.Authentication.Certificate.csproj" /> <ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication.Cookies" ProjectPath="$(RepoRoot)src\Security\Authentication\Cookies\src\Microsoft.AspNetCore.Authentication.Cookies.csproj" RefProjectPath="$(RepoRoot)src\Security\Authentication\Cookies\ref\Microsoft.AspNetCore.Authentication.Cookies.csproj" /> <ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication" ProjectPath="$(RepoRoot)src\Security\Authentication\Core\src\Microsoft.AspNetCore.Authentication.csproj" RefProjectPath="$(RepoRoot)src\Security\Authentication\Core\ref\Microsoft.AspNetCore.Authentication.csproj" /> <ProjectReferenceProvider Include="Microsoft.AspNetCore.Authentication.Facebook" ProjectPath="$(RepoRoot)src\Security\Authentication\Facebook\src\Microsoft.AspNetCore.Authentication.Facebook.csproj" RefProjectPath="$(RepoRoot)src\Security\Authentication\Facebook\ref\Microsoft.AspNetCore.Authentication.Facebook.csproj" /> diff --git a/src/Middleware/HttpOverrides/ref/Microsoft.AspNetCore.HttpOverrides.netcoreapp3.0.cs b/src/Middleware/HttpOverrides/ref/Microsoft.AspNetCore.HttpOverrides.netcoreapp3.0.cs index cb7ee21e14..f21794c54b 100644 --- a/src/Middleware/HttpOverrides/ref/Microsoft.AspNetCore.HttpOverrides.netcoreapp3.0.cs +++ b/src/Middleware/HttpOverrides/ref/Microsoft.AspNetCore.HttpOverrides.netcoreapp3.0.cs @@ -3,6 +3,10 @@ namespace Microsoft.AspNetCore.Builder { + public static partial class CertificateForwardingBuilderExtensions + { + public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseCertificateForwarding(this Microsoft.AspNetCore.Builder.IApplicationBuilder app) { throw null; } + } public static partial class ForwardedHeadersExtensions { public static Microsoft.AspNetCore.Builder.IApplicationBuilder UseForwardedHeaders(this Microsoft.AspNetCore.Builder.IApplicationBuilder builder) { throw null; } @@ -37,6 +41,17 @@ namespace Microsoft.AspNetCore.Builder } namespace Microsoft.AspNetCore.HttpOverrides { + public partial class CertificateForwardingMiddleware + { + public CertificateForwardingMiddleware(Microsoft.AspNetCore.Http.RequestDelegate next, Microsoft.Extensions.Logging.ILoggerFactory loggerFactory, Microsoft.Extensions.Options.IOptions<Microsoft.AspNetCore.HttpOverrides.CertificateForwardingOptions> options) { } + public System.Threading.Tasks.Task Invoke(Microsoft.AspNetCore.Http.HttpContext httpContext) { throw null; } + } + public partial class CertificateForwardingOptions + { + public System.Func<string, System.Security.Cryptography.X509Certificates.X509Certificate2> HeaderConverter; + public CertificateForwardingOptions() { } + public string CertificateHeader { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + } [System.FlagsAttribute] public enum ForwardedHeaders { @@ -75,3 +90,10 @@ namespace Microsoft.AspNetCore.HttpOverrides public bool Contains(System.Net.IPAddress address) { throw null; } } } +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class CertificateForwardingServiceExtensions + { + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddCertificateForwarding(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action<Microsoft.AspNetCore.HttpOverrides.CertificateForwardingOptions> configure) { throw null; } + } +} diff --git a/src/Middleware/HttpOverrides/src/CertificateForwardingBuilderExtensions.cs b/src/Middleware/HttpOverrides/src/CertificateForwardingBuilderExtensions.cs new file mode 100644 index 0000000000..038b19b637 --- /dev/null +++ b/src/Middleware/HttpOverrides/src/CertificateForwardingBuilderExtensions.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.HttpOverrides; + +namespace Microsoft.AspNetCore.Builder +{ + /// <summary> + /// Extension methods for using certificate fowarding. + /// </summary> + public static class CertificateForwardingBuilderExtensions + { + /// <summary> + /// Adds a middleware to the pipeline that will look for a certificate in a request header + /// decode it, and updates HttpContext.Connection.ClientCertificate. + /// </summary> + /// <param name="app"></param> + /// <returns></returns> + public static IApplicationBuilder UseCertificateForwarding(this IApplicationBuilder app) + { + if (app == null) + { + throw new ArgumentNullException(nameof(app)); + } + + return app.UseMiddleware<CertificateForwardingMiddleware>(); + } + } +} diff --git a/src/Middleware/HttpOverrides/src/CertificateForwardingFeature.cs b/src/Middleware/HttpOverrides/src/CertificateForwardingFeature.cs new file mode 100644 index 0000000000..a7d284a0cc --- /dev/null +++ b/src/Middleware/HttpOverrides/src/CertificateForwardingFeature.cs @@ -0,0 +1,51 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Cryptography.X509Certificates; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.HttpOverrides +{ + internal class CertificateForwardingFeature : ITlsConnectionFeature + { + private ILogger _logger; + private StringValues _header; + private CertificateForwardingOptions _options; + private X509Certificate2 _certificate; + + public CertificateForwardingFeature(ILogger logger, StringValues header, CertificateForwardingOptions options) + { + _logger = logger; + _options = options; + _header = header; + } + + public X509Certificate2 ClientCertificate + { + get + { + if (_certificate == null) + { + try + { + _certificate = _options.HeaderConverter(_header); + } + catch (Exception e) + { + _logger.NoCertificate(e); + } + } + return _certificate; + } + set => _certificate = value; + } + + public Task<X509Certificate2> GetClientCertificateAsync(CancellationToken cancellationToken) + => Task.FromResult(ClientCertificate); + } +} diff --git a/src/Middleware/HttpOverrides/src/CertificateForwardingMiddleware.cs b/src/Middleware/HttpOverrides/src/CertificateForwardingMiddleware.cs new file mode 100644 index 0000000000..77ff282361 --- /dev/null +++ b/src/Middleware/HttpOverrides/src/CertificateForwardingMiddleware.cs @@ -0,0 +1,66 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; + +namespace Microsoft.AspNetCore.HttpOverrides +{ + /// <summary> + /// Middleware that converts a forward header into a client certificate if found. + /// </summary> + public class CertificateForwardingMiddleware + { + private readonly RequestDelegate _next; + private readonly CertificateForwardingOptions _options; + private readonly ILogger _logger; + + /// <summary> + /// Constructor. + /// </summary> + /// <param name="next"></param> + /// <param name="loggerFactory"></param> + /// <param name="options"></param> + public CertificateForwardingMiddleware( + RequestDelegate next, + ILoggerFactory loggerFactory, + IOptions<CertificateForwardingOptions> options) + { + _next = next ?? throw new ArgumentNullException(nameof(next)); + + if (loggerFactory == null) + { + throw new ArgumentNullException(nameof(loggerFactory)); + } + + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + _options = options.Value; + _logger = loggerFactory.CreateLogger<CertificateForwardingMiddleware>(); + } + + /// <summary> + /// Looks for the presence of a <see cref="CertificateForwardingOptions.CertificateHeader"/> header in the request, + /// if found, converts this header to a ClientCertificate set on the connection. + /// </summary> + /// <param name="httpContext">The <see cref="HttpContext"/>.</param> + /// <returns>A <see cref="Task"/>.</returns> + public Task Invoke(HttpContext httpContext) + { + var header = httpContext.Request.Headers[_options.CertificateHeader]; + if (!StringValues.IsNullOrEmpty(header)) + { + httpContext.Features.Set<ITlsConnectionFeature>(new CertificateForwardingFeature(_logger, header, _options)); + } + return _next(httpContext); + } + } +} diff --git a/src/Middleware/HttpOverrides/src/CertificateForwardingOptions.cs b/src/Middleware/HttpOverrides/src/CertificateForwardingOptions.cs new file mode 100644 index 0000000000..4dccdda3b1 --- /dev/null +++ b/src/Middleware/HttpOverrides/src/CertificateForwardingOptions.cs @@ -0,0 +1,30 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Security.Cryptography.X509Certificates; + +namespace Microsoft.AspNetCore.HttpOverrides +{ + /// <summary> + /// Used to configure the <see cref="CertificateForwardingMiddleware"/>. + /// </summary> + public class CertificateForwardingOptions + { + /// <summary> + /// The name of the header containing the client certificate. + /// </summary> + /// <remarks> + /// This defaults to X-Client-Cert + /// </remarks> + public string CertificateHeader { get; set; } = "X-Client-Cert"; + + /// <summary> + /// The function used to convert the header to an instance of <see cref="X509Certificate2"/>. + /// </summary> + /// <remarks> + /// This defaults to a conversion from a base64 encoded string. + /// </remarks> + public Func<string, X509Certificate2> HeaderConverter = (headerValue) => new X509Certificate2(Convert.FromBase64String(headerValue)); + } +} diff --git a/src/Middleware/HttpOverrides/src/CertificateForwardingServiceExtensions.cs b/src/Middleware/HttpOverrides/src/CertificateForwardingServiceExtensions.cs new file mode 100644 index 0000000000..ffdd4e403b --- /dev/null +++ b/src/Middleware/HttpOverrides/src/CertificateForwardingServiceExtensions.cs @@ -0,0 +1,38 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.HttpOverrides; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// <summary> + /// Extension methods for using certificate fowarding. + /// </summary> + public static class CertificateForwardingServiceExtensions + { + /// <summary> + /// Adds certificate forwarding to the specified <see cref="IServiceCollection" />. + /// </summary> + /// <param name="services">The <see cref="IServiceCollection"/>.</param> + /// <param name="configure">An action delegate to configure the provided <see cref="CertificateForwardingOptions"/>.</param> + /// <returns>The <see cref="IServiceCollection"/> so that additional calls can be chained.</returns> + public static IServiceCollection AddCertificateForwarding( + this IServiceCollection services, + Action<CertificateForwardingOptions> configure) + { + if (services == null) + { + throw new ArgumentNullException(nameof(services)); + } + + if (configure == null) + { + throw new ArgumentNullException(nameof(configure)); + } + + services.AddOptions<CertificateForwardingOptions>().Validate(o => !string.IsNullOrEmpty(o.CertificateHeader), "CertificateForwarderOptions.CertificateHeader cannot be null or empty."); + return services.Configure(configure); + } + } +} diff --git a/src/Middleware/HttpOverrides/src/LoggingExtensions.cs b/src/Middleware/HttpOverrides/src/LoggingExtensions.cs new file mode 100644 index 0000000000..27a2dce494 --- /dev/null +++ b/src/Middleware/HttpOverrides/src/LoggingExtensions.cs @@ -0,0 +1,25 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.Extensions.Logging +{ + internal static class LoggingExtensions + { + private static Action<ILogger, Exception> _noCertificate; + + static LoggingExtensions() + { + _noCertificate = LoggerMessage.Define( + eventId: new EventId(0, "NoCertificate"), + logLevel: LogLevel.Warning, + formatString: "Could not read certificate from header."); + } + + public static void NoCertificate(this ILogger logger, Exception exception) + { + _noCertificate(logger, exception); + } + } +} diff --git a/src/Middleware/HttpOverrides/test/CertificateForwardingTest.cs b/src/Middleware/HttpOverrides/test/CertificateForwardingTest.cs new file mode 100644 index 0000000000..42464ccb98 --- /dev/null +++ b/src/Middleware/HttpOverrides/test/CertificateForwardingTest.cs @@ -0,0 +1,222 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; +using Microsoft.Net.Http.Headers; +using Xunit; + +namespace Microsoft.AspNetCore.HttpOverrides +{ + public class CertificateForwardingTests + { + [Fact] + public void VerifySettingNullHeaderOptionThrows() + { + var services = new ServiceCollection() + .AddOptions() + .AddCertificateForwarding(o => o.CertificateHeader = null); + var options = services.BuildServiceProvider().GetRequiredService<IOptions<CertificateForwardingOptions>>(); + Assert.Throws<OptionsValidationException>(() => options.Value); + } + + [Fact] + public void VerifySettingEmptyHeaderOptionThrows() + { + var services = new ServiceCollection() + .AddOptions() + .AddCertificateForwarding(o => o.CertificateHeader = ""); + var options = services.BuildServiceProvider().GetRequiredService<IOptions<CertificateForwardingOptions>>(); + Assert.Throws<OptionsValidationException>(() => options.Value); + } + + [Fact] + public async Task VerifyHeaderIsUsedIfNoCertificateAlreadySet() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddCertificateForwarding(options => { }); + }) + .Configure(app => + { + app.Use(async (context, next) => + { + Assert.Null(context.Connection.ClientCertificate); + await next(); + }); + app.UseCertificateForwarding(); + app.Use(async (context, next) => + { + Assert.Equal(context.Connection.ClientCertificate, Certificates.SelfSignedValidWithNoEku); + await next(); + }); + }); + var server = new TestServer(builder); + + var context = await server.SendAsync(c => + { + c.Request.Headers["X-Client-Cert"] = Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData); + }); + } + + [Fact] + public async Task VerifyHeaderOverridesCertificateEvenAlreadySet() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddCertificateForwarding(options => { }); + }) + .Configure(app => + { + app.Use(async (context, next) => + { + Assert.Null(context.Connection.ClientCertificate); + context.Connection.ClientCertificate = Certificates.SelfSignedNotYetValid; + await next(); + }); + app.UseCertificateForwarding(); + app.Use(async (context, next) => + { + Assert.Equal(context.Connection.ClientCertificate, Certificates.SelfSignedValidWithNoEku); + await next(); + }); + }); + var server = new TestServer(builder); + + var context = await server.SendAsync(c => + { + c.Request.Headers["X-Client-Cert"] = Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData); + }); + } + + [Fact] + public async Task VerifySettingTheAzureHeaderOnTheForwarderOptionsWorks() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddCertificateForwarding(options => options.CertificateHeader = "X-ARR-ClientCert"); + }) + .Configure(app => + { + app.Use(async (context, next) => + { + Assert.Null(context.Connection.ClientCertificate); + await next(); + }); + app.UseCertificateForwarding(); + app.Use(async (context, next) => + { + Assert.Equal(context.Connection.ClientCertificate, Certificates.SelfSignedValidWithNoEku); + await next(); + }); + }); + var server = new TestServer(builder); + + var context = await server.SendAsync(c => + { + c.Request.Headers["X-ARR-ClientCert"] = Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData); + }); + } + + [Fact] + public async Task VerifyACustomHeaderFailsIfTheHeaderIsNotPresent() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddCertificateForwarding(options => options.CertificateHeader = "some-random-header"); + }) + .Configure(app => + { + app.Use(async (context, next) => + { + Assert.Null(context.Connection.ClientCertificate); + await next(); + }); + app.UseCertificateForwarding(); + app.Use(async (context, next) => + { + Assert.Null(context.Connection.ClientCertificate); + await next(); + }); + }); + var server = new TestServer(builder); + + var context = await server.SendAsync(c => + { + c.Request.Headers["not-the-right-header"] = Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData); + }); + } + + [Fact] + public async Task VerifyArrHeaderEncodedCertFailsOnBadEncoding() + { + var builder = new WebHostBuilder() + .ConfigureServices(services => + { + services.AddCertificateForwarding(options => { }); + }) + .Configure(app => + { + app.Use(async (context, next) => + { + Assert.Null(context.Connection.ClientCertificate); + await next(); + }); + app.UseCertificateForwarding(); + app.Use(async (context, next) => + { + Assert.Null(context.Connection.ClientCertificate); + await next(); + }); + }); + var server = new TestServer(builder); + + var context = await server.SendAsync(c => + { + c.Request.Headers["X-Client-Cert"] = "OOPS" + Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData); + }); + } + + private static class Certificates + { + public static X509Certificate2 SelfSignedValidWithClientEku { get; private set; } = + new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedClientEkuCertificate.cer")); + + public static X509Certificate2 SelfSignedValidWithNoEku { get; private set; } = + new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedNoEkuCertificate.cer")); + + public static X509Certificate2 SelfSignedValidWithServerEku { get; private set; } = + new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedServerEkuCertificate.cer")); + + public static X509Certificate2 SelfSignedNotYetValid { get; private set; } = + new X509Certificate2(GetFullyQualifiedFilePath("selfSignedNoEkuCertificateNotValidYet.cer")); + + public static X509Certificate2 SelfSignedExpired { get; private set; } = + new X509Certificate2(GetFullyQualifiedFilePath("selfSignedNoEkuCertificateExpired.cer")); + + private static string GetFullyQualifiedFilePath(string filename) + { + var filePath = Path.Combine(AppContext.BaseDirectory, filename); + if (!File.Exists(filePath)) + { + throw new FileNotFoundException(filePath); + } + return filePath; + } + } + + } +} diff --git a/src/Middleware/HttpOverrides/test/Microsoft.AspNetCore.HttpOverrides.Tests.csproj b/src/Middleware/HttpOverrides/test/Microsoft.AspNetCore.HttpOverrides.Tests.csproj index da3cde94cb..c5f9652ddc 100644 --- a/src/Middleware/HttpOverrides/test/Microsoft.AspNetCore.HttpOverrides.Tests.csproj +++ b/src/Middleware/HttpOverrides/test/Microsoft.AspNetCore.HttpOverrides.Tests.csproj @@ -1,4 +1,4 @@ -<Project Sdk="Microsoft.NET.Sdk"> +<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>netcoreapp3.0</TargetFramework> @@ -8,6 +8,7 @@ <Reference Include="Microsoft.AspNetCore.HttpOverrides" /> <Reference Include="Microsoft.AspNetCore.TestHost" /> <Reference Include="Microsoft.Extensions.Logging.Testing" /> + <Content Include="$(SharedSourceRoot)test\Certificates\*.cer" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" /> </ItemGroup> </Project> diff --git a/src/Security/Authentication/Certificate/ref/Microsoft.AspNetCore.Authentication.Certificate.csproj b/src/Security/Authentication/Certificate/ref/Microsoft.AspNetCore.Authentication.Certificate.csproj new file mode 100644 index 0000000000..3cf6d51079 --- /dev/null +++ b/src/Security/Authentication/Certificate/ref/Microsoft.AspNetCore.Authentication.Certificate.csproj @@ -0,0 +1,10 @@ +<!-- This file is automatically generated. --> +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <TargetFrameworks>netcoreapp3.0</TargetFrameworks> + </PropertyGroup> + <ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.0'"> + <Compile Include="Microsoft.AspNetCore.Authentication.Certificate.netcoreapp3.0.cs" /> + <Reference Include="Microsoft.AspNetCore.Authentication" /> + </ItemGroup> +</Project> diff --git a/src/Security/Authentication/Certificate/ref/Microsoft.AspNetCore.Authentication.Certificate.netcoreapp3.0.cs b/src/Security/Authentication/Certificate/ref/Microsoft.AspNetCore.Authentication.Certificate.netcoreapp3.0.cs new file mode 100644 index 0000000000..7fa9e147ab --- /dev/null +++ b/src/Security/Authentication/Certificate/ref/Microsoft.AspNetCore.Authentication.Certificate.netcoreapp3.0.cs @@ -0,0 +1,59 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication.Certificate +{ + public static partial class CertificateAuthenticationDefaults + { + public const string AuthenticationScheme = "Certificate"; + } + public partial class CertificateAuthenticationEvents + { + public CertificateAuthenticationEvents() { } + public System.Func<Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationFailedContext, System.Threading.Tasks.Task> OnAuthenticationFailed { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public System.Func<Microsoft.AspNetCore.Authentication.Certificate.CertificateValidatedContext, System.Threading.Tasks.Task> OnCertificateValidated { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public virtual System.Threading.Tasks.Task AuthenticationFailed(Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationFailedContext context) { throw null; } + public virtual System.Threading.Tasks.Task CertificateValidated(Microsoft.AspNetCore.Authentication.Certificate.CertificateValidatedContext context) { throw null; } + } + public partial class CertificateAuthenticationFailedContext : Microsoft.AspNetCore.Authentication.ResultContext<Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions> + { + public CertificateAuthenticationFailedContext(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Authentication.AuthenticationScheme scheme, Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions options) : base (default(Microsoft.AspNetCore.Http.HttpContext), default(Microsoft.AspNetCore.Authentication.AuthenticationScheme), default(Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions)) { } + public System.Exception Exception { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + } + public partial class CertificateAuthenticationOptions : Microsoft.AspNetCore.Authentication.AuthenticationSchemeOptions + { + public CertificateAuthenticationOptions() { } + public Microsoft.AspNetCore.Authentication.Certificate.CertificateTypes AllowedCertificateTypes { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public new Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationEvents Events { get { throw null; } set { } } + public System.Security.Cryptography.X509Certificates.X509RevocationFlag RevocationFlag { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public System.Security.Cryptography.X509Certificates.X509RevocationMode RevocationMode { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public bool ValidateCertificateUse { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + public bool ValidateValidityPeriod { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + } + [System.FlagsAttribute] + public enum CertificateTypes + { + Chained = 1, + SelfSigned = 2, + All = 3, + } + public partial class CertificateValidatedContext : Microsoft.AspNetCore.Authentication.ResultContext<Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions> + { + public CertificateValidatedContext(Microsoft.AspNetCore.Http.HttpContext context, Microsoft.AspNetCore.Authentication.AuthenticationScheme scheme, Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions options) : base (default(Microsoft.AspNetCore.Http.HttpContext), default(Microsoft.AspNetCore.Authentication.AuthenticationScheme), default(Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions)) { } + public System.Security.Cryptography.X509Certificates.X509Certificate2 ClientCertificate { [System.Runtime.CompilerServices.CompilerGeneratedAttribute]get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute]set { } } + } + public static partial class X509Certificate2Extensions + { + public static bool IsSelfSigned(this System.Security.Cryptography.X509Certificates.X509Certificate2 certificate) { throw null; } + } +} +namespace Microsoft.Extensions.DependencyInjection +{ + public static partial class CertificateAuthenticationAppBuilderExtensions + { + public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddCertificate(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder) { throw null; } + public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddCertificate(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, System.Action<Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions> configureOptions) { throw null; } + public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddCertificate(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme) { throw null; } + public static Microsoft.AspNetCore.Authentication.AuthenticationBuilder AddCertificate(this Microsoft.AspNetCore.Authentication.AuthenticationBuilder builder, string authenticationScheme, System.Action<Microsoft.AspNetCore.Authentication.Certificate.CertificateAuthenticationOptions> configureOptions) { throw null; } + } +} diff --git a/src/Security/Authentication/Certificate/samples/Certificate.Sample/Certificate.Sample.csproj b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Certificate.Sample.csproj new file mode 100644 index 0000000000..2f085e5c41 --- /dev/null +++ b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Certificate.Sample.csproj @@ -0,0 +1,21 @@ +<Project Sdk="Microsoft.NET.Sdk.Web"> + + <PropertyGroup> + <TargetFramework>netcoreapp3.0</TargetFramework> + <AspNetCoreHostingModel>OutOfProcess</AspNetCoreHostingModel> + </PropertyGroup> + + <ItemGroup> + <Folder Include="wwwroot\" /> + </ItemGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore" /> + <Reference Include="Microsoft.AspNetCore.Authentication.Certificate" /> + <Reference Include="Microsoft.AspNetCore.Diagnostics" /> + <Reference Include="Microsoft.AspNetCore.Hosting" /> + <Reference Include="Microsoft.AspNetCore.Mvc" /> + <Reference Include="Microsoft.AspNetCore.Server.Kestrel" /> + </ItemGroup> + +</Project> diff --git a/src/Security/Authentication/Certificate/samples/Certificate.Sample/Controllers/HomeController.cs b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Controllers/HomeController.cs new file mode 100644 index 0000000000..60be48074b --- /dev/null +++ b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Controllers/HomeController.cs @@ -0,0 +1,12 @@ +using Microsoft.AspNetCore.Mvc; + +namespace Certificate.Sample.Controllers +{ + public class HomeController : Controller + { + public IActionResult Index() + { + return View(); + } + } +} diff --git a/src/Security/Authentication/Certificate/samples/Certificate.Sample/Program.cs b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Program.cs new file mode 100644 index 0000000000..1c4a2d2958 --- /dev/null +++ b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Program.cs @@ -0,0 +1,26 @@ +using Microsoft.AspNetCore; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Server.Kestrel.Https; + +namespace Certificate.Sample +{ + public class Program + { + public static void Main(string[] args) + { + BuildWebHost(args).Run(); + } + + public static IWebHost BuildWebHost(string[] args) + => WebHost.CreateDefaultBuilder(args) + .UseStartup<Startup>() + .ConfigureKestrel(options => + { + options.ConfigureHttpsDefaults(opt => + { + opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate; + }); + }) + .Build(); + } +} diff --git a/src/Security/Authentication/Certificate/samples/Certificate.Sample/Properties/launchSettings.json b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Properties/launchSettings.json new file mode 100644 index 0000000000..e796cb6c7e --- /dev/null +++ b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Properties/launchSettings.json @@ -0,0 +1,20 @@ +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "https://localhost:44331/", + "sslPort": 44331 + } + }, + "profiles": { + "Certificate.Sample": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:5001/" + } + } +} diff --git a/src/Security/Authentication/Certificate/samples/Certificate.Sample/Startup.cs b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Startup.cs new file mode 100644 index 0000000000..14e2702e07 --- /dev/null +++ b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Startup.cs @@ -0,0 +1,61 @@ +using System.Security.Claims; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication.Certificate; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Authorization; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; + +namespace Certificate.Sample +{ + public class Startup + { + // This method gets called by the runtime. Use this method to add services to the container. + // For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 + public void ConfigureServices(IServiceCollection services) + { + services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme) + .AddCertificate(options => + { + options.Events = new CertificateAuthenticationEvents + { + OnCertificateValidated = context => + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer), + new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer) + }; + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); + context.Success(); + + return Task.CompletedTask; + } + }; + }); + + services.AddAuthorization(); + + services.AddMvc(config => { }); + } + + // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) + { + app.UseRouting(); + + app.UseStatusCodePages(); + + app.UseAuthentication(); + app.UseAuthorization(); + + app.UseEndpoints(endpoints => + { + endpoints.MapDefaultControllerRoute(); + }); + } + } +} diff --git a/src/Security/Authentication/Certificate/samples/Certificate.Sample/Views/Home/Index.cshtml b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Views/Home/Index.cshtml new file mode 100644 index 0000000000..5247bfe9c6 --- /dev/null +++ b/src/Security/Authentication/Certificate/samples/Certificate.Sample/Views/Home/Index.cshtml @@ -0,0 +1 @@ +<h1>Hello @User.Identity.Name</h1>
\ No newline at end of file diff --git a/src/Security/Authentication/Certificate/src/CertificateAuthenticationDefaults.cs b/src/Security/Authentication/Certificate/src/CertificateAuthenticationDefaults.cs new file mode 100644 index 0000000000..d085dd3b70 --- /dev/null +++ b/src/Security/Authentication/Certificate/src/CertificateAuthenticationDefaults.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Authentication.Certificate +{ + /// <summary> + /// Default values related to certificate authentication middleware + /// </summary> + public static class CertificateAuthenticationDefaults + { + /// <summary> + /// The default value used for CertificateAuthenticationOptions.AuthenticationScheme + /// </summary> + public const string AuthenticationScheme = "Certificate"; + } +} diff --git a/src/Security/Authentication/Certificate/src/CertificateAuthenticationExtensions.cs b/src/Security/Authentication/Certificate/src/CertificateAuthenticationExtensions.cs new file mode 100644 index 0000000000..d49f2c274b --- /dev/null +++ b/src/Security/Authentication/Certificate/src/CertificateAuthenticationExtensions.cs @@ -0,0 +1,55 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Authentication; + +using Microsoft.AspNetCore.Authentication.Certificate; + +namespace Microsoft.Extensions.DependencyInjection +{ + /// <summary> + /// Extension methods to add Certificate authentication capabilities to an HTTP application pipeline. + /// </summary> + public static class CertificateAuthenticationAppBuilderExtensions + { + /// <summary> + /// Adds certificate authentication. + /// </summary> + /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param> + /// <returns>The <see cref="AuthenticationBuilder"/>.</returns> + public static AuthenticationBuilder AddCertificate(this AuthenticationBuilder builder) + => builder.AddCertificate(CertificateAuthenticationDefaults.AuthenticationScheme); + + /// <summary> + /// Adds certificate authentication. + /// </summary> + /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param> + /// <param name="authenticationScheme"></param> + /// <returns>The <see cref="AuthenticationBuilder"/>.</returns> + public static AuthenticationBuilder AddCertificate(this AuthenticationBuilder builder, string authenticationScheme) + => builder.AddCertificate(authenticationScheme, configureOptions: null); + + /// <summary> + /// Adds certificate authentication. + /// </summary> + /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param> + /// <param name="configureOptions"></param> + /// <returns>The <see cref="AuthenticationBuilder"/>.</returns> + public static AuthenticationBuilder AddCertificate(this AuthenticationBuilder builder, Action<CertificateAuthenticationOptions> configureOptions) + => builder.AddCertificate(CertificateAuthenticationDefaults.AuthenticationScheme, configureOptions); + + /// <summary> + /// Adds certificate authentication. + /// </summary> + /// <param name="builder">The <see cref="AuthenticationBuilder"/>.</param> + /// <param name="authenticationScheme"></param> + /// <param name="configureOptions"></param> + /// <returns>The <see cref="AuthenticationBuilder"/>.</returns> + public static AuthenticationBuilder AddCertificate( + this AuthenticationBuilder builder, + string authenticationScheme, + Action<CertificateAuthenticationOptions> configureOptions) + => builder.AddScheme<CertificateAuthenticationOptions, CertificateAuthenticationHandler>(authenticationScheme, configureOptions); + } +} diff --git a/src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs b/src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs new file mode 100644 index 0000000000..68a7abdde0 --- /dev/null +++ b/src/Security/Authentication/Certificate/src/CertificateAuthenticationHandler.cs @@ -0,0 +1,235 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; +using System.Text.Encodings.Web; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Authentication.Certificate +{ + internal class CertificateAuthenticationHandler : AuthenticationHandler<CertificateAuthenticationOptions> + { + private static readonly Oid ClientCertificateOid = new Oid("1.3.6.1.5.5.7.3.2"); + + public CertificateAuthenticationHandler( + IOptionsMonitor<CertificateAuthenticationOptions> options, + ILoggerFactory logger, + UrlEncoder encoder, + ISystemClock clock) : base(options, logger, encoder, clock) + { + } + + /// <summary> + /// The handler calls methods on the events which give the application control at certain points where processing is occurring. + /// If it is not provided a default instance is supplied which does nothing when the methods are called. + /// </summary> + protected new CertificateAuthenticationEvents Events + { + get { return (CertificateAuthenticationEvents)base.Events; } + set { base.Events = value; } + } + + /// <summary> + /// Creates a new instance of the events instance. + /// </summary> + /// <returns>A new instance of the events instance.</returns> + protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new CertificateAuthenticationEvents()); + + protected override async Task<AuthenticateResult> HandleAuthenticateAsync() + { + // You only get client certificates over HTTPS + if (!Context.Request.IsHttps) + { + return AuthenticateResult.NoResult(); + } + + try + { + var clientCertificate = await Context.Connection.GetClientCertificateAsync(); + + // This should never be the case, as cert authentication happens long before ASP.NET kicks in. + if (clientCertificate == null) + { + Logger.NoCertificate(); + return AuthenticateResult.NoResult(); + } + + // If we have a self signed cert, and they're not allowed, exit early and not bother with + // any other validations. + if (clientCertificate.IsSelfSigned() && + !Options.AllowedCertificateTypes.HasFlag(CertificateTypes.SelfSigned)) + { + Logger.CertificateRejected("Self signed", clientCertificate.Subject); + return AuthenticateResult.Fail("Options do not allow self signed certificates."); + } + + // If we have a chained cert, and they're not allowed, exit early and not bother with + // any other validations. + if (!clientCertificate.IsSelfSigned() && + !Options.AllowedCertificateTypes.HasFlag(CertificateTypes.Chained)) + { + Logger.CertificateRejected("Chained", clientCertificate.Subject); + return AuthenticateResult.Fail("Options do not allow chained certificates."); + } + + var chainPolicy = BuildChainPolicy(clientCertificate); + var chain = new X509Chain + { + ChainPolicy = chainPolicy + }; + + var certificateIsValid = chain.Build(clientCertificate); + if (!certificateIsValid) + { + var chainErrors = new List<string>(); + foreach (var validationFailure in chain.ChainStatus) + { + chainErrors.Add($"{validationFailure.Status} {validationFailure.StatusInformation}"); + } + Logger.CertificateFailedValidation(clientCertificate.Subject, chainErrors); + return AuthenticateResult.Fail("Client certificate failed validation."); + } + + var certificateValidatedContext = new CertificateValidatedContext(Context, Scheme, Options) + { + ClientCertificate = clientCertificate, + Principal = CreatePrincipal(clientCertificate) + }; + + await Events.CertificateValidated(certificateValidatedContext); + + if (certificateValidatedContext.Result != null) + { + return certificateValidatedContext.Result; + } + + certificateValidatedContext.Success(); + return certificateValidatedContext.Result; + } + catch (Exception ex) + { + var authenticationFailedContext = new CertificateAuthenticationFailedContext(Context, Scheme, Options) + { + Exception = ex + }; + + await Events.AuthenticationFailed(authenticationFailedContext); + + if (authenticationFailedContext.Result != null) + { + return authenticationFailedContext.Result; + } + + throw; + } + } + + protected override Task HandleChallengeAsync(AuthenticationProperties properties) + { + // Certificate authentication takes place at the connection level. We can't prompt once we're in + // user code, so the best thing to do is Forbid, not Challenge. + return HandleForbiddenAsync(properties); + } + + private X509ChainPolicy BuildChainPolicy(X509Certificate2 certificate) + { + // Now build the chain validation options. + X509RevocationFlag revocationFlag = Options.RevocationFlag; + X509RevocationMode revocationMode = Options.RevocationMode; + + if (certificate.IsSelfSigned()) + { + // Turn off chain validation, because we have a self signed certificate. + revocationFlag = X509RevocationFlag.EntireChain; + revocationMode = X509RevocationMode.NoCheck; + } + + var chainPolicy = new X509ChainPolicy + { + RevocationFlag = revocationFlag, + RevocationMode = revocationMode, + }; + + if (Options.ValidateCertificateUse) + { + chainPolicy.ApplicationPolicy.Add(ClientCertificateOid); + } + + if (certificate.IsSelfSigned()) + { + chainPolicy.VerificationFlags |= X509VerificationFlags.AllowUnknownCertificateAuthority; + chainPolicy.VerificationFlags |= X509VerificationFlags.IgnoreEndRevocationUnknown; + chainPolicy.ExtraStore.Add(certificate); + } + + if (!Options.ValidateValidityPeriod) + { + chainPolicy.VerificationFlags |= X509VerificationFlags.IgnoreNotTimeValid; + } + + return chainPolicy; + } + + private ClaimsPrincipal CreatePrincipal(X509Certificate2 certificate) + { + var claims = new List<Claim>(); + + var issuer = certificate.Issuer; + claims.Add(new Claim("issuer", issuer, ClaimValueTypes.String, Options.ClaimsIssuer)); + + var thumbprint = certificate.Thumbprint; + claims.Add(new Claim(ClaimTypes.Thumbprint, thumbprint, ClaimValueTypes.Base64Binary, Options.ClaimsIssuer)); + + var value = certificate.SubjectName.Name; + if (!string.IsNullOrWhiteSpace(value)) + { + claims.Add(new Claim(ClaimTypes.X500DistinguishedName, value, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + + value = certificate.SerialNumber; + if (!string.IsNullOrWhiteSpace(value)) + { + claims.Add(new Claim(ClaimTypes.SerialNumber, value, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + + value = certificate.GetNameInfo(X509NameType.DnsName, false); + if (!string.IsNullOrWhiteSpace(value)) + { + claims.Add(new Claim(ClaimTypes.Dns, value, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + + value = certificate.GetNameInfo(X509NameType.SimpleName, false); + if (!string.IsNullOrWhiteSpace(value)) + { + claims.Add(new Claim(ClaimTypes.Name, value, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + + value = certificate.GetNameInfo(X509NameType.EmailName, false); + if (!string.IsNullOrWhiteSpace(value)) + { + claims.Add(new Claim(ClaimTypes.Email, value, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + + value = certificate.GetNameInfo(X509NameType.UpnName, false); + if (!string.IsNullOrWhiteSpace(value)) + { + claims.Add(new Claim(ClaimTypes.Upn, value, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + + value = certificate.GetNameInfo(X509NameType.UrlName, false); + if (!string.IsNullOrWhiteSpace(value)) + { + claims.Add(new Claim(ClaimTypes.Uri, value, ClaimValueTypes.String, Options.ClaimsIssuer)); + } + + var identity = new ClaimsIdentity(claims, CertificateAuthenticationDefaults.AuthenticationScheme); + return new ClaimsPrincipal(identity); + } + } +} diff --git a/src/Security/Authentication/Certificate/src/CertificateAuthenticationOptions.cs b/src/Security/Authentication/Certificate/src/CertificateAuthenticationOptions.cs new file mode 100644 index 0000000000..1b8eebfa6f --- /dev/null +++ b/src/Security/Authentication/Certificate/src/CertificateAuthenticationOptions.cs @@ -0,0 +1,53 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Cryptography.X509Certificates; + +namespace Microsoft.AspNetCore.Authentication.Certificate +{ + /// <summary> + /// Options used to configure certificate authentication. + /// </summary> + public class CertificateAuthenticationOptions : AuthenticationSchemeOptions + { + /// <summary> + /// Value indicating the types of certificates accepted by the authentication middleware. + /// </summary> + public CertificateTypes AllowedCertificateTypes { get; set; } = CertificateTypes.Chained; + + /// <summary> + /// Flag indicating whether the client certificate must be suitable for client + /// authentication, either via the Client Authentication EKU, or having no EKUs + /// at all. If the certificate chains to a root CA all certificates in the chain must be validate + /// for the client authentication EKU. + /// </summary> + public bool ValidateCertificateUse { get; set; } = true; + + /// <summary> + /// Flag indicating whether the client certificate validity period should be checked. + /// </summary> + public bool ValidateValidityPeriod { get; set; } = true; + + /// <summary> + /// Specifies which X509 certificates in the chain should be checked for revocation. + /// </summary> + public X509RevocationFlag RevocationFlag { get; set; } = X509RevocationFlag.ExcludeRoot; + + /// <summary> + /// Specifies conditions under which verification of certificates in the X509 chain should be conducted. + /// </summary> + public X509RevocationMode RevocationMode { get; set; } = X509RevocationMode.Online; + + /// <summary> + /// The object provided by the application to process events raised by the certificate authentication middleware. + /// The application may implement the interface fully, or it may create an instance of CertificateAuthenticationEvents + /// and assign delegates only to the events it wants to process. + /// </summary> + public new CertificateAuthenticationEvents Events + { + get { return (CertificateAuthenticationEvents)base.Events; } + + set { base.Events = value; } + } + } +} diff --git a/src/Security/Authentication/Certificate/src/CertificateTypes.cs b/src/Security/Authentication/Certificate/src/CertificateTypes.cs new file mode 100644 index 0000000000..ab238ce3e6 --- /dev/null +++ b/src/Security/Authentication/Certificate/src/CertificateTypes.cs @@ -0,0 +1,29 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; + +namespace Microsoft.AspNetCore.Authentication.Certificate +{ + /// <summary> + /// Enum representing certificate types. + /// </summary> + [Flags] + public enum CertificateTypes + { + /// <summary> + /// Chained certificates. + /// </summary> + Chained = 1, + + /// <summary> + /// SelfSigned certificates. + /// </summary> + SelfSigned = 2, + + /// <summary> + /// All certificates. + /// </summary> + All = Chained | SelfSigned + } +} diff --git a/src/Security/Authentication/Certificate/src/Events/CertificateAuthenticationEvents.cs b/src/Security/Authentication/Certificate/src/Events/CertificateAuthenticationEvents.cs new file mode 100644 index 0000000000..bf6e559e5f --- /dev/null +++ b/src/Security/Authentication/Certificate/src/Events/CertificateAuthenticationEvents.cs @@ -0,0 +1,45 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Threading.Tasks; + +namespace Microsoft.AspNetCore.Authentication.Certificate +{ + /// <summary> + /// This default implementation of the IBasicAuthenticationEvents may be used if the + /// application only needs to override a few of the interface methods. + /// This may be used as a base class or may be instantiated directly. + /// </summary> + public class CertificateAuthenticationEvents + { + /// <summary> + /// A delegate assigned to this property will be invoked when the authentication fails. + /// </summary> + public Func<CertificateAuthenticationFailedContext, Task> OnAuthenticationFailed { get; set; } = context => Task.CompletedTask; + + /// <summary> + /// A delegate assigned to this property will be invoked when a certificate has passed basic validation, but where custom validation may be needed. + /// </summary> + /// <remarks> + /// You must provide a delegate for this property for authentication to occur. + /// In your delegate you should construct an authentication principal from the user details, + /// attach it to the context.Principal property and finally call context.Success(); + /// </remarks> + public Func<CertificateValidatedContext, Task> OnCertificateValidated { get; set; } = context => Task.CompletedTask; + + /// <summary> + /// Invoked when a certificate fails authentication. + /// </summary> + /// <param name="context"></param> + /// <returns></returns> + public virtual Task AuthenticationFailed(CertificateAuthenticationFailedContext context) => OnAuthenticationFailed(context); + + /// <summary> + /// Invoked after a certificate has been validated + /// </summary> + /// <param name="context"></param> + /// <returns></returns> + public virtual Task CertificateValidated(CertificateValidatedContext context) => OnCertificateValidated(context); + } +} diff --git a/src/Security/Authentication/Certificate/src/Events/CertificateAuthenticationFailedContext.cs b/src/Security/Authentication/Certificate/src/Events/CertificateAuthenticationFailedContext.cs new file mode 100644 index 0000000000..9742a149c2 --- /dev/null +++ b/src/Security/Authentication/Certificate/src/Events/CertificateAuthenticationFailedContext.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.Certificate +{ + /// <summary> + /// Context used when a failure occurs. + /// </summary> + public class CertificateAuthenticationFailedContext : ResultContext<CertificateAuthenticationOptions> + { + /// <summary> + /// Constructor. + /// </summary> + /// <param name="context"></param> + /// <param name="scheme"></param> + /// <param name="options"></param> + public CertificateAuthenticationFailedContext( + HttpContext context, + AuthenticationScheme scheme, + CertificateAuthenticationOptions options) + : base(context, scheme, options) + { + } + + /// <summary> + /// The exception. + /// </summary> + public Exception Exception { get; set; } + } +} diff --git a/src/Security/Authentication/Certificate/src/Events/CertificateValidatedContext.cs b/src/Security/Authentication/Certificate/src/Events/CertificateValidatedContext.cs new file mode 100644 index 0000000000..9a3870b3bd --- /dev/null +++ b/src/Security/Authentication/Certificate/src/Events/CertificateValidatedContext.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Http; + +namespace Microsoft.AspNetCore.Authentication.Certificate +{ + /// <summary> + /// Context used when certificates are being validated. + /// </summary> + public class CertificateValidatedContext : ResultContext<CertificateAuthenticationOptions> + { + /// <summary> + /// Creates a new instance of <see cref="CertificateValidatedContext"/>. + /// </summary> + /// <param name="context">The HttpContext the validate context applies too.</param> + /// <param name="scheme">The scheme used when the Certificate Authentication handler was registered.</param> + /// <param name="options">The <see cref="CertificateAuthenticationOptions"/>.</param> + public CertificateValidatedContext( + HttpContext context, + AuthenticationScheme scheme, + CertificateAuthenticationOptions options) + : base(context, scheme, options) + { + } + + /// <summary> + /// The certificate to validate. + /// </summary> + public X509Certificate2 ClientCertificate { get; set; } + } +} diff --git a/src/Security/Authentication/Certificate/src/LoggingExtensions.cs b/src/Security/Authentication/Certificate/src/LoggingExtensions.cs new file mode 100644 index 0000000000..2219a349b6 --- /dev/null +++ b/src/Security/Authentication/Certificate/src/LoggingExtensions.cs @@ -0,0 +1,48 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Extensions.Logging +{ + internal static class LoggingExtensions + { + private static Action<ILogger, Exception> _noCertificate; + private static Action<ILogger, string, string, Exception> _certRejected; + private static Action<ILogger, string, string, Exception> _certFailedValidation; + + static LoggingExtensions() + { + _noCertificate = LoggerMessage.Define( + eventId: new EventId(0, "NoCertificate"), + logLevel: LogLevel.Debug, + formatString: "No client certificate found."); + + _certRejected = LoggerMessage.Define<string, string>( + eventId: new EventId(1, "CertificateRejected"), + logLevel: LogLevel.Warning, + formatString: "{CertificateType} certificate rejected, subject was {Subject}."); + + _certFailedValidation = LoggerMessage.Define<string, string>( + eventId: new EventId(2, "CertificateFailedValidation"), + logLevel: LogLevel.Warning, + formatString: "Certificate validation failed, subject was {Subject}." + Environment.NewLine + "{ChainErrors}"); + } + + public static void NoCertificate(this ILogger logger) + { + _noCertificate(logger, null); + } + + public static void CertificateRejected(this ILogger logger, string certificateType, string subject) + { + _certRejected(logger, certificateType, subject, null); + } + + public static void CertificateFailedValidation(this ILogger logger, string subject, IEnumerable<string> chainedErrors) + { + _certFailedValidation(logger, subject, String.Join(Environment.NewLine, chainedErrors), null); + } + } +} diff --git a/src/Security/Authentication/Certificate/src/Microsoft.AspNetCore.Authentication.Certificate.csproj b/src/Security/Authentication/Certificate/src/Microsoft.AspNetCore.Authentication.Certificate.csproj new file mode 100644 index 0000000000..1795d68877 --- /dev/null +++ b/src/Security/Authentication/Certificate/src/Microsoft.AspNetCore.Authentication.Certificate.csproj @@ -0,0 +1,16 @@ +<Project Sdk="Microsoft.NET.Sdk"> + + <PropertyGroup> + <Description>ASP.NET Core middleware that enables an application to support certificate authentication.</Description> + <TargetFramework>netcoreapp3.0</TargetFramework> + <DefineConstants>$(DefineConstants);SECURITY</DefineConstants> + <GenerateDocumentationFile>true</GenerateDocumentationFile> + <PackageTags>aspnetcore;authentication;security;x509;certificate</PackageTags> + <IsShippingPackage>true</IsShippingPackage> + </PropertyGroup> + + <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Authentication" /> + </ItemGroup> + +</Project> diff --git a/src/Security/Authentication/Certificate/src/README-IISConfig.png b/src/Security/Authentication/Certificate/src/README-IISConfig.png Binary files differnew file mode 100644 index 0000000000..3af15e9d06 --- /dev/null +++ b/src/Security/Authentication/Certificate/src/README-IISConfig.png diff --git a/src/Security/Authentication/Certificate/src/README.md b/src/Security/Authentication/Certificate/src/README.md new file mode 100644 index 0000000000..542131fdf1 --- /dev/null +++ b/src/Security/Authentication/Certificate/src/README.md @@ -0,0 +1,234 @@ +# Microsoft.AspNetCore.Authentication.Certificate + +This project sort of contains an implementation of [Certificate Authentication](https://tools.ietf.org/html/rfc5246#section-7.4.4) for ASP.NET Core. +Certificate authentication happens at the TLS level, long before it ever gets to ASP.NET Core, so, more accurately this is an authentication handler +that validates the certificate and then gives you an event where you can resolve that certificate to a ClaimsPrincipal. + +You **must** [configure your host](#hostConfiguration) for certificate authentication, be it IIS, Kestrel, Azure Web Applications or whatever else you're using. + +## Getting started + +First acquire an HTTPS certificate, apply it and then [configure your host](#hostConfiguration) to require certificates. + +In your web application add a reference to the package, then in the `ConfigureServices` method in `startup.cs` call +`app.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).UseCertificateAuthentication(...);` with your options, +providing a delegate for `OnValidateCertificate` to validate the client certificate sent with requests and turn that information +into an `ClaimsPrincipal`, set it on the `context.Principal` property and call `context.Success()`. + +If you change your scheme name in the options for the authentication handler you need to change the scheme name in +`AddAuthentication()` to ensure it's used on every request which ends in an endpoint that requires authorization. + +If authentication fails this handler will return a `403 (Forbidden)` response rather a `401 (Unauthorized)` as you +might expect - this is because the authentication should happen during the initial TLS connection - by the time it +reaches the handler it's too late, and there's no way to actually upgrade the connection from an anonymous connection +to one with a certificate. + +You must also add `app.UseAuthentication();` in the `Configure` method, otherwise nothing will ever get called. + +For example; + +```c# +public void ConfigureServices(IServiceCollection services) +{ + services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme) + .AddCertificate(); + // All the other service configuration. +} + +public void Configure(IApplicationBuilder app, IHostingEnvironment env) +{ + app.UseAuthentication(); + + // All the other app configuration. +} +``` + +In the sample above you can see the default way to add certificate authentication. The handler will construct a user principal using the common certificate properties for you. + +## Configuring Certificate Validation + +The `CertificateAuthenticationOptions` handler has some built in validations that are the minimium validations you should perform on +a certificate. Each of these settings are turned on by default. + +### ValidateCertificateChain + +This check validates that the issuer for the certificate is trusted by the application host OS. If +you are going to accept self-signed certificates you must disable this check. + +### ValidateCertificateUse + +This check validates that the certificate presented by the client has the Client Authentication +extended key use, or no EKUs at all (as the specifications say if no EKU is specified then all EKUs +are valid). + +### ValidateValidityPeriod + +This check validates that the certificate is within its validity period. As the handler runs on every +request this ensures that a certificate that was valid when it was presented has not expired during +its current session. + +### RevocationFlag + +A flag which specifies which certificates in the chain are checked for revocation. + +Revocation checks are only performed when the certificate is chained to a root certificate. + +### RevocationMode + +A flag which specifies how revocation checks are performed. +Specifying an on-line check can result in a long delay while the certificate authority is contacted. + +Revocation checks are only performed when the certificate is chained to a root certificate. + +### Can I configure my application to require a certificate only on certain paths? + +Not possible, remember the certificate exchange is done that the start of the HTTPS conversation, +it's done by the host, not the application. Kestrel, IIS, Azure Web Apps don't have any configuration for +this sort of thing. + +# Handler events + +The handler has two events, `OnAuthenticationFailed()`, which is called if an exception happens during authentication and allows you to react, and `OnValidateCertificate()` which is +called after certificate has been validated, passed validation, abut before the default principal has been created. This allows you to perform your own validation, for example +checking if the certificate is one your services knows about, and to construct your own principal. For example, + +```c# +services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme) + .AddCertificate(options => + { + options.Events = new CertificateAuthenticationEvents + { + OnValidateCertificate = context => + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer), + new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer) + }; + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); + context.Success(); + + return Task.CompletedTask; + } + }; + }); +``` + +If you find the inbound certificate doesn't meet your extra validation call `context.Fail("failure Reason")` with a failure reason. + +For real functionality you will probably want to call a service registered in DI which talks to a database or other type of +user store. You can grab your service by using the context passed into your delegates, like so + +```c# +services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme) + .AddCertificate(options => + { + options.Events = new CertificateAuthenticationEvents + { + OnCertificateValidated = context => + { + var validationService = + context.HttpContext.RequestServices.GetService<ICertificateValidationService>(); + + if (validationService.ValidateCertificate(context.ClientCertificate)) + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer), + new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer) + }; + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); + context.Success(); + } + + return Task.CompletedTask; + } + }; + }); +``` +Note that conceptually the validation of the certification is an authorization concern, and putting a check on, for example, an issuer or thumbprint in an authorization policy rather +than inside OnCertificateValidated() is perfectly acceptable. + +## <a name="hostConfiguration"></a>Configuring your host to require certificates + +### Kestrel + +In program.cs configure `UseKestrel()` as follows. + +```c# +public static IWebHost BuildWebHost(string[] args) + => WebHost.CreateDefaultBuilder(args) + .UseStartup<Startup>() + .ConfigureKestrel(options => + { + options.ConfigureHttpsDefaults(opt => + { + opt.ClientCertificateMode = ClientCertificateMode.RequireCertificate; + }); + }) + .Build(); +``` +You must set the `ClientCertificateValidation` delegate to `CertificateValidator.DisableChannelValidation` in order to stop Kestrel using the default OS certificate validation routine and, +instead, letting the authentication handler perform the validation. + +### IIS + +In the IIS Manager + +1. Select your Site in the Connections tab. +2. Double click the SSL Settings in the Features View window. +3. Check the `Require SSL` Check Box and select the `Require` radio button under Client Certificates. + +![Client Certificate Settings in IIS](README-IISConfig.png "Client Certificate Settings in IIS") + +### Azure + +See the [Azure documentation](https://docs.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth) +to configure Azure Web Apps then add the following to your application startup method, `Configure(IApplicationBuilder app)` add the +following line before the call to `app.UseAuthentication();` + +```c# +app.UseCertificateHeaderForwarding(); +``` + +### Random custom web proxies + +If you're using a proxy which isn't IIS or Azure's Web Apps Application Request Routing you will need to configure your proxy +to forward the certificate it received in an HTTP header. +In your application startup method, `Configure(IApplicationBuilder app)`, add the +following line before the call to `app.UseAuthentication();` + +```c# +app.UseCertificateForwarding(); +``` + +You will also need to configure the Certificate Forwarding middleware to specify the header name. +In your service configuration method, `ConfigureServices(IServiceCollection services)` add +the following code to configure the header the forwarding middleware will build a certificate from; + +```c# +services.AddCertificateForwarding(options => +{ + options.CertificateHeader = "YOUR_CUSTOM_HEADER_NAME"; +}); +``` + +Finally, if your proxy is doing something weird to pass the header on, rather than base 64 encoding it +(looking at you nginx (╯°□°)╯︵ ┻━┻) you can override the converter option to be a func that will +perform the optional conversion, for example + +```c# +services.AddCertificateForwarding(options => +{ + options.CertificateHeader = "YOUR_CUSTOM_HEADER_NAME"; + options.HeaderConverter = (headerValue) => + { + var clientCertificate = + /* some weird conversion logic to create an X509Certificate2 */ + return clientCertificate; + } +}); +``` + diff --git a/src/Security/Authentication/Certificate/src/X509CertificateExtensions.cs b/src/Security/Authentication/Certificate/src/X509CertificateExtensions.cs new file mode 100644 index 0000000000..de8f0d3df2 --- /dev/null +++ b/src/Security/Authentication/Certificate/src/X509CertificateExtensions.cs @@ -0,0 +1,27 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Security.Cryptography.X509Certificates; + +namespace Microsoft.AspNetCore.Authentication.Certificate +{ + /// <summary> + /// Extension methods for <see cref="X509Certificate2"/>. + /// </summary> + public static class X509Certificate2Extensions + { + /// <summary> + /// Determines if the certificate is self signed. + /// </summary> + /// <param name="certificate">The <see cref="X509Certificate2"/>.</param> + /// <returns>True if the certificate is self signed.</returns> + public static bool IsSelfSigned(this X509Certificate2 certificate) + { + Span<byte> subject = certificate.SubjectName.RawData; + Span<byte> issuer = certificate.IssuerName.RawData; + return subject.SequenceEqual(issuer); + } + } +} diff --git a/src/Security/Authentication/test/CertificateTests.cs b/src/Security/Authentication/test/CertificateTests.cs new file mode 100644 index 0000000000..a5a2d0294d --- /dev/null +++ b/src/Security/Authentication/test/CertificateTests.cs @@ -0,0 +1,628 @@ +// Copyright (c) Barry Dorrans. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.IO; +using System.Linq; +using System.Net; +using System.Security.Claims; +using System.Security.Cryptography.X509Certificates; +using System.Threading.Tasks; +using System.Xml.Linq; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.TestHost; +using Microsoft.AspNetCore.Testing.xunit; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.Authentication.Certificate.Test +{ + public class ClientCertificateAuthenticationTests + { + + [Fact] + public async Task VerifySchemeDefaults() + { + var services = new ServiceCollection(); + services.AddAuthentication().AddCertificate(); + var sp = services.BuildServiceProvider(); + var schemeProvider = sp.GetRequiredService<IAuthenticationSchemeProvider>(); + var scheme = await schemeProvider.GetSchemeAsync(CertificateAuthenticationDefaults.AuthenticationScheme); + Assert.NotNull(scheme); + Assert.Equal("CertificateAuthenticationHandler", scheme.HandlerType.Name); + Assert.Null(scheme.DisplayName); + } + + [Fact] + public void VerifyIsSelfSignedExtensionMethod() + { + Assert.True(Certificates.SelfSignedValidWithNoEku.IsSelfSigned()); + } + + [Fact] + public async Task VerifyValidSelfSignedWithClientEkuAuthenticates() + { + var server = CreateServer( + new CertificateAuthenticationOptions + { + AllowedCertificateTypes = CertificateTypes.SelfSigned, + Events = sucessfulValidationEvents + }, + Certificates.SelfSignedValidWithClientEku); + + var response = await server.CreateClient().GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task VerifyValidSelfSignedWithNoEkuAuthenticates() + { + var server = CreateServer( + new CertificateAuthenticationOptions + { + AllowedCertificateTypes = CertificateTypes.SelfSigned, + Events = sucessfulValidationEvents + }, + Certificates.SelfSignedValidWithNoEku); + + var response = await server.CreateClient().GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task VerifyValidSelfSignedWithClientEkuFailsWhenSelfSignedCertsNotAllowed() + { + var server = CreateServer( + new CertificateAuthenticationOptions + { + AllowedCertificateTypes = CertificateTypes.Chained + }, + Certificates.SelfSignedValidWithClientEku); + + var response = await server.CreateClient().GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task VerifyValidSelfSignedWithNoEkuFailsWhenSelfSignedCertsNotAllowed() + { + var server = CreateServer( + new CertificateAuthenticationOptions + { + AllowedCertificateTypes = CertificateTypes.Chained, + Events = sucessfulValidationEvents + }, + Certificates.SelfSignedValidWithNoEku); + + var response = await server.CreateClient().GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task VerifyValidSelfSignedWithServerFailsEvenIfSelfSignedCertsAreAllowed() + { + var server = CreateServer( + new CertificateAuthenticationOptions + { + AllowedCertificateTypes = CertificateTypes.SelfSigned, + Events = sucessfulValidationEvents + }, + Certificates.SelfSignedValidWithServerEku); + + var response = await server.CreateClient().GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task VerifyValidSelfSignedWithServerPassesWhenSelfSignedCertsAreAllowedAndPurposeValidationIsOff() + { + var server = CreateServer( + new CertificateAuthenticationOptions + { + AllowedCertificateTypes = CertificateTypes.SelfSigned, + ValidateCertificateUse = false, + Events = sucessfulValidationEvents + }, + Certificates.SelfSignedValidWithServerEku); + + var response = await server.CreateClient().GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task VerifyValidSelfSignedWithServerFailsPurposeValidationIsOffButSelfSignedCertsAreNotAllowed() + { + var server = CreateServer( + new CertificateAuthenticationOptions + { + AllowedCertificateTypes = CertificateTypes.Chained, + ValidateCertificateUse = false, + Events = sucessfulValidationEvents + }, + Certificates.SelfSignedValidWithServerEku); + + var response = await server.CreateClient().GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task VerifyExpiredSelfSignedFails() + { + var server = CreateServer( + new CertificateAuthenticationOptions + { + AllowedCertificateTypes = CertificateTypes.SelfSigned, + ValidateCertificateUse = false, + Events = sucessfulValidationEvents + }, + Certificates.SelfSignedExpired); + + var response = await server.CreateClient().GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task VerifyExpiredSelfSignedPassesIfDateRangeValidationIsDisabled() + { + var server = CreateServer( + new CertificateAuthenticationOptions + { + AllowedCertificateTypes = CertificateTypes.SelfSigned, + ValidateValidityPeriod = false, + Events = sucessfulValidationEvents + }, + Certificates.SelfSignedExpired); + + var response = await server.CreateClient().GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task VerifyNotYetValidSelfSignedFails() + { + var server = CreateServer( + new CertificateAuthenticationOptions + { + AllowedCertificateTypes = CertificateTypes.SelfSigned, + ValidateCertificateUse = false, + Events = sucessfulValidationEvents + }, + Certificates.SelfSignedNotYetValid); + + var response = await server.CreateClient().GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task VerifyNotYetValidSelfSignedPassesIfDateRangeValidationIsDisabled() + { + var server = CreateServer( + new CertificateAuthenticationOptions + { + AllowedCertificateTypes = CertificateTypes.SelfSigned, + ValidateValidityPeriod = false, + Events = sucessfulValidationEvents + }, + Certificates.SelfSignedNotYetValid); + + var response = await server.CreateClient().GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task VerifyFailingInTheValidationEventReturnsForbidden() + { + var server = CreateServer( + new CertificateAuthenticationOptions + { + ValidateCertificateUse = false, + Events = failedValidationEvents + }, + Certificates.SelfSignedValidWithServerEku); + + var response = await server.CreateClient().GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task DoingNothingInTheValidationEventReturnsOK() + { + var server = CreateServer( + new CertificateAuthenticationOptions + { + AllowedCertificateTypes = CertificateTypes.SelfSigned, + ValidateCertificateUse = false, + Events = unprocessedValidationEvents + }, + Certificates.SelfSignedValidWithServerEku); + + var response = await server.CreateClient().GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task VerifyNotSendingACertificateEndsUpInForbidden() + { + var server = CreateServer( + new CertificateAuthenticationOptions + { + Events = sucessfulValidationEvents + }); + + var response = await server.CreateClient().GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task VerifyHeaderIsUsedIfCertIsNotPresent() + { + var server = CreateServer( + new CertificateAuthenticationOptions + { + AllowedCertificateTypes = CertificateTypes.SelfSigned, + Events = sucessfulValidationEvents + }, + wireUpHeaderMiddleware : true); + + var client = server.CreateClient(); + client.DefaultRequestHeaders.Add("X-Client-Cert", Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData)); + var response = await client.GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task VerifyHeaderEncodedCertFailsOnBadEncoding() + { + var server = CreateServer( + new CertificateAuthenticationOptions + { + Events = sucessfulValidationEvents + }, + wireUpHeaderMiddleware: true); + + var client = server.CreateClient(); + client.DefaultRequestHeaders.Add("X-Client-Cert", "OOPS" + Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData)); + var response = await client.GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task VerifySettingTheAzureHeaderOnTheForwarderOptionsWorks() + { + var server = CreateServer( + new CertificateAuthenticationOptions + { + AllowedCertificateTypes = CertificateTypes.SelfSigned, + Events = sucessfulValidationEvents + }, + wireUpHeaderMiddleware: true, + headerName: "X-ARR-ClientCert"); + + var client = server.CreateClient(); + client.DefaultRequestHeaders.Add("X-ARR-ClientCert", Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData)); + var response = await client.GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + } + + [Fact] + public async Task VerifyACustomHeaderFailsIfTheHeaderIsNotPresent() + { + var server = CreateServer( + new CertificateAuthenticationOptions + { + Events = sucessfulValidationEvents + }, + wireUpHeaderMiddleware: true, + headerName: "X-ARR-ClientCert"); + + var client = server.CreateClient(); + client.DefaultRequestHeaders.Add("random-Weird-header", Convert.ToBase64String(Certificates.SelfSignedValidWithNoEku.RawData)); + var response = await client.GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode); + } + + [Fact] + public async Task VerifyNoEventWireupWithAValidCertificateCreatesADefaultUser() + { + var server = CreateServer( + new CertificateAuthenticationOptions + { + AllowedCertificateTypes = CertificateTypes.SelfSigned + }, + Certificates.SelfSignedValidWithNoEku); + + var response = await server.CreateClient().GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + XElement responseAsXml = null; + if (response.Content != null && + response.Content.Headers.ContentType != null && + response.Content.Headers.ContentType.MediaType == "text/xml") + { + var responseContent = await response.Content.ReadAsStringAsync(); + responseAsXml = XElement.Parse(responseContent); + } + + Assert.NotNull(responseAsXml); + + // There should always be an Issuer and a Thumbprint. + var actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == "issuer"); + Assert.Single(actual); + Assert.Equal(Certificates.SelfSignedValidWithNoEku.Issuer, actual.First().Value); + + actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Thumbprint); + Assert.Single(actual); + Assert.Equal(Certificates.SelfSignedValidWithNoEku.Thumbprint, actual.First().Value); + + // Now the optional ones + if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.SubjectName.Name)) + { + actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.X500DistinguishedName); + if (actual.Count() > 0) + { + Assert.Single(actual); + Assert.Equal(Certificates.SelfSignedValidWithNoEku.SubjectName.Name, actual.First().Value); + } + } + + if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.SerialNumber)) + { + actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.SerialNumber); + if (actual.Count() > 0) + { + Assert.Single(actual); + Assert.Equal(Certificates.SelfSignedValidWithNoEku.SerialNumber, actual.First().Value); + } + } + + if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.DnsName, false))) + { + actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Dns); + if (actual.Count() > 0) + { + Assert.Single(actual); + Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.DnsName, false), actual.First().Value); + } + } + + if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.EmailName, false))) + { + actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Email); + if (actual.Count() > 0) + { + Assert.Single(actual); + Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.EmailName, false), actual.First().Value); + } + } + + if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.SimpleName, false))) + { + actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Name); + if (actual.Count() > 0) + { + Assert.Single(actual); + Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.SimpleName, false), actual.First().Value); + } + } + + if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.UpnName, false))) + { + actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Upn); + if (actual.Count() > 0) + { + Assert.Single(actual); + Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.UpnName, false), actual.First().Value); + } + } + + if (!string.IsNullOrEmpty(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.UrlName, false))) + { + actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Uri); + if (actual.Count() > 0) + { + Assert.Single(actual); + Assert.Equal(Certificates.SelfSignedValidWithNoEku.GetNameInfo(X509NameType.UrlName, false), actual.First().Value); + } + } + } + + [Fact] + public async Task VerifyValidationEventPrincipalIsPropogated() + { + const string Expected = "John Doe"; + + var server = CreateServer( + new CertificateAuthenticationOptions + { + AllowedCertificateTypes = CertificateTypes.SelfSigned, + Events = new CertificateAuthenticationEvents + { + OnCertificateValidated = context => + { + // Make sure we get the validated principal + Assert.NotNull(context.Principal); + var claims = new[] + { + new Claim(ClaimTypes.Name, Expected, ClaimValueTypes.String, context.Options.ClaimsIssuer) + }; + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); + context.Success(); + return Task.CompletedTask; + } + } + }, + Certificates.SelfSignedValidWithNoEku); + + var response = await server.CreateClient().GetAsync("https://example.com/"); + Assert.Equal(HttpStatusCode.OK, response.StatusCode); + + XElement responseAsXml = null; + if (response.Content != null && + response.Content.Headers.ContentType != null && + response.Content.Headers.ContentType.MediaType == "text/xml") + { + var responseContent = await response.Content.ReadAsStringAsync(); + responseAsXml = XElement.Parse(responseContent); + } + + Assert.NotNull(responseAsXml); + var actual = responseAsXml.Elements("claim").Where(claim => claim.Attribute("Type").Value == ClaimTypes.Name); + Assert.Single(actual); + Assert.Equal(Expected, actual.First().Value); + Assert.Single(responseAsXml.Elements("claim")); + } + + private static TestServer CreateServer( + CertificateAuthenticationOptions configureOptions, + X509Certificate2 clientCertificate = null, + Func<HttpContext, bool> handler = null, + Uri baseAddress = null, + bool wireUpHeaderMiddleware = false, + string headerName = "") + { + var builder = new WebHostBuilder() + .Configure(app => + { + app.Use((context, next) => + { + if (clientCertificate != null) + { + context.Connection.ClientCertificate = clientCertificate; + } + return next(); + }); + + + if (wireUpHeaderMiddleware) + { + app.UseCertificateForwarding(); + } + + app.UseAuthentication(); + + app.Use(async (context, next) => + { + var request = context.Request; + var response = context.Response; + + var authenticationResult = await context.AuthenticateAsync(); + + if (authenticationResult.Succeeded) + { + response.StatusCode = (int)HttpStatusCode.OK; + response.ContentType = "text/xml"; + + await response.WriteAsync("<claims>"); + foreach (Claim claim in context.User.Claims) + { + await response.WriteAsync($"<claim Type=\"{claim.Type}\" Issuer=\"{claim.Issuer}\">{claim.Value}</claim>"); + } + await response.WriteAsync("</claims>"); + } + else + { + await context.ChallengeAsync(); + } + }); + }) + .ConfigureServices(services => + { + if (configureOptions != null) + { + services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(options => + { + options.AllowedCertificateTypes = configureOptions.AllowedCertificateTypes; + options.Events = configureOptions.Events; + options.ValidateCertificateUse = configureOptions.ValidateCertificateUse; + options.RevocationFlag = options.RevocationFlag; + options.RevocationMode = options.RevocationMode; + options.ValidateValidityPeriod = configureOptions.ValidateValidityPeriod; + }); + } + else + { + services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme).AddCertificate(); + } + + if (wireUpHeaderMiddleware && !string.IsNullOrEmpty(headerName)) + { + services.AddCertificateForwarding(options => + { + options.CertificateHeader = headerName; + }); + } + }); + + var server = new TestServer(builder) + { + BaseAddress = baseAddress + }; + + return server; + } + + private CertificateAuthenticationEvents sucessfulValidationEvents = new CertificateAuthenticationEvents() + { + OnCertificateValidated = context => + { + var claims = new[] + { + new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer), + new Claim(ClaimTypes.Name, context.ClientCertificate.Subject, ClaimValueTypes.String, context.Options.ClaimsIssuer) + }; + + context.Principal = new ClaimsPrincipal(new ClaimsIdentity(claims, context.Scheme.Name)); + context.Success(); + return Task.CompletedTask; + } + }; + + private CertificateAuthenticationEvents failedValidationEvents = new CertificateAuthenticationEvents() + { + OnCertificateValidated = context => + { + context.Fail("Not validated"); + return Task.CompletedTask; + } + }; + + private CertificateAuthenticationEvents unprocessedValidationEvents = new CertificateAuthenticationEvents() + { + OnCertificateValidated = context => + { + return Task.CompletedTask; + } + }; + + private static class Certificates + { + public static X509Certificate2 SelfSignedValidWithClientEku { get; private set; } = + new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedClientEkuCertificate.cer")); + + public static X509Certificate2 SelfSignedValidWithNoEku { get; private set; } = + new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedNoEkuCertificate.cer")); + + public static X509Certificate2 SelfSignedValidWithServerEku { get; private set; } = + new X509Certificate2(GetFullyQualifiedFilePath("validSelfSignedServerEkuCertificate.cer")); + + public static X509Certificate2 SelfSignedNotYetValid { get; private set; } = + new X509Certificate2(GetFullyQualifiedFilePath("selfSignedNoEkuCertificateNotValidYet.cer")); + + public static X509Certificate2 SelfSignedExpired { get; private set; } = + new X509Certificate2(GetFullyQualifiedFilePath("selfSignedNoEkuCertificateExpired.cer")); + + private static string GetFullyQualifiedFilePath(string filename) + { + var filePath = Path.Combine(AppContext.BaseDirectory, filename); + if (!File.Exists(filePath)) + { + throw new FileNotFoundException(filePath); + } + return filePath; + } + } + } +} diff --git a/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj b/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj index f575bfa0cf..fd033a9169 100644 --- a/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj +++ b/src/Security/Authentication/test/Microsoft.AspNetCore.Authentication.Test.csproj @@ -22,6 +22,10 @@ <Content Include="WsFederation\ValidToken.xml"> <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> </Content> + <Content Include="$(SharedSourceRoot)test\Certificates\*.cer"> + <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory> + <CopyToPublishDirectory>PreserveNewest</CopyToPublishDirectory> + </Content> </ItemGroup> <ItemGroup> @@ -34,6 +38,7 @@ </ItemGroup> <ItemGroup> + <Reference Include="Microsoft.AspNetCore.Authentication.Certificate" /> <Reference Include="Microsoft.AspNetCore.Authentication.Cookies" /> <Reference Include="Microsoft.AspNetCore.Authentication.Facebook" /> <Reference Include="Microsoft.AspNetCore.Authentication.Google" /> @@ -42,6 +47,7 @@ <Reference Include="Microsoft.AspNetCore.Authentication.OpenIdConnect" /> <Reference Include="Microsoft.AspNetCore.Authentication.Twitter" /> <Reference Include="Microsoft.AspNetCore.Authentication.WsFederation" /> + <Reference Include="Microsoft.AspNetCore.HttpOverrides" /> <Reference Include="Microsoft.AspNetCore.TestHost" /> </ItemGroup> diff --git a/src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateExpired.cer b/src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateExpired.cer Binary files differnew file mode 100644 index 0000000000..81b6326d6f --- /dev/null +++ b/src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateExpired.cer diff --git a/src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateNotValidYet.cer b/src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateNotValidYet.cer Binary files differnew file mode 100644 index 0000000000..9c8cf9d71b --- /dev/null +++ b/src/Security/Authentication/test/TestCertificates/selfSignedNoEkuCertificateNotValidYet.cer diff --git a/src/Security/Authentication/test/TestCertificates/validSelfSignedClientEkuCertificate.cer b/src/Security/Authentication/test/TestCertificates/validSelfSignedClientEkuCertificate.cer Binary files differnew file mode 100644 index 0000000000..db4bb5b90a --- /dev/null +++ b/src/Security/Authentication/test/TestCertificates/validSelfSignedClientEkuCertificate.cer diff --git a/src/Security/Authentication/test/TestCertificates/validSelfSignedNoEkuCertificate.cer b/src/Security/Authentication/test/TestCertificates/validSelfSignedNoEkuCertificate.cer Binary files differnew file mode 100644 index 0000000000..2be2a46d79 --- /dev/null +++ b/src/Security/Authentication/test/TestCertificates/validSelfSignedNoEkuCertificate.cer diff --git a/src/Security/Authentication/test/TestCertificates/validSelfSignedServerEkuCertificate.cer b/src/Security/Authentication/test/TestCertificates/validSelfSignedServerEkuCertificate.cer Binary files differnew file mode 100644 index 0000000000..823000d4a1 --- /dev/null +++ b/src/Security/Authentication/test/TestCertificates/validSelfSignedServerEkuCertificate.cer diff --git a/src/Security/Security.sln b/src/Security/Security.sln index 0e10f4be5e..1405fe94dd 100644 --- a/src/Security/Security.sln +++ b/src/Security/Security.sln @@ -153,6 +153,13 @@ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc", "..\Mvc\Mvc\src\Microsoft.AspNetCore.Mvc.csproj", "{27B5D7B5-75A6-4BE6-BD09-597044D06970}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Mvc.Core", "..\Mvc\Mvc.Core\src\Microsoft.AspNetCore.Mvc.Core.csproj", "{553F8C79-13AF-4993-99C1-D70F2143AD8E}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Certificate", "Certificate", "{4DF524BF-D9A9-46F2-882C-68C48FF5FF33}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.Authentication.Certificate", "Authentication\Certificate\src\Microsoft.AspNetCore.Authentication.Certificate.csproj", "{2B88E3EA-6FBE-4690-A56E-0744FFAC9870}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Certificate.Sample", "Authentication\Certificate\samples\Certificate.Sample\Certificate.Sample.csproj", "{11F3B44F-DE5F-42C4-8EC9-1AA51FB89158}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HeaderPropagation", "..\Middleware\HeaderPropagation\ref\Microsoft.AspNetCore.HeaderPropagation.csproj", "{9F9CBDD0-C8B3-4E79-B2B3-9ADE4AE08AEA}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -400,6 +407,18 @@ Global {553F8C79-13AF-4993-99C1-D70F2143AD8E}.Debug|Any CPU.Build.0 = Debug|Any CPU {553F8C79-13AF-4993-99C1-D70F2143AD8E}.Release|Any CPU.ActiveCfg = Release|Any CPU {553F8C79-13AF-4993-99C1-D70F2143AD8E}.Release|Any CPU.Build.0 = Release|Any CPU + {2B88E3EA-6FBE-4690-A56E-0744FFAC9870}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2B88E3EA-6FBE-4690-A56E-0744FFAC9870}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2B88E3EA-6FBE-4690-A56E-0744FFAC9870}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2B88E3EA-6FBE-4690-A56E-0744FFAC9870}.Release|Any CPU.Build.0 = Release|Any CPU + {11F3B44F-DE5F-42C4-8EC9-1AA51FB89158}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {11F3B44F-DE5F-42C4-8EC9-1AA51FB89158}.Debug|Any CPU.Build.0 = Debug|Any CPU + {11F3B44F-DE5F-42C4-8EC9-1AA51FB89158}.Release|Any CPU.ActiveCfg = Release|Any CPU + {11F3B44F-DE5F-42C4-8EC9-1AA51FB89158}.Release|Any CPU.Build.0 = Release|Any CPU + {9F9CBDD0-C8B3-4E79-B2B3-9ADE4AE08AEA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F9CBDD0-C8B3-4E79-B2B3-9ADE4AE08AEA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F9CBDD0-C8B3-4E79-B2B3-9ADE4AE08AEA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F9CBDD0-C8B3-4E79-B2B3-9ADE4AE08AEA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -476,6 +495,10 @@ Global {8771B5C8-4B96-4A40-A3FC-8CC7E16D7A82} = {A482E4FD-51C2-4061-8357-1E4757D6CF27} {27B5D7B5-75A6-4BE6-BD09-597044D06970} = {A3766414-EB5C-40F7-B031-121804ED5D0A} {553F8C79-13AF-4993-99C1-D70F2143AD8E} = {A3766414-EB5C-40F7-B031-121804ED5D0A} + {4DF524BF-D9A9-46F2-882C-68C48FF5FF33} = {79C549BA-2932-450A-B87D-635879361343} + {2B88E3EA-6FBE-4690-A56E-0744FFAC9870} = {4DF524BF-D9A9-46F2-882C-68C48FF5FF33} + {11F3B44F-DE5F-42C4-8EC9-1AA51FB89158} = {4DF524BF-D9A9-46F2-882C-68C48FF5FF33} + {9F9CBDD0-C8B3-4E79-B2B3-9ADE4AE08AEA} = {A3766414-EB5C-40F7-B031-121804ED5D0A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {ABF8089E-43D0-4010-84A7-7A9DCFE49357} diff --git a/src/Shared/test/Certificates/selfSignedNoEkuCertificateExpired.cer b/src/Shared/test/Certificates/selfSignedNoEkuCertificateExpired.cer Binary files differnew file mode 100644 index 0000000000..81b6326d6f --- /dev/null +++ b/src/Shared/test/Certificates/selfSignedNoEkuCertificateExpired.cer diff --git a/src/Shared/test/Certificates/selfSignedNoEkuCertificateNotValidYet.cer b/src/Shared/test/Certificates/selfSignedNoEkuCertificateNotValidYet.cer Binary files differnew file mode 100644 index 0000000000..9c8cf9d71b --- /dev/null +++ b/src/Shared/test/Certificates/selfSignedNoEkuCertificateNotValidYet.cer diff --git a/src/Shared/test/Certificates/validSelfSignedClientEkuCertificate.cer b/src/Shared/test/Certificates/validSelfSignedClientEkuCertificate.cer Binary files differnew file mode 100644 index 0000000000..db4bb5b90a --- /dev/null +++ b/src/Shared/test/Certificates/validSelfSignedClientEkuCertificate.cer diff --git a/src/Shared/test/Certificates/validSelfSignedNoEkuCertificate.cer b/src/Shared/test/Certificates/validSelfSignedNoEkuCertificate.cer Binary files differnew file mode 100644 index 0000000000..2be2a46d79 --- /dev/null +++ b/src/Shared/test/Certificates/validSelfSignedNoEkuCertificate.cer diff --git a/src/Shared/test/Certificates/validSelfSignedServerEkuCertificate.cer b/src/Shared/test/Certificates/validSelfSignedServerEkuCertificate.cer Binary files differnew file mode 100644 index 0000000000..823000d4a1 --- /dev/null +++ b/src/Shared/test/Certificates/validSelfSignedServerEkuCertificate.cer |