Using Chained Certificates for Certificate Authentication in ASP.NET Core 3.1

This article shows how to create self signed certificates and use these for chained certificate authentication in ASP.NET Core. By using chained certificates, each client application can use a unique certificate which was created from a root CA directly, or an intermediate certificate which was created from the root CA. The clients can then be grouped or authenticated as required.

Code https://github.com/damienbod/AspNetCoreCertificateAuth

Posts in this series

History

2020-02-19: Updated certificates, now using CertificateManager
2019-12-06: Updated Nuget packages, .NET Core 3.1
2019-09-06: Updated Nuget packages, .NET Core 3 preview 9

Creating the Certificates

Creating the certificates is the hardest part in setting up this flow. A self signed Root CA Certificate is created using the CertificateManager nuget package. When creating this, please use a strong password, replace the demo one, do not just copy the code.

var serviceProvider = new ServiceCollection()
	.AddCertificateManager()
	.BuildServiceProvider();

var createClientServerAuthCerts = 
	serviceProvider.GetService<CreateCertificatesClientServerAuth>();

var root = createClientServerAuthCerts.NewRootCertificate(
	new DistinguishedName { CommonName = "root_localhost",
		Country = "CH" },
	new ValidityPeriod { ValidFrom = DateTime.UtcNow, 
		ValidTo = DateTime.UtcNow.AddYears(10) },
	3, "localhost");
	
root.FriendlyName = "root_localhost certificate";

string password = "1234";
var importExportCertificate = serviceProvider.GetService<ImportExportCertificate>();

var rootCertInPfxBtyes = importExportCertificate.ExportRootPfx(password, root);
File.WriteAllBytes("root_localhost.pfx", rootCertInPfxBtyes);

Install the root certificate in the trusted root of the host windows PC. If deploying this on Linux, different tools need to be used.

https://social.msdn.microsoft.com/Forums/SqlServer/en-US/5ed119ef-1704-4be4-8a4f-ef11de7c8f34/a-certificate-chain-processed-but-terminated-in-a-root-certificate-which-is-not-trusted-by-the

A chained intermediate certificate can now be created from the root certificate. This is not required for all use cases, but you might need to create many certificates or need to activate, disable groups of certificates. The TextExtension parameter is required to set the pathlength in the basic constraints of the certificate.

The intermediate certificate can then be added to the trusted intermediate certificate in the windows host system.

var intermediate = createClientServerAuthCerts
		.NewIntermediateChainedCertificate(
		
	new DistinguishedName { CommonName = "intermediate_localhost",
		Country = "CH" },
		
	new ValidityPeriod { ValidFrom = DateTime.UtcNow, 
		ValidTo = DateTime.UtcNow.AddYears(10) },
	2, "localhost", root);
	
intermediate.FriendlyName = "intermediate_localhost certificate";

string password = "1234";
var importExportCertificate = serviceProvider.GetService<ImportExportCertificate>();

var intermediateCertInPfxBtyes = importExportCertificate.ExportChainedCertificatePfx(password, intermediate, root);
File.WriteAllBytes("intermediate_localhost.pfx", intermediateCertInPfxBtyes);

A child certificate can be created from the intermediate certificate. This is the end entity and does not need to create more child certificates.


var client = createClientServerAuthCerts.NewClientChainedCertificate(
	new DistinguishedName { CommonName = "client", Country = "CH" },
	new ValidityPeriod { ValidFrom = DateTime.UtcNow, ValidTo = DateTime.UtcNow.AddYears(10) },
	"localhost", intermediate);
client.FriendlyName = "client certificate";

string password = "1234";
var importExportCertificate = serviceProvider.GetService<ImportExportCertificate>();

var clientCertInPfxBtyes = importExportCertificate.ExportChainedCertificatePfx(password, client, intermediate);
File.WriteAllBytes("client.pfx", clientCertInPfxBtyes);

See the CertificateManager documentation for details in creating certificates.

https://github.com/damienbod/AspNetCoreCertificates

Server Setup

Now that the certificates are setup, the applications are created like in the previous blog. The AddAuthentication is configured to only accept CertificateTypes.Chained and the RevocationMode is set to NoCheck because we are using self signed chained certificates.

services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
	.AddCertificate(options => // code from ASP.NET Core sample
	{
		options.AllowedCertificateTypes = CertificateTypes.Chained;
		options.RevocationMode = X509RevocationMode.NoCheck;

		options.Events = new CertificateAuthenticationEvents
		{
			OnCertificateValidated = context =>
			{
				var validationService =
					context.HttpContext.RequestServices.GetService<MyCertificateValidationService>();

				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();
				}
				else
				{
					context.Fail("invalid cert");
				}

				return Task.CompletedTask;
			}
		};
	});

The application is configured in the program class to use the root certificate to validate the requests.

public static IWebHost BuildWebHost(string[] args)
	=> WebHost.CreateDefaultBuilder(args)
	.UseStartup<Startup>()
	.ConfigureKestrel(options =>
	{
		var cert = new X509Certificate2(Path.Combine("root_ca_dev_damienbod.pfx"), "1234");
		options.ConfigureHttpsDefaults(o =>
		{
			o.ServerCertificate = cert;
			o.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
		});
	})
	.Build();

