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

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 New-SelfSignedCertificate powershell cmdlet. When creating this, please use a strong password, replace the demo one, do not just copy the code. It is important to add the KeyUsageProperty parameter and the KeyUsage parameter as shown.

Powershell commands:

New-SelfSignedCertificate -DnsName "root_ca_dev_damienbod.com", "root_ca_dev_damienbod.com" 
   -CertStoreLocation "cert:\LocalMachine\My" 
   -NotAfter (Get-Date).AddYears(20) 
   -FriendlyName "root_ca_dev_damienbod.com" 
   -KeyUsageProperty All 
   -KeyUsage CertSign, CRLSign, DigitalSignature

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\root_ca_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath root_ca_dev_damienbod.crt

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 self signed 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.

Powershell commands:


$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint of the root..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my 
   -dnsname "intermediate_dev_damienbod.com" 
   -Signer $parentcert 
   -NotAfter (Get-Date).AddYears(20) 
   -FriendlyName "intermediate_dev_damienbod.com" 
   -KeyUsageProperty All 
   -KeyUsage CertSign, CRLSign, DigitalSignature
   -TextExtension @("2.5.29.19={text}CA=1&pathlength=1")

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\intermediate_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath intermediate_dev_damienbod.crt

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

Powershell commands:

$parentcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the Intermediate certificate..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my 
  -dnsname "child_a_dev_damienbod.com" 
  -Signer $parentcert 
  -NotAfter (Get-Date).AddYears(20) 
  -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt

A child certificate can also be created from the root certificate directly. If you do not have many API clients, this could be used.

$rootcert = ( Get-ChildItem -Path cert:\LocalMachine\My\"The thumbprint from the root cert..." )

New-SelfSignedCertificate -certstorelocation cert:\localmachine\my 
  -dnsname "child_a_dev_damienbod.com" 
  -Signer $rootcert 
  -NotAfter (Get-Date).AddYears(20) 
  -FriendlyName "child_a_dev_damienbod.com"

$mypwd = ConvertTo-SecureString -String "1234" -Force -AsPlainText

Get-ChildItem -Path cert:\localMachine\my\"The thumbprint..." | Export-PfxCertificate -FilePath C:\git\AspNetCoreCertificateAuth\Certs\child_a_dev_damienbod.pfx -Password $mypwd

Export-Certificate -Cert cert:\localMachine\my\"The thumbprint..." -FilePath child_a_dev_damienbod.crt

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.IO;
using System.Security.Cryptography.X509Certificates;

namespace AspNetCoreCertificateAuthApi
{
    public class MyCertificateValidationService
    {
        private readonly X509Certificate2 rootCertificate = new X509Certificate2(Path.Combine("root_ca_dev_damienbod.pfx"), "1234");
        private readonly X509Certificate2 intermediateCertificate = new X509Certificate2(Path.Combine("child_a_dev_damienbod.pfx"), "1234");

        public bool ValidateCertificate(X509Certificate2 clientCertificate)
        {
            if (clientCertificate.Issuer == rootCertificate.Issuer || 
                clientCertificate.Issuer == intermediateCertificate.Subject)
            {
                return true;
            }

            return false;
        }
    }
}

Client Code

The client application is then setup to send the client certificate in the X-ARR-ClientCert request header. 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<JArray> GetApiDataAsyncChained()
{
	try
	{
		// This is a child created from the root cert, must work
		//var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "child_a_dev_damienbod.pfx"), "1234");

		// 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, "child_b_from_a_dev_damienbod.pfx"), "1234");

		// This is a NOT child of the root cert or the intermediate certificate
		// , must fail
		//var cert = new X509Certificate2(Path.Combine(_environment.ContentRootPath, "sts_dev_cert.pfx"), "1234");

		var client = _clientFactory.CreateClient();

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

		request.Headers.Add("X-ARR-ClientCert", cert.GetRawCertDataString());
		var response = await client.SendAsync(request);

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

			return data;
		}

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

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.

Links

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 to Certificate Authentication in ASP.NET Core 3.0 | Software Engineering Cancel 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 )

Google photo

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

Twitter picture

You are commenting using your Twitter 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: