For one of our projects we require only one endpoint of a service to have client certificate validation. The code will be running on Kubernetes and I've passed the client certificate through to the pod in the form of a header. This seems to be received by the code without any problems.
Now, the client certificate needs to be in a specific chain of certificates (they're signed by the government and not trusted by default). My approach was setting a CustomTrustStore
with the entire chain of certificates. This does not seem to give me the result I was looking for.
So I've added the following:
services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "ssl-client-cert";
options.HeaderConverter = (headerValue) =>
{
X509Certificate2? clientCertificate = null;
if (!string.IsNullOrWhiteSpace(headerValue))
{
clientCertificate = X509Certificate2.CreateFromPem(WebUtility.UrlDecode(headerValue));
}
return clientCertificate!;
};
});
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.All;
options.ChainTrustValidationMode = X509ChainTrustMode.CustomRootTrust;
options.CustomTrustStore = getTrustStore(configData);
options.Events = new CertificateAuthenticationEvents
{
...
};
});
The forwarding seems to work, the breakpoint shows that clientCertificate
has a correct value.
The getTrustStore
method returns an X509Certificate2Collection
with all the certificates (simply loads a bunch of .CER files, puts them in an X509Certificate2
object and adds it to the collection):
- Root certificate (Government)
- Intermediate (Government)
- Intermediate (Reseller1)
- Intermediate (Reseller2)
- Intermediate (Reseller3)
- Intermediate (Government)
The client certificate needs to be purchased from one of the resellers and be part of that chain.
Local debugging makes the certificate check succeed (ends up in the OnCertificateValidated
event), even if the X509Certificate2Collection
is missing some certificates. Is it looking at my computer's store anyway? Deployed inside Kubernetes it seems to fail (ends up in the OnChallenge
event), no matter what. There's no additional logging either.
I tried debugging but I got stuck and have no idea where to look. The documentation on these methods is pretty minimalistic. All controller methods have the correct Authorization
attribute with the correct scheme provided.
CodePudding user response:
For best results, the root certificate should be the only one in CustomTrustStore
, the remainder should be in ExtraStore
. But, since no one understands that, we were nice and let the CustomRootTrust
mode just copy everything that's not self-signed over into ExtraStore. (A theoretical future mode of CustomAnchors
would tell the chain engine to stop at the first certificate it finds in CustomTrustStore... not the first one that's self-signed). Though I'm not sure how those X509ChainPolicy properties map to the ASP.NET versions, maybe you can't influence ExtraStore at that level.
Local debugging makes the certificate check succeed (ends up in the OnCertificateValidated event), even if the X509Certificate2Collection is missing some certificates. Is it looking at my computer's store anyway?
Yes. The chain engine will use your computer's cache of certificates (in the CurrentUser\CA, CurrentUser\My, LocalMachine\CA, and maybe LocalMachine\My stores... and possibly in the CU/LM Root and/or ThirdPartyRoot stores) in addition to the ones you specified programmatically for building the chain (it will also, unless disabled, attempt to download anything it can't find via a mechanism called "AIA fetching"). Since you're in a custom trust mode it will only call the chain trusted (and thus, successful) if it contains the root certificate that you specified in your custom collection.
CodePudding user response:
Turns out that the issue originated from the communication between Kubernetes (NGINX) and the code. The certificate was added as an HTTP header but not correctly processed.
Adding the following code to the Startup
fixed it:
services.Configure<ForwardedHeadersOptions>(options =>
{
options.ForwardedHeaders = ForwardedHeaders.XForwardedProto;
options.ForwardedProtoHeaderName = "X-Forwarded-Proto";
});