Description
Is there an existing issue for this?
- I have searched the existing issues
Describe the bug
🔐 Certificate Structure:
I’m using a custom certificate chain with the following hierarchy:
Root CA
└── Intermediate CA
└── End-Entity (Leaf) Certificate
The Root CA signs the Intermediate CA.
The Intermediate CA signs the End-Entity (client/leaf) certificate.
The server trusts only the Root CA. The Intermediate CA is not installed on the server's trusted certificate store.
The client certificate includes both the Intermediate and End-Entity certificates in a full chain (e.g., PFX or PEM bundle).
Problem:
When the client sends the certificate with both the Intermediate and End-Entity certificates:
The ASP.NET Certificate Authentication Middleware fails with this error:
PartialChain: unable to get local issuer certificate
The middleware is unable to build a full trust chain, even though the intermediate is present in the client certificate chain.
If only the Intermediate certificate is sent by the client, the request is correctly success
Expected Behavior:
The middleware should be able to build the chain from the leaf through the intermediate up to the trusted root, using the intermediate provided by the client — as most TLS clients (e.g., OpenSSL, browsers) do.
Server side code
public static AuthenticationBuilder AddSaleSwiftCertificate(this AuthenticationBuilder builder)
{
return builder.AddCertificate(options =>
{
X509Certificate2 rootCertificate = GetRootCertificate();
options.RevocationMode = X509RevocationMode.NoCheck;
options.ChainTrustValidationMode = X509ChainTrustMode.CustomRootTrust;
options.CustomTrustStore = [rootCertificate];
options.Events = new CertificateAuthenticationEvents()
{
OnCertificateValidated = context =>
{
IEnumerable<Claim> claims = ParseSaleSwiftCertificateToClaims(context);
ClaimsIdentity claimsIdentity = new(claims, context.Scheme.Name);
if (context.Principal == null)
{
context.Principal = new ClaimsPrincipal(claimsIdentity);
}
else
{
context.Principal.AddIdentity(claimsIdentity);
}
context.Success();
return Task.CompletedTask;
}
};
})
.AddCertificateCache(options =>
{
options.CacheSize = 1024;
options.CacheEntryExpiration = TimeSpan.FromMinutes(2);
});
}
Client side code (HttpClient that communicate with the api)
services.AddHttpClient<PossHttpClient>(httpClient =>
{
httpClient.BaseAddress = new Uri(configuration.GetConnectionString("PossApi")!);
}).ConfigurePrimaryHttpMessageHandler((serviceProvider) =>
{
HttpClientHandler httpClientHandler = new();
httpClientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
PosServerCertificate posServerCertificate = serviceProvider.GetRequiredService<PosServerCertificate>();
httpClientHandler.ClientCertificates.Add(posServerCertificate.Value);
return httpClientHandler;
});
Question:
Is this behavior expected in ASP.NET Core?
Is it correct that the server trusts only the root CA, while the client provides both the intermediate and leaf certificates in its chain?
Or am I missing a required step (e.g., should the server also know about the intermediate)?
Certificates are added to files
PosServer is the leaf
PosCa is the intermidate
SaleSwiftCA is the root
All passwords 123456
Expected Behavior
No response
Steps To Reproduce
No response
Exceptions (if any)
No response
.NET Version
9
Anything else?
No response