Setting up mTLS and Kestrel (cont.)
In the last post 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 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:
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:
1 |
|
With the “good” cert:
1 |
|
With the “bad” cert:
1 |
|
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” and/or “Subject Key Identifier” on the CA that signed the certificate:
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
Let’s review the current code within Startup
I even left a little note for myself and others from the last post:
Implementation Stub
1 |
|
Let’s introduce a service into this section of code, its abstraction will look like this:
1 |
|
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:
1 |
|
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).
1 |
|
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:
Changing the stubbed implementation to return false
from IsValid
:
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?
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
:
1 |
|
Should be relatively self explanatory what’s going on in the above, but here’s the breakdown:
- Setting a variable as the CA certificates “Subject Key Identifier” (SKI), using the hex string from the images near the beginning of the post. (I pointed it out in the comments in code but note that the byte array is reversed from how we want it, so we reversed the bytes to get what we needed)
- Enumerate through the
clientCert.Extensions
until finding the one with a “Friendly Name” of “Authority Key Identifier” - When found, drop the “KeyID=” (assumed) bytes by doing an array copy starting at the full raw bytes length minus the length of the SKI
- Compare the two sequences
- Return true if they match, false otherwise
Testing it out
It’s now time to check our work! Run the app if it’s not already running, and let’s check if we can get information using our good certificate:
1 |
|
1 |
|
References
Setting up mTLS and Kestrel (cont.)
https://blog.kritner.com/2020/07/22/setting-up-mtls-and-kestrel-cont/