RO EN

Autentificare cu certificat X.509 în ASP.NET Core

Autentificare cu certificat X.509 în ASP.NET Core
Doru Bulubașa
22 April 2026

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 PeriodNotBefore / 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:

  1. Verifică că certificatul este semnat de un CA de încredere (chain validation)
  2. Verifică că certificatul nu a expirat
  3. Verifică că certificatul nu a fost revocat (CRL / OCSP — opțional)
  4. 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 obligatoriu
  • RequireCertificate — 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ă:

  1. Monitorizare expirare — alertă la 30 de zile înainte. Azure Key Vault și Application Insights pot face asta automat.
  2. Acceptă ambele certificate temporar — în perioada de tranziție, whitelist-ul conține atât thumbprint-ul vechi cât și cel nou.
  3. 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).
  4. 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 = Online activat (sau cel puțin Offline)
  • X509KeyStorageFlags.EphemeralKeySet pe 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 (nu All)
  • ✅ Logging pentru OnAuthenticationFailed trimis î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.