X.509 ve PKI Geliştirici Gözüyle: Sertifika Yükleme, Chain Doğrulama ve ASP.NET Core'da mTLS
Bir sertifikaya güvenmek için zinciri kendi trust anchor'ına kadar doğrulamak, EKU'yu ve host adını ayrıca kontrol etmek gerekir; chain geçerli olsa bile ad uyuşmazlığını X509Chain değil MatchesHostname yakalar. X509CertificateLoader, .NET 10'un SHA-256 thumbprint araması ve PKCS#12 algoritma seçimi, CustomRootTrust ile doğrulama ve Bookstore servisleri arasında SAN tabanlı mTLS'i gerçek çıktılar ve testlerle kuruyoruz.
Sürüm notu: Bu yazıdaki kodlar .NET 10, ASP.NET Core 10, Microsoft.AspNetCore.Authentication.Certificate 10.0.15 ve xUnit v3 4.0 ile derlenip denenmiştir; çıktılar Ubuntu 24.04 (OpenSSL 3.0) üzerinde alınmıştır.
Önce Tahmin Et
Bookstore'da sipariş akışı iki servise bölündü: Ordering siparişi alıyor, stok rezervasyonu için Bookstore.Api'nin iç endpoint'ini TLS (Transport Layer Security; HTTPS'in altındaki şifreli bağlantı protokolü, eski sürümlerinin adı SSL (Secure Sockets Layer; TLS'in öncülü)) üzerinden çağırıyor. İç ağda olduğu için ilk sürümde istemci tarafında sunucu sertifikası kontrolü kapatılmış, sunucu tarafında da gelen client certificate'ın yalnız "bir sertifika var mı" kontrolü yapılıyor. Testler yeşil, istekler 201 dönüyor.
Bu kurulumda neyi kanıtlamış olduk? Hiçbir şeyi. Sertifika bir public key'in kime ait olduğunu söyleyen imzalı bir belge; o imzayı kimin attığını, belgenin hangi iş için verildiğini, süresini ve adını kontrol etmezsen elinde yalnız "birisi" ile şifreli bir bağlantı var. Bir önceki yazıda public key ile imza doğrulamayı gördük. Bu yazıda o public key'lerin sahibini kanıtlayan yapıyı kodla kuruyoruz: X.509 (sertifikaların standart formatı) sertifikasının içi, chain doğrulaması, doğrulama callback'lerinin tuzakları ve Bookstore'un servisleri arasında mTLS (mutual TLS; iki tarafın da sertifikayla kimliğini kanıtladığı TLS).
Sertifikanın operasyon tarafı, yani sertifikayı almak, yenilemek ve izlemek, SSL/TLS Certificate'ları ve HTTPS serisinin konusu; o serinin mTLS yazısı Kestrel ve Nginx kurulumunu da ayrıntılı anlatıyor. Burada geliştiricinin yazdığı doğrulama koduna odaklanıyoruz.
Sertifikanın İçinde Ne Var?
Örnekler için kendi küçük PKI'mızı (Public Key Infrastructure; sertifikaları üreten ve onlara güveni tanımlayan yapı) kuruyoruz: bir root CA (Certificate Authority; sertifikaları imzalayıp güvenilirliğini üstlenen kuruluş), onun imzaladığı bir intermediate CA ve intermediate'ın imzaladığı servis sertifikaları. Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı); kod Bookstore.Crypto library'sinin Pki klasöründe. Servis sertifikası şöyle üretiliyor:
public X509Certificate2 Issue(string commonName, string[] dnsNames, Oid eku, DateTimeOffset notBefore, DateTimeOffset notAfter)
{
using ECDsa key = ECDsa.Create(ECCurve.NamedCurves.nistP256);
CertificateRequest request = new($"CN={commonName}, O=Bookstore", key, HashAlgorithmName.SHA256);
request.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true));
request.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, true));
request.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension([eku], false));
SubjectAlternativeNameBuilder san = new();
foreach (string dnsName in dnsNames)
{
san.AddDnsName(dnsName);
}
request.CertificateExtensions.Add(san.Build());
request.CertificateExtensions.Add(new X509SubjectKeyIdentifierExtension(request.PublicKey, false));
request.CertificateExtensions.Add(X509AuthorityKeyIdentifierExtension.CreateFromCertificate(Intermediate, true, false));
using X509Certificate2 certificate = request.Create(Intermediate, notBefore, notAfter, NewSerialNumber());
return certificate.CopyWithPrivateKey(key);
}
pki-init komutu sertifikaları ve anahtarları git'e girmeyen certs/ klasörüne yazıyor; cert-info da API'nin sertifikasını okuyor:
$ dotnet run --project src/Bookstore.CryptoLab -- cert-info
Subject : CN=api.bookstore.test, O=Bookstore
Issuer : CN=Bookstore Dev Services CA, O=Bookstore
Serial : 4916A2CE...
Valid : 2026-11-14 02:53 .. 2026-12-14 02:58 UTC
Key : ECC 256 bits, signed with sha256ECDSA
X509v3 Basic Constraints (critical) CA=False
X509v3 Key Usage (critical) DigitalSignature
X509v3 Extended Key Usage TLS Web Server Authentication
X509v3 Subject Alternative Name DNS: api.bookstore.test, localhost
X509v3 Subject Key Identifier 041448D5B8C99022...
X509v3 Authority Key Identifier 30168014DF1D9805...
Thumbprint SHA-1 : D467920CC13B318618196AD219F78A2E7837EAD8
Thumbprint SHA-256: 3161218CF3954831F86549CBE8CAF214F5B3091F087D0A978EC5331F9AC5532B
api.crt.pem holds 2 certificates; FindByThumbprint(SHA256) -> 1 match
Doğrulama kodunu yazarken bakacağın alanlar bunlar. Basic Constraints sertifikanın CA olup olamayacağını söylüyor; servis sertifikası başka sertifika imzalayamamalı. Key Usage anahtarın teknik olarak ne yapabileceğini (burada imza), EKU (Extended Key Usage; sertifikanın hangi iş için verildiği) ise hangi rolde kullanılabileceğini söylüyor: sunucu kimliği mi, istemci kimliği mi. SAN (Subject Alternative Name; sertifikanın geçerli olduğu adlar) host adı kontrolünün baktığı yer; CN (Common Name; sertifika sahibinin adı) uzun zamandır bu iş için kullanılmıyor. Subject ve Authority Key Identifier ise chain kurarken "bu sertifikayı hangi anahtar imzaladı" sorusunu hızlandırıyor.
Thumbprint satırları .NET 10'un bir eklemesini gösteriyor. Thumbprint property'si SHA (Secure Hash Algorithm; standart hash algoritmalarının ortak adı) ailesinden SHA-1 kullanıyor; X509Certificate2Collection.FindByThumbprint(HashAlgorithmName, string) ise .NET 10 ile geldi ve SHA-256 thumbprint ile arama yapabiliyor. Eski Find(X509FindType.FindByThumbprint, ...) yalnız SHA-1'e bakıyor. Sertifikayı config'de thumbprint ile işaret ediyorsan yeni yazdığın kodda SHA-256 kullan.
Sertifika Yüklemek: X509CertificateLoader ve PEM
.NET 9 ile sertifika yüklemenin önerilen yolu X509CertificateLoader oldu. Eski constructor'lar artık uyarı veriyor:
$ dotnet build
Program.cs(4,30): warning SYSLIB0057: 'X509Certificate2.X509Certificate2(byte[])' is obsolete: 'Loading certificate data through the constructor or Import is obsolete. Use X509CertificateLoader instead to load certificates.' (https://aka.ms/dotnet-warnings/SYSLIB0057)
Sebep, eski constructor'ın gelen byte'ların formatını kendisi tahmin etmesi: sertifika beklediğin yerde PKCS (Public-Key Cryptography Standards; public key kriptografisi için standart dizisi) #12 formatında, yani sertifikayı private key ile birlikte paketleyen parola korumalı bir dosya da yüklenebiliyordu ve bu, beklenmedik bir dosyanın beklenmedik yoldan işlenmesi demek. Yeni loader ne beklediğini senden istiyor: LoadCertificate yalnız tek bir sertifika kabul ediyor, aynı byte'lara PKCS#12 verdiğimde ASN1 corrupted data ile reddetti; PKCS#12 için ayrıca LoadPkcs12 var. Eski kodu taşımayı serinin son yazısında topluca ele alıyoruz.
Linux dünyasında sertifika ve anahtar çoğunlukla iki ayrı PEM (Base64 metin formatı) dosyası olarak geliyor. X509Certificate2.CreateFromPemFile(certPath, keyPath) ikisini birleştiriyor. Windows'ta bir fark var: PEM'den yüklenen anahtar ephemeral, yani diske yazılmamış bir anahtar ve Windows'un TLS katmanı Schannel bu anahtarı kullanamıyor. Microsoft'un SslStream sorun giderme sayfası bunu No credentials are available in the security package hatasıyla belgeliyor. Kestrel'in kendi PEM yükleyicisi bu yüzden Windows'ta sertifikayı PKCS#12'ye çevirip yeniden yüklüyor; bizim yardımcımız da aynısını yapıyor:
public static X509Certificate2 LoadFromPem(string certificatePath, string keyPath)
{
X509Certificate2 certificate = X509Certificate2.CreateFromPemFile(certificatePath, keyPath);
if (!OperatingSystem.IsWindows())
{
return certificate;
}
using (certificate)
{
return X509CertificateLoader.LoadPkcs12(certificate.Export(X509ContentType.Pkcs12), null);
}
}
PKCS#12 dosyası, Windows'taki adıyla PFX (sertifika ve private key paketi), dışa aktarılırken de .NET 10'da bir seçim var. Eski Export(X509ContentType.Pkcs12, password) en eski okuyucularla uyumlu olmak için 3DES ve SHA-1 kullanıyor; yeni ExportPkcs12 algoritmayı sana seçtiriyor. İkisini de üretip openssl pkcs12 -info ile baktım (parola her koşuda openssl rand ile üretilip environment variable olarak verildi):
cert.Export(X509ContentType.Pkcs12, password)
cert.ExportPkcs12(Pkcs12ExportPbeParameters.Pbes2Aes256Sha256, password)
== ordering-default.pfx
MAC: sha1, Iteration 2000
PKCS7 Encrypted data: pbeWithSHA1And3-KeyTripleDES-CBC, Iteration 2000
Shrouded Keybag: pbeWithSHA1And3-KeyTripleDES-CBC, Iteration 2000
== ordering-aes.pfx
MAC: sha256, Iteration 2000
PKCS7 Encrypted data: PBES2, PBKDF2, AES-256-CBC, Iteration 2000, PRF hmacWithSHA256
Shrouded Keybag: PBES2, PBKDF2, AES-256-CBC, Iteration 2000, PRF hmacWithSHA256
Dosyayı yalnız modern sistemler okuyacaksa Pbes2Aes256Sha256 seç. İki çıktıda da 2000 iterasyon var; bu, parolayı tek başına koruma olarak görmemen gerektiğinin bir hatırlatması. PFX dosyasını secret gibi sakla.
Chain Building ve Trust Anchor
Bir sertifikaya güvenmek, imza zincirini izleyip güvendiğin bir köke, yani trust anchor'a ulaşmak demek. .NET'te bunu X509Chain yapıyor ve varsayılan olarak işletim sisteminin trust store'unu kullanıyor. İç servisler için bu yanlış kapsam: makinenin güvendiği yüzlerce public CA'nın herhangi birinden alınmış bir sertifikanın iç API'mize girmesini istemiyoruz. Doğrulayıcımız yalnız kendi root'umuza güveniyor:
public CertificateCheck Validate(X509Certificate2 certificate, Oid requiredUsage, string? expectedDnsName, DateTime? at = null)
{
using X509Chain chain = new();
X509ChainPolicy policy = chain.ChainPolicy;
policy.TrustMode = X509ChainTrustMode.CustomRootTrust;
policy.CustomTrustStore.AddRange(trustAnchors);
policy.ExtraStore.AddRange(intermediates);
policy.ApplicationPolicy.Add(requiredUsage);
policy.RevocationMode = RevocationMode;
policy.DisableCertificateDownloads = true;
if (at is not null)
{
policy.VerificationTime = at.Value;
}
bool chainOk = chain.Build(certificate);
List<string> problems = [.. chain.ChainStatus.Select(s => s.Status.ToString()).Distinct()];
// Chain isimlerle ilgili hiçbir şey söylemez: host adı ayrı bir kontrol
if (expectedDnsName is not null && !certificate.MatchesHostname(expectedDnsName))
{
problems.Add($"NameMismatch({expectedDnsName})");
}
List<string> elements = [.. chain.ChainElements.Select(e => e.Certificate.GetNameInfo(X509NameType.SimpleName, false))];
return new CertificateCheck(chainOk && problems.Count == 0, problems, elements);
}
ApplicationPolicy EKU kontrolünü chain'e ekliyor, ExtraStore intermediate'ları veriyor ama onlara güvenmiyor, DisableCertificateDownloads ise chain kurulurken sertifikadaki AIA (Authority Information Access; eksik intermediate'ın indirileceği adres) alanından internete çıkılmasını engelliyor. chain komutu aynı doğrulayıcıyı yedi durumla deniyor:
$ dotnet run --project src/Bookstore.CryptoLab -- chain
valid server certificate : OK [] chain: api.bookstore.test > Bookstore Dev Services CA > Bookstore Dev Root CA
expired : REJECTED [NotTimeValid] chain: api.bookstore.test > Bookstore Dev Services CA > Bookstore Dev Root CA
client cert used as server : REJECTED [NotValidForUsage] chain: bookstore-ordering > Bookstore Dev Services CA > Bookstore Dev Root CA
issued by another root : REJECTED [PartialChain] chain: api.bookstore.test > Bookstore Dev Services CA
intermediate not available : REJECTED [PartialChain] chain: api.bookstore.test
wrong host name : REJECTED [NameMismatch(admin.bookstore.test)] chain: api.bookstore.test > Bookstore Dev Services CA
revocation Online, no CRL : REJECTED [RevocationStatusUnknown, OfflineRevocation] chain: api.bookstore.test > Bookstore Dev Services CA > Bookstore Dev Root CA
Satırların her biri ayrı bir ders. Ordering'in client certificate'ı zincir olarak tamamen geçerli, ama EKU'su istemci kimliği; sunucu rolünde NotValidForUsage ile reddediliyor. Başka bir PKI'nın sertifikası, intermediate'ı elimizde olsa bile, bizim root'umuza ulaşamadığı için PartialChain oluyor. "Wrong host name" satırında zincir kusursuz ve ChainStatus boş; ad uyuşmazlığını yakalayan, X509Chain'in değil bizim eklediğimiz MatchesHostname çağrısı. Kendi doğrulamanı yazıyorsan bu ayrımı unutma: chain "bu sertifikayı güvenilir biri verdi" der, "bu sertifika konuştuğun sunucuya ait" demez.
Revocation ve Kısa Ömürlü Sertifikalar
Son satır revocation'ı, yani iptal kontrolünü gösteriyor. Bir sertifikanın private key'i sızarsa CA onu iptal eder ve bunu CRL (Certificate Revocation List; iptal edilmiş sertifikaların listesi) ya da OCSP (Online Certificate Status Protocol; tek bir sertifikanın durumunu soran protokol) ile yayınlar. Bizim dev PKI'mız CRL yayınlamıyor; Online modunda .NET iptal durumunu öğrenemiyor ve güvenli tarafta kalıp reddediyor. İç PKI'da iki seçenek var: CRL yayınlamak ya da sertifika ömrünü iptale gerek kalmayacak kadar kısa tutup NoCheck kullanmak. Biz ikincisini seçtik ve servis sertifikaları 30 gün geçerli.
Public sertifikalarda sektör de aynı yöne gidiyor. CA/Browser Forum'un (CA'lar ile tarayıcı üreticilerinin kural koyduğu kurul) Baseline Requirements belgesine göre public TLS sertifikalarının en uzun ömrü 15 Mart 2026'dan beri 200 gün; 15 Mart 2027'de 100 güne, 15 Mart 2029'da 47 güne iniyor. Aynı belge Mart 2024'ten beri OCSP'yi CA'lar için isteğe bağlı tutuyor, CRL yayınlamayı ise zorunlu kılıyor. Bunun geliştiriciye yansıması: sertifikayı elle yenilemek ve thumbprint'i koda gömmek artık sürdürülebilir değil. Sertifikayı adıyla ya da dosya yoluyla bul, yenilenmiş sertifikayı restart gerektirmeden yükle.
Doğrulama Callback'i: Her Şeye true Dönmek
İstemci tarafında en sık gördüğüm hata, iç CA'ya güvenmek için doğrulamayı tamamen kapatmak: ServerCertificateCustomValidationCallback'ten her durumda true dönmek ya da DangerousAcceptAnyServerCertificateValidator kullanmak. Bu, saldırganın kendi ürettiği sertifikayı da kabul etmek demek; SSL/TLS Certificate'ları ve HTTPS serisinin Kestrel yazısı bunu gerçek bir koşuyla gösteriyor. Doğru yol, platformun doğrulamasını açık tutup yalnız güvenilen kökü değiştirmek:
public static SocketsHttpHandler CreateMtlsHandler(X509Certificate2Collection trustAnchors, X509Certificate2? clientCertificate)
{
X509ChainPolicy policy = new()
{
TrustMode = X509ChainTrustMode.CustomRootTrust,
RevocationMode = X509RevocationMode.NoCheck,
DisableCertificateDownloads = true,
};
policy.CustomTrustStore.AddRange(trustAnchors);
return new SocketsHttpHandler
{
SslOptions = new SslClientAuthenticationOptions
{
CertificateChainPolicy = policy,
ClientCertificates = clientCertificate is null ? null : [clientCertificate],
},
};
}
CertificateChainPolicy yalnız zinciri değiştiriyor; host adı kontrolü yerinde kalıyor. Aynı sunucuya bu kez localhost yerine 127.0.0.1 ile bağlandığımda, sertifikanın SAN'ında IP adresi olmadığı için üç istek de TLS aşamasında düştü:
$ dotnet run --project src/Bookstore.CryptoLab -- mtls-call https://127.0.0.1:7443
ordering : HttpRequestException: The remote certificate is invalid according to the validation procedure: RemoteCertificateNameMismatch
shipping : HttpRequestException: The remote certificate is invalid according to the validation procedure: RemoteCertificateNameMismatch
no client certificate : HttpRequestException: The remote certificate is invalid according to the validation procedure: RemoteCertificateNameMismatch
Callback'e gerçekten ihtiyacın varsa, örneğin sertifikanın belirli bir intermediate'tan gelmesini de istiyorsan, platformun kararını ezme, üstüne ekle: callback'e gelen SslPolicyErrors değeri None değilse hemen false dön, ek kontrolünü ancak ondan sonra yap.
ASP.NET Core'da mTLS
Sunucu tarafında Kestrel her bağlantıda client certificate istiyor, doğrulamayı da Microsoft.AspNetCore.Authentication.Certificate paketindeki handler yapıyor. Kurulumun ayrıntıları ve Kestrel'in iç CA'dan gelen sertifikayı TLS katmanında sessizce reddetmesi SSL/TLS Certificate'ları ve HTTPS serisinin mTLS yazısında anlatıldı; ben de aynı sonucu gördüm, AllowAnyClientCertificate olmadan Ordering'in geçerli sertifikası bile bağlantıyı koparıyordu. Bizim kurulumumuzun farkı iki noktada:
builder.WebHost.ConfigureKestrel(kestrel => kestrel.ConfigureHttpsDefaults(https =>
{
https.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
// TLS yalnız sertifikayı ister; güven kararını aşağıdaki handler kendi root'umuzla verir
https.AllowAnyClientCertificate();
}));
builder.Services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.ChainTrustValidationMode = X509ChainTrustMode.CustomRootTrust;
options.CustomTrustStore = trustAnchors;
options.AdditionalChainCertificates = intermediates;
options.AllowedCertificateTypes = CertificateTypes.Chained;
options.RevocationMode = X509RevocationMode.NoCheck; // dev PKI CRL yayınlamıyor
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
string? service = ServiceNameOf(context.ClientCertificate);
if (service is null)
{
context.Fail("No bookstore service name in the certificate SAN.");
return Task.CompletedTask;
}
context.Principal = new ClaimsPrincipal(new ClaimsIdentity(
[new Claim(ServiceClaim, service), new Claim(ClaimTypes.Name, service)], context.Scheme.Name));
context.Success();
return Task.CompletedTask;
},
};
});
builder.Services.AddAuthorizationBuilder()
.SetFallbackPolicy(new AuthorizationPolicyBuilder().RequireAuthenticatedUser().Build())
.AddPolicy("OrderingService", policy => policy.RequireClaim(ServiceClaim, "ordering"));
Birincisi, servis kimliğini CN'den değil SAN'daki ordering.bookstore.test adından alıyoruz: ServiceNameOf yalnız .bookstore.test ile biten DNS adlarını kabul ediyor. İkincisi, kimlik doğrulama ile yetkilendirmeyi ayırıyoruz: Shipping servisinin sertifikası da aynı CA'dan ve geçerli, ama stok rezervasyonu yalnız Ordering'e açık. Fallback policy de AllowAnyClientCertificate'ın açtığı kapıyı kapatıyor: kimliği doğrulanmamış hiçbir istek hiçbir endpoint'e ulaşmıyor. API'yi başlatıp Ordering rolündeki lab komutunu çalıştırdım:
$ dotnet run --project src/Bookstore.Api &
$ dotnet run --project src/Bookstore.CryptoLab -- mtls-call
ordering : 201 Created {"reservationId":"01a1d7fe-8fff-7eb1-87e2-5afd783dfd2d","orderId":"0197f001-0000-7000-8000...
shipping : 403 Forbidden
no client certificate : HttpRequestException: The response ended prematurely. (ResponseEnded)
Sertifikasız istek HTTP yanıtı bile almıyor. TLS 1.3'te client certificate handshake'in son adımında gönderiliyor ve istemci, sunucunun reddini ancak ilk isteği yazdıktan sonra bağlantının kapanmasıyla öğreniyor; hata mesajının bir TLS hatası değil "yanıt erken bitti" olmasının sebebi bu. openssl s_client ile bağlandığımda sunucunun leaf'i ve intermediate'ı birlikte gönderdiği de görülüyordu: Kestrel, PEM dosyasındaki ikinci sertifikayı chain olarak kullanıyor, bu yüzden pki-init leaf'in arkasına intermediate'ı ekliyor.
Bu davranışları testlerle kilitledim. Testler gerçek Kestrel'i geçici bir PKI ile TLS üzerinde başlatıyor ve dört durumu kilitliyor: Ordering 201 alıyor, Shipping 403 alıyor, başka bir CA'dan gelen aynı adlı sertifika 403 alıyor, sertifikasız istek HttpRequestException ile kesiliyor:
[Fact]
public async Task Client_certificate_from_another_ca_gets_403()
{
using DevPki rogue = DevPki.Create(DateTimeOffset.UtcNow.AddMinutes(-10));
using X509Certificate2 fake = rogue.IssueClient("ordering", DateTimeOffset.UtcNow.AddMinutes(-5), DateTimeOffset.UtcNow.AddDays(1));
using HttpResponseMessage response = await Reserve(fake);
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}
$ dotnet test
Test run summary: Passed!
total: 55
failed: 0
succeeded: 55
skipped: 0
duration: 4s 597ms
TLS bir reverse proxy'de sonlanıyorsa sertifika uygulamaya bir header ile geliyor. Bu durumda proxy'nin gönderdiği header'a yalnız proxy'den gelen isteklerde güvenmek gerekiyor; Web Server'lar ve Hosting serisinin reverse proxy yazısı ile SSL/TLS Certificate'ları ve HTTPS serisinin mTLS yazısı bu kurulumu anlatıyor.
Üretimde Dikkat
Root CA'nın private key'i bu yapının en değerli parçası: onunla herhangi bir servis adına sertifika üretilebilir. Root'u offline tut, günlük imzalamayı intermediate'a bırak. Bizim DevPki class'ı her şeyi bellekte ve tek bir yerde üretiyor; yalnız geliştirme ve testler için.
Sertifika süresi dolmadan yenilenmeli ve yenilenen sertifika restart'sız yüklenmeli. Kestrel config'deki sertifika dosyasını izleyip değişince yeniden yükleyebiliyor; kendi HttpClient'ının client certificate'ını ise SocketsHttpHandler'ı yeniden oluşturarak, örneğin IHttpClientFactory'nin handler ömrüyle, yenilemen gerekiyor. Süre dolmadan önce uyarı veren bir metrik ya da health check, 3 aylık sertifikadan 47 günlüğe geçerken fark yaratıyor.
Doğrulama hatalarını sonuçlarıyla birlikte logla: PartialChain, NotTimeValid ve NotValidForUsage farklı sorunlar ve farklı ekiplerin işi. Loglarda sertifikanın subject'i, issuer'ı ve SHA-256 thumbprint'i yeterli; private key'in olduğu PEM dosyası asla log'a, artifact'a ya da container image'ına girmemeli. Bizim snapshot'ımızın .gitignore'u certs/, *.pem ve *.pfx'i bu yüzden dışarıda tutuyor.
Sık Düşülen Tuzaklar
Birinci tuzak, Önce Tahmin Et'teki: iç ağ diye doğrulamayı kapatmak. İç ağ, saldırganın bir kez içeri girdikten sonra en rahat dolaştığı yer; mTLS'in değeri tam da orada.
İkincisi, chain'i doğrulayıp adı doğrulamamak. Kendi X509Chain kodunu yazdığında MatchesHostname ya da eşdeğer bir kontrol eklemezsen, aynı CA'dan sertifika almış herhangi bir servis başka bir servisi taklit edebilir.
Üçüncüsü, kimliği CN'den okumak. SAN host adı doğrulamasının baktığı yer; CN'yi serbest metin gibi kullanan bir sistemde iki farklı alanın farklı şey söylemesi sorun çıkarır.
Dördüncüsü, EKU'yu kontrol etmemek. EKU kontrolü olmayan bir doğrulayıcı, istemci sertifikasını sunucu sertifikası olarak da kabul eder; yukarıdaki NotValidForUsage satırı bunu yakalıyor.
Beşincisi, PEM dosyalarını string olarak birleştirirken araya satır sonu koymamak. ExportCertificatePem() çıktısı satır sonuyla bitmiyor; iki çıktıyı doğrudan yan yana yazdığımda dosya -----END CERTIFICATE----------BEGIN CERTIFICATE----- satırıyla bozuldu ve okumaya çalışınca ASN1 corrupted data hatası aldım.
Küçük Bir Deneme
★☆☆ chain komutuna, servis sertifikasının root tarafından doğrudan, intermediate atlanarak imzalandığı bir durum ekle. Sonuç geçerli mi? Intermediate'ın pathLength: 0 kısıtı bu durumu etkiliyor mu?
★★☆ ServiceCertificateValidator'a bir ayar ekle: yalnız belirli bir intermediate'ın imzaladığı sertifikalar kabul edilsin (chain'in ikinci elemanının SHA-256 thumbprint'i beklenen değer olmalı). Başka bir intermediate'tan gelen ama aynı root'a bağlanan bir sertifikanın reddedildiğini test et.
★★★ Ordering'in client certificate'ını yenilenebilir yap: sertifika dosyası değiştiğinde yeni bir SocketsHttpHandler oluşturan küçük bir MtlsClientProvider yaz ve süresi 1 dakika kalmış bir sertifikayla başlayıp dosyayı yenisiyle değiştirdiğinde isteklerin kesintisiz devam ettiğini bir testle göster.
Sertifikalar kimin kim olduğunu kanıtlıyor; bağlantının içindeki veriyi ise TLS koruyor. Bir sonraki yazıda TLS'in içine giriyoruz: handshake'ten çıkan secret'ların trafik anahtarlarına nasıl dönüştüğü, record layer'da AEAD (Authenticated Encryption with Associated Data; hem gizleyen hem bütünlüğü doğrulayan şifreleme) ve forward secrecy'nin nereden geldiği, ve kendi trafiğimizi .NET'in key log'uyla çözmek.