~~NOTOC~~
====== Scenario 9.1: mTLS Client Authentication ======
**Category:** [[.:start|Authentication]] \\
**Complexity:** **** (High) \\
**Prerequisites:** Client and server certificates \\
**Estimated Time:** 20-30 minutes
----
===== Description =====
This scenario describes **Mutual TLS (mTLS) authentication**, where both server and client authenticate each other using certificates.
**Advantages over password authentication:**
* **Stronger** - Cryptographically based
* **Automatable** - No user interaction required
* **Revocable** - Central revocation capability
* **Zero-Trust** - Every request is authenticated
----
===== Workflow =====
sequenceDiagram
participant Client
participant Server
Client->>Server: ClientHello
Server->>Client: ServerHello + ServerCert
Server->>Client: CertificateRequest
Client->>Server: ClientCert + CertificateVerify
Server->>Server: Validate client certificate
Server->>Client: Finished
Client->>Server: Finished
Note over Client,Server: mTLS connection established
----
===== Code Example: mTLS Server (ASP.NET Core) =====
using Microsoft.AspNetCore.Authentication.Certificate;
using Microsoft.AspNetCore.Server.Kestrel.Https;
var builder = WebApplication.CreateBuilder(args);
// Configure Kestrel for mTLS
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(https =>
{
// Load server certificate
https.ServerCertificate = new X509Certificate2(
"server.pfx", "ServerPassword!");
// Require client certificate
https.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
// Validate client certificate
https.ClientCertificateValidation = (cert, chain, errors) =>
{
// Custom validation
if (errors != SslPolicyErrors.None)
{
Console.WriteLine($"SSL Policy Errors: {errors}");
return false;
}
// Check against own CA
var trustedRoots = new X509Certificate2Collection();
trustedRoots.Add(new X509Certificate2("root-ca.crt"));
var customChain = new X509Chain();
customChain.ChainPolicy.CustomTrustStore.AddRange(trustedRoots);
customChain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
customChain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
return customChain.Build(cert);
};
});
});
// Add certificate authentication
builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.Chained;
options.RevocationMode = X509RevocationMode.Online;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
// Extract claims from certificate
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, context.ClientCertificate.Subject),
new Claim(ClaimTypes.Name, context.ClientCertificate.GetNameInfo(X509NameType.SimpleName, false)),
new Claim("certificate_thumbprint", context.ClientCertificate.Thumbprint)
};
context.Principal = new ClaimsPrincipal(
new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
Console.WriteLine($"Auth failed: {context.Exception.Message}");
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorization();
var app = builder.Build();
app.UseAuthentication();
app.UseAuthorization();
app.MapGet("/api/protected", [Authorize] (HttpContext ctx) =>
{
var subject = ctx.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
return Results.Ok(new { message = "Authenticated", subject });
});
app.Run();
----
===== Code Example: mTLS Client (HttpClient) =====
using WvdS.Security.Cryptography.X509Certificates.Extensions.PQ;
using var ctx = PqCryptoContext.Initialize();
// Load client certificate and key
var clientCert = ctx.LoadCertificateWithPrivateKey(
"client.crt.pem",
"client.key.pem",
"ClientPassword!"
);
// HttpClientHandler with client certificate
var handler = new HttpClientHandler
{
ClientCertificateOptions = ClientCertificateOption.Manual,
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) =>
{
// Validate server certificate
if (errors != SslPolicyErrors.None)
{
Console.WriteLine($"Server certificate error: {errors}");
// In production: return false;
}
return true;
}
};
// Add client certificate
handler.ClientCertificates.Add(clientCert);
using var httpClient = new HttpClient(handler);
// Send request
var response = await httpClient.GetAsync("https://api.example.com/api/protected");
if (response.IsSuccessStatusCode)
{
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Response: {content}");
}
else
{
Console.WriteLine($"Error: {response.StatusCode}");
}
----
===== Certificate-based Authorization =====
public class CertificateAuthorizationHandler : AuthorizationHandler
{
protected override Task HandleRequirementAsync(
AuthorizationHandlerContext context,
CertificateRequirement requirement)
{
// Get certificate from principal
var thumbprint = context.User.FindFirst("certificate_thumbprint")?.Value;
var subject = context.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
if (string.IsNullOrEmpty(thumbprint))
{
return Task.CompletedTask;
}
// Check if certificate is authorized
if (requirement.AllowedThumbprints.Contains(thumbprint) ||
requirement.AllowedSubjectPatterns.Any(p => Regex.IsMatch(subject, p)))
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
public class CertificateRequirement : IAuthorizationRequirement
{
public HashSet AllowedThumbprints { get; set; } = new();
public List AllowedSubjectPatterns { get; set; } = new();
}
// Registration
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("ServiceAccounts", policy =>
policy.Requirements.Add(new CertificateRequirement
{
AllowedSubjectPatterns = new List
{
@"^CN=.*\.svc\.cluster\.local$", // Kubernetes services
@"^CN=service-[a-z]+$" // Named services
}
}));
});
builder.Services.AddSingleton();
----
===== Nginx as mTLS Reverse Proxy =====
server {
listen 443 ssl;
server_name api.example.com;
# Server certificate
ssl_certificate /etc/nginx/ssl/server-chain.pem;
ssl_certificate_key /etc/nginx/ssl/server.key.pem;
# Client certificate authentication
ssl_client_certificate /etc/nginx/ssl/ca-chain.pem;
ssl_verify_client on;
ssl_verify_depth 2;
# TLS 1.3
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers on;
# OCSP Stapling
ssl_stapling on;
ssl_stapling_verify on;
location / {
# Pass client certificate info to backend
proxy_set_header X-Client-Cert $ssl_client_cert;
proxy_set_header X-Client-Verify $ssl_client_verify;
proxy_set_header X-Client-S-DN $ssl_client_s_dn;
proxy_pass http://backend:8080;
}
}
----
===== Industry-Specific mTLS Requirements =====
^ Industry ^ Certificate Lifetime ^ Revocation ^ Specifics ^
| **Kubernetes** | 1 year | CRL | SPIFFE/SPIRE |
| **Financial Sector** | 90 days | OCSP | PCI-DSS compliant |
| **Healthcare** | 1 year | CRL + OCSP | DiGAV-compliant |
| **IoT/SCADA** | 5 years | CRL | Offline capability |
----
===== Related Scenarios =====
^ Relationship ^ Scenario ^ Description ^
| **Prerequisite** | [[en:int:pqcrypt:szenarien:zertifikate:client_cert|3.2 Client Certificate]] | Create client cert |
| **Related** | [[.:smartcard_login|9.2 Smartcard Login]] | Hardware token |
| **Related** | [[en:int:pqcrypt:szenarien:tls:mtls_deployment|10.3 mTLS Deployment]] | TLS configuration |
----
<< [[.:start|<- Authentication Overview]] | [[en:int:pqcrypt:szenarien:start|^ Scenarios]] | [[.:smartcard_login|9.2 Smartcard Login ->]] >>
{{tag>scenario authentication mtls client-certificate zero-trust}}
----
//Wolfgang van der Stille @ EMSR DATA d.o.o. - Post-Quantum Cryptography Professional//