The custom validation can then be added to the MyCertificateValidationService class. Here the client certificates are validated against the root certificate, or the intermediate certificate. This change be extended to use a dynamic list of Issuers and Subjects so that certificates can be activated or deactivated at runtime.

using System.Collections.Generic;
using System.IO;
using System.Security.Cryptography.X509Certificates;

namespace AspNetCoreCertificateAuthApi
{
    public class MyCertificateValidationService 
    {
        public bool ValidateCertificate(X509Certificate2 clientCertificate)
        {
            return CheckIfThumbprintIsValid(clientCertificate);
        }

        private bool CheckIfThumbprintIsValid(X509Certificate2 clientCertificate)
        {
            var listOfValidThumbprints = new List<string>
            {
                "CBF52D037D4CF0401F8EC8260C6382520D60EDD3",
                "BEE026E73A64D58943A66451D94389FA466169A4",
                "70D38240A71DD2882B4103E703F94D0B22285B0D",
                // valid but incorret DNS
                "ABF302B616CDEED10C53EA2C0E07CA1616814C68"
            };

            if (listOfValidThumbprints.Contains(clientCertificate.Thumbprint))
            {
                return true;
            }

            return false;
        }
    }
}

Client Code

The client application is then setup to send the client certificate. The server API is configured to use this to receive the certificates from the client. Now the chained certificates can be used to get access to the API.

private async Task<JsonDocument> CallApiClientIntermediateLocalhost()
{
	try
	{
		// This is a child created from the intermediate certificate which is a cert created from the root cert, must work
		//var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "client_intermediate_localhost.pfx"), "1234");
		var client = _clientFactory.CreateClient("client");

		var request = new HttpRequestMessage()
		{
			RequestUri = new Uri("https://localhost:44378/api/values"),
			Method = HttpMethod.Get,
		};

		var response = await client.SendAsync(request);

		if (response.IsSuccessStatusCode)
		{
			var responseContent = await response.Content.ReadAsStringAsync();
			var data = JsonDocument.Parse(responseContent);

			return data;
		}

		throw new ApplicationException($"Status code: {response.StatusCode}, Error: {response.ReasonPhrase}");
	}
	catch (Exception e)
	{
		throw new ApplicationException($"Exception {e}");
	}
}

In the Startup ConfigureServices method, add the handler:

var clientCertificateIntermediate = new X509Certificate2("../Certs/client.pfx", "1234");
var handlerClientCertificateIntermediate = new HttpClientHandler();
handlerClientCertificateIntermediate.ClientCertificates.Add(clientCertificateIntermediate);

services.AddHttpClient("client", c => {})
	.ConfigurePrimaryHttpMessageHandler(() => handlerClientCertificateIntermediate);

See the github code for the full working example. By using chained certificates, new certificates can be created on the fly for usage with new API clients, and the root certificate does not need to be deployed. This would become really useful when securing APIs which are not always connected to the internet, or with distributed devices.

Note In the examples, the passsword for the pfx certificate is entered directly in code. Never do this in a production application. Read the password from a configuration, environment, or a key vault. Also use a string password to create the pfx file.

Links

https://github.com/damienbod/AspNetCoreCertificates

https://docs.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-3.0

https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/working-with-certificates

https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/how-to-create-temporary-certificates-for-use-during-development

HowTo: Create Self-Signed Certificates with PowerShell

HTTPS and X509 certificates in .NET Part 2: creating self-signed certificates

https://www.humankode.com/asp-net-core/develop-locally-with-https-self-signed-certificates-and-asp-net-core

https://damienbod.com/2018/09/21/deploying-an-asp-net-core-application-to-windows-iis/

https://docs.microsoft.com/en-us/powershell/module/pkiclient/new-selfsignedcertificate?view=win10-ps

https://github.com/damienbod/IdentityServer4AspNetCoreIdentityTemplate#using-powershell-to-create-the-self-signed-certs

Using client certificates in .NET part 5: working with client certificates in a web project

https://stackoverflow.com/questions/42623080/how-to-validate-a-certificate-chain-from-a-specific-root-ca-in-c-sharp

https://docs.microsoft.com/en-us/aspnet/core/security/authentication/certauth?view=aspnetcore-3.0

https://social.msdn.microsoft.com/Forums/SqlServer/en-US/5ed119ef-1704-4be4-8a4f-ef11de7c8f34/a-certificate-chain-processed-but-terminated-in-a-root-certificate-which-is-not-trusted-by-the

https://tools.ietf.org/html/rfc3280.html

https://github.com/aspnet/AspNetCore/tree/master/src/Security/Authentication/Certificate/src

https://tools.ietf.org/html/rfc5246#section-7.4.4

4 comments

  1. […] Using Chained Certificates for Certificate Authentication in ASP.NET Core 3.0 […]

  2. […] Using Chained Certificates for Certificate Authentication in ASP.NET Core 3.0 | Software Engineering – Damien Bowden […]

  3. […] Using Chained Certificates for Certificate Authentication in ASP.NET Core 3.0 (Damien Bowden) […]

  4. […] Using Chained Certificates for Certificate Authentication in ASP.NET Core 3.0 […]

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: