Parolele sunt un risc. Token-urile pot fi furate. Certificatele client X.509 oferă un mecanism de autentificare bazat pe criptografie cu cheie publică — fără secrete partajate, fără parole care pot fi scurse, fără tokens care expiră inconvenient.
Este mecanismul preferat pentru comunicare machine-to-machine (M2M), API-uri interne, și scenarii în care vrei să elimini complet factorul uman din autentificare. În articolul următor vom vedea cum se extinde la mTLS — dar mai întâi să înțelegem fundamentele.
1. Ce este un certificat X.509?
Un certificat X.509 este un document digital standardizat care leagă o cheie publică de o identitate (persoană, server, aplicație). Certificatul este semnat de o Autoritate de Certificare (CA) — fie una publică (DigiCert, Let's Encrypt), fie una privată (CA intern al organizației tale).
Structura esențială a unui certificat:
- Subject — identitatea pentru care e emis (
CN=myservice, O=MyCompany) - Issuer — cine l-a semnat (CA-ul)
- Public Key — cheia publică asociată
- Validity Period —
NotBefore/NotAfter - Serial Number — identificator unic per CA
- Thumbprint — hash SHA-1 sau SHA-256 al întregului certificat (folosit pentru identificare rapidă)
- Extensions — Key Usage, Extended Key Usage (EKU), Subject Alternative Names (SAN)
Cum funcționează autentificarea
Clientul prezintă certificatul în timpul handshake-ului TLS. Serverul:
- Verifică că certificatul este semnat de un CA de încredere (chain validation)
- Verifică că certificatul nu a expirat
- Verifică că certificatul nu a fost revocat (CRL / OCSP — opțional)
- Extrage identitatea din certificate (
Subject,SAN, thumbprint) și o mapează la un utilizator/serviciu
Cheia privată nu părăsește niciodată clientul. Serverul verifică identitatea fără să o cunoască — asta e esența criptografiei cu cheie publică.
2. Tipuri de certificate și formate de fișiere
Înainte de orice cod, merită să cunoști formatele cu care vei lucra:
| Format | Extensie | Conținut | Folosit pentru |
|---|---|---|---|
| PEM | .pem, .crt, .cer |
Base64, text lizibil | Certificat public, CA chain |
| DER | .der, .cer |
Binar | Certificat public (Windows) |
| PFX / PKCS#12 | .pfx, .p12 |
Binar, parolat | Certificat + cheie privată împreună |
| PEM separat | .pem + .key |
Două fișiere text | Linux/macOS, OpenSSL, Nginx |
În lumea .NET vei lucra mai ales cu .pfx (conține tot) sau cu Certificate Store (Windows/macOS). Pe Linux în containere, cel mai des vei importa din fișiere PEM.
3. Creare certificate pentru development
Nu ai nevoie de o CA reală pentru a testa local. Creezi o CA auto-semnată și din ea emiți certificate client.
3.1 Creare CA privat (root)
# Generăm cheia privată a CA-ului
openssl genrsa -out ca.key 4096
# Creăm certificatul root al CA-ului (auto-semnat, valid 10 ani)
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
-subj "/CN=MyDev CA/O=MyCompany/C=RO"
3.2 Creare certificat client semnat de CA
# Generăm cheia privată a clientului
openssl genrsa -out client.key 2048
# Generăm CSR (Certificate Signing Request)
openssl req -new -key client.key -out client.csr \
-subj "/CN=my-service/O=MyCompany/C=RO"
# CA semnează certificatul clientului (valid 1 an)
openssl x509 -req -days 365 \
-in client.csr \
-CA ca.crt -CAkey ca.key -CAcreateserial \
-out client.crt
# Împachetăm certificatul + cheia privată într-un .pfx
openssl pkcs12 -export \
-in client.crt -inkey client.key \
-out client.pfx \
-passout pass:dev-password
3.3 Alternativă: .NET CLI cu dotnet dev-certs
Pentru scenarii simple de development, .NET oferă un shortcut:
# Generează certificat HTTPS dev (nu e ideal pentru client cert auth, dar merge pentru teste)
dotnet dev-certs https --export-path ./dev-cert.pfx --password dev-password --trust
4. Configurare server — Kestrel
ASP.NET Core cu Kestrel trebuie configurat explicit să ceară certificate client. Există trei moduri:
NoCertificate— nu cere deloc (implicit)AllowCertificate— acceptă dacă e furnizat, dar nu îl cere obligatoriuRequireCertificate— refuză conexiunea dacă nu există certificat
4.1 Configurare în Program.cs
builder.WebHost.ConfigureKestrel(options =>
{
options.ConfigureHttpsDefaults(https =>
{
// Cere certificat client — refuză dacă lipsește
https.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
// Validare custom: acceptăm certificate self-signed / CA intern
https.ClientCertificateValidation = (certificate, chain, errors) =>
{
// În producție validezi chain-ul complet
// În dev poți returna true pentru a accepta orice certificat
if (builder.Environment.IsDevelopment())
return true;
return errors == SslPolicyErrors.None;
};
});
});
4.2 Configurare în appsettings.json
{
"Kestrel": {
"Endpoints": {
"Https": {
"Url": "https://localhost:5001",
"Certificate": {
"Path": "server.pfx",
"Password": "server-password"
},
"ClientCertificateMode": "RequireCertificate"
}
}
}
}
Notă importantă: Dacă aplicația ta rulează în spatele unui reverse proxy (Nginx, Azure Application Gateway, IIS), certificatul client este terminat la proxy. Serverul ASP.NET Core nu îl vede direct — proxy-ul trebuie configurat să transmită certificatul prin header (ex:
X-ARR-ClientCert). Vom detalia asta în articolul despre mTLS.
5. Configurare autentificare în ASP.NET Core
5.1 Instalare pachet
dotnet add package Microsoft.AspNetCore.Authentication.Certificate
5.2 Configurare în Program.cs
builder.Services
.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
// Ce tipuri de certificate acceptăm
options.AllowedCertificateTypes = CertificateTypes.All;
// CertificateTypes.SelfSigned — doar self-signed
// CertificateTypes.Chained — doar certificate semnate de CA
// CertificateTypes.All — ambele
// Verificare revocație (recomandată în producție)
options.RevocationMode = X509RevocationMode.Online; // sau NoCheck în dev
// Custom validation — mapare certificat → ClaimsPrincipal
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var certificate = context.ClientCertificate;
// Extragem identitatea din Subject
var commonName = certificate.GetNameInfo(
X509NameType.SimpleName, forIssuer: false);
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, certificate.Thumbprint),
new Claim(ClaimTypes.Name, commonName ?? "unknown"),
new Claim("thumbprint", certificate.Thumbprint),
new Claim("cert-subject", certificate.Subject)
};
context.Principal = new ClaimsPrincipal(
new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
context.Fail("Certificate authentication failed: "
+ context.Exception.Message);
return Task.CompletedTask;
}
};
});
builder.Services.AddAuthorization();
// IMPORTANT: AddCertificateForwarding dacă ești în spatele unui proxy
// builder.Services.AddCertificateForwarding(options =>
// {
// options.CertificateHeader = "X-ARR-ClientCert";
// });
app.UseAuthentication();
app.UseAuthorization();
6. Validare avansată — whitelist și chain validation
În scenarii reale nu accepti orice certificat semnat de CA-ul tău — vrei să controlezi exact ce certificate sunt autorizate. Două strategii comune:
6.1 Whitelist prin Thumbprint
Cel mai simplu mecanism: menții o listă de thumbprint-uri cunoscute și refuzi orice altceva.
OnCertificateValidated = context =>
{
var allowedThumbprints = context.HttpContext
.RequestServices
.GetRequiredService<IConfiguration>()
.GetSection("Auth:AllowedCertificates")
.Get<string[]>() ?? [];
var thumbprint = context.ClientCertificate.Thumbprint;
if (!allowedThumbprints.Contains(thumbprint,
StringComparer.OrdinalIgnoreCase))
{
context.Fail($"Certificate thumbprint not in whitelist: {thumbprint}");
return Task.CompletedTask;
}
// Certificat valid — construim Principal
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, thumbprint),
new Claim(ClaimTypes.Name,
context.ClientCertificate.GetNameInfo(
X509NameType.SimpleName, false) ?? "")
};
context.Principal = new ClaimsPrincipal(
new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
return Task.CompletedTask;
}
// appsettings.json
{
"Auth": {
"AllowedCertificates": [
"A1B2C3D4E5F6...",
"1234567890AB..."
]
}
}
6.2 Validare prin CA intern
Mai scalabil: accepti orice certificat emis de CA-ul tău intern, fără să gestionezi o listă.
OnCertificateValidated = context =>
{
var certificate = context.ClientCertificate;
// Încarcă CA-ul de încredere
var trustedCa = new X509Certificate2("ca.crt");
var chain = new X509Chain();
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
chain.ChainPolicy.CustomTrustStore.Add(trustedCa);
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; // sau Online
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority;
var isValid = chain.Build(certificate);
if (!isValid)
{
var errors = string.Join(", ",
chain.ChainStatus.Select(s => s.StatusInformation));
context.Fail($"Chain validation failed: {errors}");
return Task.CompletedTask;
}
var claims = new[]
{
new Claim(ClaimTypes.NameIdentifier, certificate.Thumbprint),
new Claim(ClaimTypes.Name,
certificate.GetNameInfo(X509NameType.SimpleName, false) ?? "")
};
context.Principal = new ClaimsPrincipal(
new ClaimsIdentity(claims, context.Scheme.Name));
context.Success();
return Task.CompletedTask;
}
7. Client — cum trimiți certificatul dintr-un alt serviciu .NET
De obicei autentificarea cu certificate X.509 e folosită în comunicare M2M — un serviciu .NET apelează alt serviciu. Iată cum configurezi HttpClient să trimită certificatul:
// Varianta 1: din fișier .pfx
var certificate = new X509Certificate2(
"client.pfx",
"dev-password",
X509KeyStorageFlags.MachineKeySet);
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(certificate);
var client = new HttpClient(handler)
{
BaseAddress = new Uri("https://api.myservice.com")
};
// Varianta 2: din Certificate Store (Windows/macOS)
using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
var certificates = store.Certificates.Find(
X509FindType.FindByThumbprint,
"A1B2C3D4E5F6...",
validOnly: true);
if (certificates.Count == 0)
throw new InvalidOperationException("Client certificate not found in store.");
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(certificates[0]);
var client = new HttpClient(handler);
7.1 Integrare cu IHttpClientFactory (recomandat)
// Program.cs
builder.Services.AddHttpClient("SecureApiClient", client =>
{
client.BaseAddress = new Uri("https://api.myservice.com");
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
var cert = new X509Certificate2(
builder.Configuration["Certificates:ClientCertPath"]!,
builder.Configuration["Certificates:ClientCertPassword"]);
var handler = new HttpClientHandler();
handler.ClientCertificates.Add(cert);
return handler;
});
// Injectare și utilizare
public class MyService(IHttpClientFactory httpClientFactory)
{
public async Task<string> CallSecureApiAsync()
{
var client = httpClientFactory.CreateClient("SecureApiClient");
var response = await client.GetAsync("/api/data");
response.EnsureSuccessStatusCode();
return await response.Content.ReadAsStringAsync();
}
}
8. Certificate în Azure — scenarii de producție
8.1 Stocare în Azure Key Vault
În producție, nu stochezi niciodată fișiere .pfx pe disc sau în appsettings.json. Le pui în Azure Key Vault:
dotnet add package Azure.Security.KeyVault.Certificates
dotnet add package Azure.Identity
// Citire certificat din Key Vault la startup
var keyVaultUrl = builder.Configuration["KeyVault:Url"]!;
var credential = new DefaultAzureCredential();
var certClient = new CertificateClient(new Uri(keyVaultUrl), credential);
var secretClient = new SecretClient(new Uri(keyVaultUrl), credential);
// Key Vault stochează PFX-ul ca secret (base64)
var secret = await secretClient.GetSecretAsync("client-cert");
var pfxBytes = Convert.FromBase64String(secret.Value.Value);
var certificate = new X509Certificate2(pfxBytes, (string?)null,
X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet);
// Înregistrare în DI
builder.Services.AddSingleton(certificate);
8.2 Azure App Service — upload certificat
Pe Azure App Service poți încărca un certificat direct în portal (Certificates → Bring your own certificates) și accesezi cu thumbprint-ul:
// appsettings.json
{
"WEBSITE_LOAD_CERTIFICATES": "*" // sau thumbprint specific
}
// Citire din Certificate Store pe App Service
using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
var thumbprint = builder.Configuration["Certificates:ClientThumbprint"]!;
var certs = store.Certificates.Find(
X509FindType.FindByThumbprint, thumbprint, validOnly: false);
if (certs.Count == 0)
throw new InvalidOperationException(
$"Certificate {thumbprint} not found on App Service.");
9. Reînnoire certificate — fără downtime
Expirarea unui certificat care nu e gestionat proactiv poate opri complet un serviciu în producție. Strategia recomandată:
- Monitorizare expirare — alertă la 30 de zile înainte. Azure Key Vault și Application Insights pot face asta automat.
- Acceptă ambele certificate temporar — în perioada de tranziție, whitelist-ul conține atât thumbprint-ul vechi cât și cel nou.
- Deploy nou certificat — actualizezi secretul în Key Vault sau App Service, fără restart dacă citești certificatul la fiecare request (sau folosești o strategie de reload).
- Elimini certificatul vechi — după ce toate serviciile client au migrat.
// Strategie: reload certificat la fiecare request (costisitor, dar simplu)
// Mai bine: IOptionsMonitor<T> cu certificate reîncărcate din Key Vault periodic
public class CertificateProvider(IConfiguration config)
{
private X509Certificate2? _cached;
private DateTime _loadedAt;
public X509Certificate2 GetCertificate()
{
// Re-citim certificatul din Key Vault o dată pe oră
if (_cached != null &&
DateTime.UtcNow - _loadedAt < TimeSpan.FromHours(1))
{
return _cached;
}
// ... citire din Key Vault
_loadedAt = DateTime.UtcNow;
return _cached!;
}
}
10. Probleme comune și soluțiile lor
| Problemă | Cauza probabilă | Soluție |
|---|---|---|
403 deși certificatul e prezent |
Autentificarea a reușit, dar authorization a eșuat | Verifică că context.Success() e apelat și că ClaimsPrincipal e populat corect |
| Certificat ignorat în spatele unui proxy | TLS terminat la proxy, cererea ajunge HTTP la ASP.NET | Configurează AddCertificateForwarding cu header-ul proxy-ului |
CryptographicException la import PFX |
Flag-uri greșite pe Linux/container | Adaugă X509KeyStorageFlags.EphemeralKeySet pe Linux |
| Chain validation eșuează | CA-ul nu e în trusted store | Folosește CustomRootTrust și adaugă CA-ul manual în CustomTrustStore |
| Certificate expirat în producție | Lipsă monitorizare | Alertă în Azure Key Vault / Application Insights la 30+ zile înainte de expirare |
SslPolicyErrors.RemoteCertificateChainErrors |
Server dev cu self-signed cert | În dev: handler.ServerCertificateCustomValidationCallback = HttpClientHandler.DangerousAcceptAnyServerCertificateValidator — NICIODATĂ în producție |
11. Checklist de producție
- ✅ Certificate stocate în Azure Key Vault sau Certificate Store, nu pe disc
- ✅ Parola PFX în Key Vault Secret, nu în
appsettings.json - ✅
RevocationMode = Onlineactivat (sau cel puținOffline) - ✅
X509KeyStorageFlags.EphemeralKeySetpe Linux/containere - ✅ Alertă automată la expirare cu 30 de zile înainte
- ✅ Strategie de tranziție (whitelist cu două thumbprint-uri) la reînnoire
- ✅
AllowedCertificateTypes = CertificateTypes.Chainedîn producție (nuAll) - ✅ Logging pentru
OnAuthenticationFailedtrimis în Application Insights - ✅ Testat cu certificat expirat și cu certificat revocat
- ✅ Reverse proxy configurat să transmită certificatul client dacă TLS e terminat la proxy
Concluzie
Autentificarea cu certificate X.509 elimină clasele întregi de vulnerabilități asociate parolelor și token-urilor: nu există nimic de furat dintr-o bază de date, nu există secrete partajate, nu există replay attacks simple. Cheia privată rămâne întotdeauna la client.
Complexitatea reală nu e în codul de autentificare — care, după cum ai văzut, e relativ compact — ci în gestionarea ciclului de viață al certificatelor: emitere, distribuire, reînnoire și revocare. Investiția în automatizarea acestor procese se amortizează rapid.
În articolul următor facem pasul logic următor: mTLS (mutual TLS) — scenariul în care atât clientul cât și serverul se autentifică reciproc prin certificate, folosit extensiv în arhitecturi de microservicii și service mesh.