Setting up mTLS and Kestrel (cont.)
Tags: csharp, netcore, tutorial, security
Published: 2020-08-17 12:18:18

---
title: Setting up mTLS and Kestrel (cont.)
published: true
date: 2020-08-17 00:00:00 UTC
tags: csharp,netcore,tutorial,security
canonical_url: https://blog.kritner.com/2020/07/22/setting-up-mtls-and-kestrel-cont/
cover_image: https://blog.kritner.com/2020/07/22/setting-up-mtls-and-kestrel-cont/cover.jpg
---

In the [last post](https://blog.kritner.com/2020/07/15/setting-up-mtls-and-kestrel/) we started talking about mTLS. In the post I pointed out that the client cert’s signing CA was not verified, let’s fix that!

## The problem

The main thing accomplished in the [previous post](https://blog.kritner.com/2020/07/15/setting-up-mtls-and-kestrel/) was getting a CA up and running with a client certificate signed by that CA. We updated our web api to require client certificates, and successfully connected to the application using our new client certificate.

The flaw in the previous post was though we were validating a client certificate was present, we weren’t actually validating that the client certificate was signed by the CA we created. If the client certificate were valid for some other reason (like if it were signed by another trusted root CA on the system, like an actual internet CA) it would still “get in”.

## Steps to Remedy

- Set up for depicting the problem
- Demonstrate the problem
- Solve the problem with code

### Set up for depicting the problem

So first things first, we need to show that this is in fact a problem. I’ve already described the problem, but how can we go about proving it?

Easiest thing IMO, is to just create another CA and client cert with the new CA. The web api should accept both the original “client” cert, as well as the “badClient” cert.

I’m going to do all the steps from the previous post for setting up the CA, including trusting our “badCa”. The reason this is being done has already been covered above, but just to reiterate, we want to show that _any_ valid cert is currently getting in; where we want _only valid certs from our intended CA_ to get in.

So now we should have the following:

- A “good” CA - the one we want to ensure checked out client certs
- A “good” client cert - the one that should be accepted via our app
- A “bad” CA - a valid CA, but not one that should be allowed to sign certificates that can get into our app
- A “bad” client cert - signed by the “bad” ca, should _not_ (but currently will be able to) get into our app

It’ll look something like this if you’re inspecting the certificates:

![So many certs](https://blog.kritner.com/2020/07/22/setting-up-mtls-and-kestrel-cont/certs.JPG)

### Demonstrate the problem

Now all there is to do is hit our web api with both the “good” cert and the “bad” cert, and confirm we are in fact able to get output from both.

Just like previously, let’s make sure the web api is running with a `dotnet run` from the “Kritner.Mtls” project.

Then hit the application first w/o a cert:

```cmd
curl --insecure -v https://localhost:5001/weatherForecast
```

![No cert](https://blog.kritner.com/2020/07/22/setting-up-mtls-and-kestrel-cont/preChangeNoCert.JPG)

With the “good” cert:

```cmd
curl --insecure -v --key client.key --cert client.crt https://localhost:5001/weatherForecast
```

![Good cert](https://blog.kritner.com/2020/07/22/setting-up-mtls-and-kestrel-cont/preChangeGoodCert.JPG)

With the “bad” cert:

```cmd
curl --insecure -v --key badClient.key --cert badClient.crt https://localhost:5001/weatherForecast
```

![Good cert](https://blog.kritner.com/2020/07/22/setting-up-mtls-and-kestrel-cont/preChangeBadCert.JPG)

* * *

You can see, as expected, the request w/o a client cert is rejected, and the requests with both the “good” and “bad” client certs get through. Now, we need to figure out how to go about restricting the app to only accept client certificates signed by our “good” CA

### Solve the problem with code

So first things first, we need to identify something about what makes “good” client certificate “good”, and what makes “bad” client certificate “bad”. If you inspect the certificates on your system, you’ll see there is an “Authority Key Identifier” as an attribute. This “Authority Key Identifier” on the client certificate matches the [“Authority Key Identifier”](https://knowledge.digicert.com/solution/SO18140#AKI) and/or [“Subject Key Identifier”](https://knowledge.digicert.com/solution/SO18140#SKI) on the CA that signed the certificate:

![Authority Key Identifier](https://blog.kritner.com/2020/07/22/setting-up-mtls-and-kestrel-cont/authorityKeyIdentifier.JPG)

Apologies about all different CA labels and whatnot if you’ve noticed them in the screenshots, I’m switching around computers like a madlad!

In the above, you’ll be able to see that the “good” cert “belongs” to the “good” CA, and the “bad” cert “belongs” to the “bad” CA - this is the information we need! Now we just need a way to get to the information in code.

#### Introduce an additional service to validate the CA

Note the starting point of the code I’m working with is [https://github.com/Kritner-Blogs/Kritner.Mtls/releases/tag/v0.9.1](https://github.com/Kritner-Blogs/Kritner.Mtls/releases/tag/v0.9.1)

Let’s review the current code within `Startup` I even left a little note for myself and others from the last post:

##### Implementation Stub

```cs
OnCertificateValidated = context =>
{
    var logger = context.HttpContext.RequestServices.GetService<ILogger<Startup>>();

    // You should implement a service that confirms the certificate passed in
    // was signed by the root CA.
    
    // Otherwise, a certificate that is valid to one of the other trusted CAs on the webserver,
    // would be valid in this case as well.
    
    logger.LogInformation("You did it my dudes!");

    return Task.CompletedTask;
} 
```

Let’s introduce a service into this section of code, its abstraction will look like this:

```cs
public interface ICertificateAuthorityValidator
{
    bool IsValid(X509Certificate2 clientCert);
}
```

In the above abstraction, we’re taking in a client certificate, and returning whether or not it’s valid (obviously). the `context` within `OnCertificateValidated` has access to the `ClientCertificate` and it’s already in the form of `X509Certificate2`.

Let’s stub out our implementation:

```cs
public class CertificateAuthorityValidator : ICertificateAuthorityValidator
{
    private readonly ILogger<CertificateAuthorityValidator> _logger;

    public CertificateAuthorityValidator(ILogger<CertificateAuthorityValidator> logger)
    {
        _logger = logger;
    }

    public bool IsValid(X509Certificate2 clientCert)
    {
        _logger.LogInformation($"Validating certificate within the {nameof(CertificateAuthorityValidator)}");
        return true;
    }
}
```

The above is obviously just a “starting point”, where we’re always saying its valid. We’ll wire it up by registering it as a service, and plugging it into our `OnCertificateValidated`. The writing up of the services I’ve covered several times in other posts but if you need help, take a look at the finished code (TODO put a link here… if I miss this on my review, there’ll probably be a link at the bottom).

```cs
OnCertificateValidated = context =>
{
    var logger = context.HttpContext.RequestServices.GetService<ILogger<Startup>>();
    logger.LogInformation("Within the OnCertificateValidated portion of Startup");

    var caValidator = context.HttpContext.RequestServices.GetService<ICertificateAuthorityValidator>();
    if (!caValidator.IsValid(context.ClientCertificate))
    {
        const string failValidationMsg = "The client certificate failed to validate";
        logger.LogWarning(failValidationMsg);
        context.Fail(failValidationMsg);
    }

    return Task.CompletedTask;
}
```

Above, we’re getting an instance of our `ICertificateAuthorityValidator` once the client certificate is (otherwise) validated, then running out additional validation procedure on it. If the validation fails, it will mark the validation as failure, otherwise it will still be successful.

With our stubbed implementation returning `true` from `IsValid`, let’s see what that looks like:

![Success with our IsValid stub always returning true](https://blog.kritner.com/2020/07/22/setting-up-mtls-and-kestrel-cont/implStubTrue.JPG)

Changing the stubbed implementation to return `false` from `IsValid`:

![Failure with our IsValid stub always returning false](https://blog.kritner.com/2020/07/22/setting-up-mtls-and-kestrel-cont/implStubFalse.JPG)

##### Actual Implementation

Now we can work on our actual implementation of the `CertificateAuthorityValidator`. You’ll recall that we can (hopefully) rely on the “Authority Key Identifier” to ensure only our intended CA’s signed certificates can make it through validation.

Shall we do some debugging?

![Authority Key Identifier within clientCert](https://blog.kritner.com/2020/07/22/setting-up-mtls-and-kestrel-cont/authorityKeyIdentifierDebug.JPG)

The screenshot above shows that the information we need is in fact present in the data presented to us from the `X509Certificate2`. To save a bit of time and writing, know that the “raw data” on this extension does represent the same value on the CA cert, but there’s a few additional bytes of information, namely “KeyID=” (as seen in the screenshots earlier). I could not actually get this data from the bytes to confirm (tried getting the byte string as ascii, utf8, and several others), but that’s what it seemed to be. This means for our implementation, we need the “raw data” from this extension, minus a few of the first bytes to account for what I can only assume is “KeyID=”.

The full `CertificateAuthorityValidator`:

```cs
public class CertificateAuthorityValidator : ICertificateAuthorityValidator
{
    private readonly ILogger<CertificateAuthorityValidator> _logger;

    // this should probably be injected via config or loaded from the cert
    // Apparently the bytes are in the reverse order when using this BigInteger parse method,
    // hence the reverse
    private readonly byte[] _caCertSubjectKeyIdentifier = BigInteger.Parse(
        "e9be86f64eb53bc12c1b5fe0f63df450274811da",
        System.Globalization.NumberStyles.HexNumber
    ).ToByteArray().Reverse().ToArray();

    private const string AuthorityKeyIdentifier = "Authority Key Identifier";
    
    public CertificateAuthorityValidator(ILogger<CertificateAuthorityValidator> logger)
    {
        _logger = logger;
    }

    public bool IsValid(X509Certificate2 clientCert)
    {
        _logger.LogInformation($"Validating certificate within the {nameof(CertificateAuthorityValidator)}");
        
        if (clientCert == null)
            return false;
        foreach (var extension in clientCert.Extensions)
        {
            if (extension.Oid.FriendlyName.Equals(AuthorityKeyIdentifier, StringComparison.OrdinalIgnoreCase))
            {
                try
                {
                    var authorityKeyIdentifier = new byte[_caCertSubjectKeyIdentifier.Length];
                    // Copy from the extension raw data, starting at the index that should be after the "KeyID=" bytes
                    Array.Copy(
                        extension.RawData, extension.RawData.Length - _caCertSubjectKeyIdentifier.Length, 
                        authorityKeyIdentifier, 0, 
                        authorityKeyIdentifier.Length);

                    if (_caCertSubjectKeyIdentifier.SequenceEqual(authorityKeyIdentifier))
                    {
                        _logger.LogInformation("Successfully validated the certificate came from the intended CA.");
                        return true;
                    }
                    else
                    {
                        _logger.LogError($"Client cert with subject '{clientCert.Subject}' not signed by our CA.");
                        return false;
                    }
                }
                catch (Exception e)
                {
                    _logger.LogError(e, string.Empty);
                    return false;
                }
            }
        }
        
        _logger.LogError($"'{clientCert.Subject}' did not contain the extension to check for CA validity.");
        return false;
    }
}
```

![Good cert test](https://blog.kritner.com/2020/07/22/setting-up-mtls-and-kestrel-cont/fixGoodCert.JPG)

```cmd
curl --insecure -v --key badClient.key --cert badClient.crt https://localhost:5001/weatherForecast
```

![Bad cert test](https://blog.kritner.com/2020/07/22/setting-up-mtls-and-kestrel-cont/fixBadCert.JPG)

```cmd
curl --insecure -v --key badClient.key --cert badClient.crt https://localhost:5001/weatherForecast
```

![It works!](https://media.giphy.com/media/26u4lOMA8JKSnL9Uk/giphy.gif)

## References

- [Setting up mTLS and Kestrel](https://blog.kritner.com/2020/07/15/setting-up-mtls-and-kestrel/)
- [What extensions and details are included in a SSL certificate?](https://knowledge.digicert.com/solution/SO18140#AKI)
- [Starting point of code at beginning of post](https://github.com/Kritner-Blogs/Kritner.Mtls/releases/tag/v.0.9.1)
- [Ending point of code](https://github.com/Kritner-Blogs/Kritner.Mtls/releases/tag/v1.0)
- Photo by [blackwood\_castle](https://unsplash.com/photos/tqxzgJlDbZg) on [Unsplash](https://unsplash.com/).