Bir TLS Certificate'ın İçinde Ne Var? X.509, SAN, Chain of Trust ve Dosya Formatları
Bir TLS certificate'ı, bir public key'i SAN listesindeki isimlere bağlayan ve bir CA'nın imzaladığı X.509 belgesidir; istemci ona, imzalar zinciri trust store'undaki bir root'a ulaştığı için güvenir. openssl ve .NET X509CertificateLoader ile alanları okuyor, wildcard'ın tek seviyelik sınırını, Let's Encrypt'in cross-sign'lı Generation Y zincirini ve eksik intermediate'ın curl'ü kırıp .NET'te AIA ile nasıl gizlendiğini gerçek koşularla görüyoruz.
Sürüm notu: Bu yazıdaki örnekler OpenSSL 3.6, Nginx 1.30.6, curl 8.5 ve .NET 10 (SDK 10.0.404) ile Docker 29 üzerinde denenmiştir; Bookstore certificate'ları çalışma klasöründe üretilmiş test certificate'larıdır, gerçek zincir örneği 14 Kasım 2026'da alınmıştır.
Önce Tahmin Et
Bookstore'un Nginx'ine yeni bir certificate takıldı. Tarayıcıda kilit simgesi yeşil, kimse bir şikâyette bulunmuyor. Aynı gün iki şey oluyor: monitoring tarafındaki curl tabanlı health check, SSL adını hâlâ taşıyan "SSL certificate problem" diye kırmızıya dönüyor, kargo firmasının Linux'ta çalışan .NET servisi ise siparişleri sorunsuz çekmeye devam ediyor. Bir hafta sonra kargo firması outbound trafiğini bir firewall arkasına alıyor ve onların servisi de aynı hatayı vermeye başlıyor.
Tahmin et: certificate'ta mı sorun var, sunucuda mı? Firewall'un bir HTTPS (HTTP Secure) bağlantısıyla ne ilgisi olabilir? Cevabı yazının ortasında gerçek koşuyla göreceğiz; o noktaya gelene kadar certificate'ın içinde gerçekte ne olduğunu açalım.
Bir Certificate'ı Okumak: openssl x509 -text
Serinin adında SSL geçiyor, çünkü sektör hâlâ "SSL certificate" diyor. Ama SSL protokolü çoktan kullanımdan kalktı: SSL 3.0'ın yerini 1999'da TLS 1.0 aldı ve bugün sunucunda açık olması gereken sürümler TLS 1.2 ile TLS 1.3. "SSL certificate" dediğimiz şey de aslında protokolden bağımsız bir X.509 certificate'ı; aynı dosya TLS 1.2 ile de TLS 1.3 ile de kullanılır. Bu seride protokole TLS, dosyaya certificate diyeceğiz. Handshake'in kablodaki adımları Networking ve HTTP serisinin "TLS 1.3 Handshake'i" yazısında; burada operasyon tarafındayız: certificate'ın içinde ne var, istemci ona neden güveniyor, hangi dosya ne işe yarıyor.
Çalışma klasöründe üç katmanlı küçük bir PKI (sertifika ve anahtarların yönetildiği güven altyapısı) kurdum: bir test root CA'sı (sertifika veren güvenilir kuruluş), onun imzaladığı bir intermediate ve intermediate'ın bookstore.example için imzaladığı 90 günlük bir leaf certificate. Leaf'i açalım; public key ve imza baytları kalabalık yapmasın diye -certopt ile gizledim:
$ openssl x509 -in bookstore.example.crt -noout -text -certopt no_pubkey,no_sigdump
Certificate:
Data:
Version: 3 (0x2)
Serial Number:
29:89:cc:0e:ad:e7:33:48:1f:4f:57:57:1b:26:6a:fe:21:b1:f9:38
Signature Algorithm: ecdsa-with-SHA256
Issuer: C=TR, O=Bookstore Test, CN=Bookstore Test Issuing CA 1
Validity
Not Before: Nov 13 21:12:32 2026 GMT
Not After : Feb 11 21:12:32 2027 GMT
Subject: CN=bookstore.example
X509v3 extensions:
X509v3 Basic Constraints: critical
CA:FALSE
X509v3 Key Usage: critical
Digital Signature
X509v3 Extended Key Usage:
TLS Web Server Authentication
X509v3 Subject Key Identifier:
2A:F7:21:14:36:46:83:3F:F3:76:1C:4A:4D:C6:5E:31:F0:D6:85:9C
X509v3 Authority Key Identifier:
EA:24:1D:4C:8C:BB:6B:C6:3F:E2:30:0F:34:3B:B5:22:7E:00:5B:CC
X509v3 Subject Alternative Name:
DNS:bookstore.example, DNS:www.bookstore.example
Authority Information Access:
CA Issuers - URI:http://pki.bookstore.example/issuing-ca-1.crt
Yukarıdan aşağıya okuyalım. Version: 3, extension taşıyabilen X.509 sürümü; bugün gördüğün her certificate v3'tür ve asıl bilgi extension'lardadır. Serial Number, CA'nın bu certificate'a verdiği benzersiz numara; revocation listelerinde certificate'lar bu numarayla anılır. Issuer, certificate'ı imzalayan CA'nın adı; Subject ise certificate'ın sahibi. Validity iki zaman damgasıdır ve istemci "şimdi" bu aralıkta değilse bağlantıyı reddeder; sunucunun saati değil, istemcinin saati belirleyicidir. Public key bölümünü gizledik ama orada sunucunun ECDSA (eliptik eğri tabanlı dijital imza algoritması) P-256 public key'i duruyor; certificate'ın bütün amacı bu key'i bir isme bağlamak.
Extension'lar certificate'ın neye yaradığını söyler. Basic Constraints: CA:FALSE, bu certificate'ın başka certificate imzalayamayacağını belirtir; intermediate'ta burada CA:TRUE, pathlen:0 yazar. Key Usage key'in kriptografik olarak ne için kullanılabileceğini, Extended Key Usage (EKU) ise hangi uygulama için geçerli olduğunu söyler: TLS Web Server Authentication bu certificate'ın bir TLS sunucusunu tanıtmak için verildiğini gösterir. Subject Key Identifier (SKI) ve Authority Key Identifier (AKI), zincirdeki halkaları birbirine bağlayan parmak izleri: leaf'in AKI değeri, intermediate'ın SKI değeriyle aynıdır. AIA (sertifikada intermediate sertifikanın adresini veren alan) ise "beni imzalayan CA'nın certificate'ını şu adresten indirebilirsin" diyor; bu satırın neden önemli olduğunu az sonra göreceğiz. Aynı alanları tarayıcıda kilit simgesinden açılan certificate viewer'da da görebilirsin. Ama orada gördüğün zincir tarayıcının kendi kurduğu zincirdir, sunucunun gönderdiği değil; bu fark yazının ortasında önem kazanacak.
Aynı alanlara gerçek bir certificate'ta bakmak için Let's Encrypt'in kendi sitesinin certificate'ını indirip yalnızca ilgilendiğim extension'ları seçtim (OpenSSL 3'teki -ext seçeneğiyle):
$ openssl x509 -in le-leaf.pem -noout -subject -issuer -dates \
-ext subjectAltName,certificatePolicies,crlDistributionPoints,authorityInfoAccess
subject=CN=letsencrypt.org
issuer=C=US, O=Let's Encrypt, CN=YE2
notBefore=Nov 3 13:44:34 2026 GMT
notAfter=Feb 1 13:44:33 2027 GMT
Authority Information Access:
CA Issuers - URI:http://ye2.i.lencr.org/
X509v3 Subject Alternative Name:
DNS:cp.letsencrypt.org, DNS:cp.root-x1.letsencrypt.org, DNS:cps.letsencrypt.org, DNS:cps.root-x1.letsencrypt.org, DNS:lencr.org, DNS:letsencrypt.com, DNS:letsencrypt.org, DNS:www.lencr.org, DNS:www.letsencrypt.com, DNS:www.letsencrypt.org
X509v3 Certificate Policies:
Policy: 2.23.140.1.2.1
X509v3 CRL Distribution Points:
Full Name:
URI:http://ye2.c.lencr.org/51.crl
Tek certificate on farklı ismi kapsıyor. 2.23.140.1.2.1, CA/Browser Forum'un (CA'lar ile tarayıcı üreticilerinin kuralları birlikte yazdığı kurul) "domain validated" policy OID'si (sertifika alanlarını tanımlayan noktalı numara). CRL (iptal edilmiş sertifikaların listesi) adresi var, ama OCSP (sertifikanın iptal durumunu anlık sorgulama protokolü) adresi yok; Let's Encrypt OCSP'yi 2025'te bıraktı. Tam -text çıktısında bir de Certificate Transparency (CT) log'larının verdiği SCT (sertifikanın CT log'una kaydedildiğini gösteren imzalı kanıt) kayıtları görünüyor; CT ve revocation, serinin ikinci yazısının konusu.
SAN ve CN: Hostname Nasıl Eşleşir?
Eski alışkanlık, hostname'i Subject'teki CN'de (sertifikadaki sahip adı alanı) aramaktı. Bugün tarayıcılar ve TLS library'leri hostname'i yalnızca SAN (sertifikanın geçerli olduğu alan adları listesi) extension'ında arar. HTTPS'i tanımlayan RFC 2818 (Request for Comments; IETF, yani Internet Engineering Task Force standart dokümanı) CN'yi yıllar önce eskimiş saymıştı, Baseline Requirements (CA/Browser Forum'un public CA'lara koyduğu kurallar) 2012'den beri SAN'ı zorunlu tutuyor ve Chrome 58 (2017) CN'ye geri dönüşü tamamen kaldırdı. Pratik sonucu: kendi ürettiğin bir certificate'ta yalnızca /CN=api.internal yazıp SAN eklemezsen, modern istemciler onu reddeder. Let's Encrypt'in yeni tlsserver profili Subject CN alanını hiç doldurmuyor bile; isimlerin tek yeri SAN.
Wildcard certificate da SAN'daki bir girdiden ibaret: DNS:*.bookstore.example. Yıldız tam olarak bir etiketi (label) karşılar. Bunu test etmek için Nginx'e bir wildcard certificate taktım ve üç farklı isimle bağlandım:
$ for h in api.bookstore.example v2.api.bookstore.example bookstore.example; do
printf "%-26s " $h
echo | openssl s_client -connect 127.0.0.1:19443 -servername $h \
-CAfile root.crt -verify_hostname $h 2>&1 | grep -E '^Verify return code'
done
api.bookstore.example Verify return code: 0 (ok)
v2.api.bookstore.example Verify return code: 62 (hostname mismatch)
bookstore.example Verify return code: 62 (hostname mismatch)
İki ders var. *.bookstore.example, v2.api.bookstore.example gibi iki seviye aşağıdaki ismi kapsamaz; onun için ayrıca *.api.bookstore.example gerekir. Apex (çıplak domain) bookstore.example de wildcard'ın içinde değildir; bu yüzden wildcard certificate'larda SAN listesine apex'i ayrıca eklemek yaygındır. -servername parametresi ise SNI (TLS handshake'inde istenen host adının bildirilmesi); istemcinin handshake başında hangi host'a bağlanmak istediğini söylediği alan. Aynı IP adresi üzerinde birden çok certificate sunan sunucu, doğru certificate'ı bu bilgiye göre seçer.
Chain of Trust: Leaf, Intermediate ve Root

Bir certificate tek başına hiçbir şey kanıtlamaz; herkes kendine "ben bookstore.example'ım" diyen bir certificate üretebilir. Güven, imzalar zinciriyle kurulur. Leaf certificate'ı intermediate CA imzalar, intermediate'ı root CA imzalar, root ise kendi kendini imzalar (self-signed). Root'un güvenilir olmasının tek sebebi, istemcinin trust store'unda önceden bulunması. İstemci, sunucunun gönderdiği certificate'lardan yola çıkıp imzaları tek tek doğrular ve trust store'undaki bir root'a ulaşırsa zinciri kabul eder; ardından süreye, isme ve kullanım alanına bakar. Root'ların private key'leri genelde offline tutulur, günlük imzalamayı intermediate'lar yapar; bir intermediate'ın key'i sızarsa root'a dokunmadan yalnızca o intermediate iptal edilir.
Gerçek hayatta zincir bazen dört halkalıdır. Let's Encrypt'in sitesine bağlanınca sunucunun gönderdiği certificate'lar:
$ echo | openssl s_client -connect letsencrypt.org:443 -servername letsencrypt.org 2>&1 \
| grep -E '^ *[0-9] s:|^ i:'
0 s:CN=letsencrypt.org
i:C=US, O=Let's Encrypt, CN=YE2
1 s:C=US, O=Let's Encrypt, CN=YE2
i:C=US, O=ISRG, CN=Root YE
2 s:C=US, O=ISRG, CN=Root YE
i:C=US, O=Internet Security Research Group, CN=ISRG Root X2
3 s:C=US, O=Internet Security Research Group, CN=ISRG Root X2
i:C=US, O=Internet Security Research Group, CN=ISRG Root X1
Bu, Let's Encrypt'in "Generation Y" hiyerarşisi. YE2 intermediate'ı yeni Root YE tarafından imzalanmış. Ama Root YE henüz pek çok trust store'da yok; bu yüzden sunucu Root YE'nin ISRG (Internet Security Research Group, Let's Encrypt'i işleten kuruluş) Root X2 tarafından çapraz imzalanmış (cross-signed) bir kopyasını, X2'nin de X1 tarafından çapraz imzalanmış kopyasını gönderiyor. İstemci, trust store'unda bulduğu ilk root'ta durur. Yeni bir istemci zinciri Root YE'de bitirebilir, eski bir istemci X1'e kadar yürür. Aynı sunucu yanıtı, istemciye göre farklı uzunlukta zincirler üretir.
Trust store'lar platforma göre değişir: Windows'un kendi certificate store'u, macOS'in Keychain'i, Firefox'un kendi root listesi, Chrome'un Chrome Root Store'u ve Linux'ta dağıtımın ca-certificates paketi. .NET ayrı bir liste taşımaz; Windows'ta Windows store'unu, Linux'ta OpenSSL'in gösterdiği bundle'ı kullanır. Bookstore.Api'nin çalıştığı .NET 10 SDK image'ında (Ubuntu 24.04) baktım:
$ grep -c "BEGIN CERT" /etc/ssl/certs/ca-certificates.crt
120
$ ls /etc/ssl/certs | grep -i isrg
ISRG_Root_X1.pem
ISRG_Root_X2.pem
120 root var; X1 ve X2 içinde, Root YE yok. Yukarıdaki cross-sign'lar tam olarak bu container gibi istemciler için orada. Buradan önemli bir operasyon kuralı çıkıyor: container image'ın base'ini güncel tutmak, aynı zamanda trust store'unu güncel tutmak demek. Yıllar önce build edilmiş bir image yeni bir root'u tanımaz.
Eksik Intermediate: Kim Kırılır, Kim Kırılmaz?
Önce Tahmin Et'teki senaryoya dönelim. Production'da en sık görülen certificate hatası, sunucunun yalnızca leaf'i gönderip intermediate'ı unutması. Nginx'te iki port açtım: 443'te ssl_certificate yalnızca leaf'i, 8443'te leaf ile intermediate'ın birleştirildiği fullchain dosyasını gösteriyor. Root, istemcinin trust store'unda:
$ echo | openssl s_client -connect 127.0.0.1:18443 -servername bookstore.example \
-CAfile root.crt -verify_hostname bookstore.example 2>&1 | grep -E 'verify error|Verify return'
verify error:num=20:unable to get local issuer certificate
verify error:num=21:unable to verify the first certificate
Verify return code: 21 (unable to verify the first certificate)
OpenSSL zinciri tamamlayamıyor. Sonra aynı iki adrese, test root'unu update-ca-certificates ile trust store'a eklediğim Linux container'ından curl ve .NET HttpClient ile gittim. HttpClient denemesinde AIA indirmesini bir kez açık, bir kez kapalı bıraktım:
static async Task GetAsync(string url, bool allowDownloads)
{
SocketsHttpHandler handler = new()
{
SslOptions = new SslClientAuthenticationOptions
{
CertificateChainPolicy = new X509ChainPolicy
{
// false (varsayılan): eksik intermediate AIA adresinden indirilir
DisableCertificateDownloads = !allowDownloads,
// test certificate'larında CRL adresi yok
RevocationMode = X509RevocationMode.NoCheck
}
}
};
using HttpClient client = new(handler);
try
{
string body = await client.GetStringAsync(url);
Console.WriteLine($"OK: {body.Trim()}");
}
catch (HttpRequestException ex)
{
Console.WriteLine($"{ex.GetType().Name}: {ex.Message}");
Console.WriteLine($" inner: {ex.InnerException?.Message}");
}
}
$ curl -sS https://bookstore.example:8443/
full chain
$ curl -sS https://bookstore.example/ 2>&1 | head -1
curl: (60) SSL certificate problem: unable to get local issuer certificate
$ dotnet Bookstore.CertInspector.dll get https://bookstore.example:8443/ aia-off
OK: full chain
$ dotnet Bookstore.CertInspector.dll get https://bookstore.example/ aia-off
HttpRequestException: The SSL connection could not be established, see inner exception.
inner: The remote certificate is invalid because of errors in the certificate chain: PartialChain
$ dotnet Bookstore.CertInspector.dll get https://bookstore.example/ aia-on
OK: leaf only
Son satır çoğu kişiyi şaşırtır: Linux'taki .NET, varsayılan ayarla eksik intermediate'ı kendisi tamamladı. Nginx log'u nasıl yaptığını gösteriyor:
172.19.0.3 - - [13/Nov/2026:21:13:48 +0000] "GET /issuing-ca-1.crt HTTP/1.1" 200 573 "-" "-" "-"
172.19.0.3 - - [13/Nov/2026:21:13:48 +0000] "GET / HTTP/1.1" 200 10 "-" "-" "-"
.NET, leaf'teki AIA adresinden intermediate'ı düz HTTP ile indirdi, zinciri kurdu ve isteği gönderdi. Microsoft'un cross-platform cryptography dokümanına göre indirilen intermediate'lar Linux'ta kullanıcının CurrentUser\Intermediate store'unda cache'leniyor. Tarayıcılar da benzer şekilde eksik intermediate'ı çoğu zaman tamamlar: ya AIA ile indirir ya da önceden bildiği intermediate'ları kullanır. curl ve OpenSSL tabanlı istemcilerin çoğu ise AIA takip etmez.
Önce Tahmin Et'in cevabı bu. Certificate'ta sorun yoktu; sunucu zinciri eksik gönderiyordu. Tarayıcı ve kargo firmasının .NET servisi eksiği AIA ile kapatıyordu. Firewall gelip intermediate'ın indirildiği http:// adresini kapatınca .NET servisi de curl ile aynı duruma düştü. Ders: sunucuda her zaman fullchain ver ve test ederken AIA'ya güvenmeyen bir istemci (openssl veya curl) kullan.
DV, OV, EV: Ne Doğrulanıyor?
Public CA'lar certificate'ı üç doğrulama seviyesinde verir. DV (yalnız alan adı sahipliği doğrulanan sertifika) yalnızca başvuranın domain'i kontrol ettiğini kanıtlar; Let's Encrypt'in verdiği tek tür budur ve yukarıdaki 2.23.140.1.2.1 OID'si bunu söyler. OV (kurumun kimliği de doğrulanan sertifika) buna şirketin gerçekten var olduğunun kontrolünü ekler ve Subject'e O= alanı girer. EV (kurumu en sıkı şekilde doğrulanan sertifika türü) daha sıkı bir şirket doğrulamasıdır ve bir zamanlar adres çubuğunda yeşil şirket adı olarak gösteriliyordu.
Teknik açıdan üçü aynı şifrelemeyi sağlar; fark yalnızca CA'nın başvuranı ne kadar araştırdığında. Chrome 77 ve Firefox 70, 2019'da EV göstergesini adres çubuğundan kaldırıp kilit simgesinin arkasındaki bilgi paneline taşıdı. Gerekçeleri, çalışmaların EV arayüzünün kullanıcıları phishing'e karşı beklendiği gibi korumadığını göstermesiydi. Bugün bir API veya SPA (sayfa yenilemeden çalışan tek sayfalık web uygulaması) için EV almak çoğu zaman yalnızca maliyet ve otomasyonsuz bir süreç demek; OV/EV ihtiyacı genellikle bir sözleşmeden veya regülasyondan gelir.
Dosya Formatları: PEM, DER, PFX ve Uzantı Karmaşası
Certificate'ın içeriği hep aynı ASN.1 (Abstract Syntax Notation One) yapısı; değişen, bu yapının diske nasıl yazıldığı. Uzantılar ise neredeyse hiçbir şey söylemez: .crt ve .cer PEM (sertifika ve anahtarlar için Base64 metin formatı) de olabilir, DER (sertifikaların binary kodlama formatı) de. Dosyanın ne olduğunu ilk baytlarına bakarak anlarsın:
$ head -c 64 bookstore.example.crt
-----BEGIN CERTIFICATE-----
MIICgjCCAgegAwIBAgIUKYnMDq3nM0gfT1dX
$ openssl x509 -in bookstore.example.crt -outform DER -out bookstore.example.cer
$ head -c 16 bookstore.example.cer | xxd
00000000: 3082 0282 3082 0207 a003 0201 0202 1429 0...0..........)
$ head -1 bookstore.example.key
-----BEGIN EC PRIVATE KEY-----
| Format | İçerik | Tipik uzantı | Nerede görürsün |
|---|---|---|---|
| PEM | Base64 metin, -----BEGIN ...----- başlıklı; bir dosyada birden çok certificate olabilir | .pem, .crt, .key | Nginx, Certbot, Kestrel Path/KeyPath |
| DER | Aynı yapının ikili hali, tek certificate | .der, .cer | Windows'ta dışa aktarma, AIA adresleri |
| PKCS#12 / PFX (Public-Key Cryptography Standards #12 / Personal Information Exchange) | Certificate, zincir ve private key tek, parolalı bir dosyada | .pfx, .p12 | Windows, IIS (Windows'un web sunucusu), Azure Key Vault, Kestrel Password |
| Private key | PKCS#8 (BEGIN PRIVATE KEY) veya algoritmaya özgü (BEGIN EC PRIVATE KEY) | .key | Yalnızca sunucunun kendisinde |
PEM, DER'in Base64 hali; 3082 ile başlayan ikili dosya ise DER'in tipik imzası (ASN.1 SEQUENCE). PFX'e dönüştürmek için leaf, intermediate ve key'i tek dosyada birleştiriyorsun; parolayı komut satırına yazmak yerine environment variable'dan okutmak shell history'ye düşmesini engeller:
$ openssl pkcs12 -export -in bookstore.example.crt -inkey bookstore.example.key \
-certfile issuing-ca-1.crt -out bookstore.example.pfx -passout env:PFX_PASSWORD
$ openssl pkcs12 -in bookstore.example.pfx -passin env:PFX_PASSWORD -info -noout
MAC: sha256, Iteration 2048
MAC length: 32, salt length: 16
PKCS7 Encrypted data: PBES2, PBKDF2, AES-256-CBC, Iteration 2048, PRF hmacWithSHA256
Certificate bag
Certificate bag
PKCS7 Data
Shrouded Keybag: PBES2, PBKDF2, AES-256-CBC, Iteration 2048, PRF hmacWithSHA256
İki "Certificate bag" leaf ile intermediate, "Shrouded Keybag" parolayla şifrelenmiş private key. OpenSSL 3 varsayılan olarak AES-256 (Advanced Encryption Standard) kullanıyor. OpenSSL 1.1 döneminden kalma eski PFX'ler eski RC2 ve 3DES algoritmalarıyla şifrelenmiş olabilir ve yeni sistemler bunları açmakta zorlanabilir.
Format karmaşasının en tehlikeli sonucu, private key'in certificate ile aynı muameleyi görmesi. Certificate public bir belgedir; her TLS bağlantısında herkese zaten gönderiliyor. Private key ise sunucunun kimliğinin kendisi. Fullchain .crt dosyasını bir wiki sayfasına koymanın zararı yok; ama key'i içeren bir .pem veya parolası yanına yazılmış bir .pfx repository'ye, image'a ya da bir ticket'a düştüyse certificate'ı revoke edip yeni key ile yeniden almak gerekir.
.NET'te Certificate Okumak: X509CertificateLoader
.NET 9 ile gelen X509CertificateLoader, certificate yüklemenin önerilen yolu. Eski X509Certificate2 constructor'ları içeriğe bakıp formatı tahmin ediyordu; yeni API'de ne yüklediğini sen söylüyorsun. LoadCertificateFromFile yalnızca tek bir certificate'ı (PEM veya DER) kabul eder, LoadPkcs12FromFile ise PFX içindir. Bookstore'un küçük inceleme aracı:
static void Inspect(X509Certificate2 cert)
{
Console.WriteLine($"Subject : {cert.Subject}");
Console.WriteLine($"Issuer : {cert.Issuer}");
Console.WriteLine($"Serial : {cert.SerialNumber}");
Console.WriteLine($"Valid : {cert.NotBefore:u} -> {cert.NotAfter:u}");
Console.WriteLine($"Key : {cert.PublicKey.Oid.FriendlyName}, HasPrivateKey={cert.HasPrivateKey}");
Console.WriteLine($"Thumbprint : {cert.Thumbprint}");
foreach (X509Extension ext in cert.Extensions)
{
switch (ext)
{
case X509SubjectAlternativeNameExtension san:
Console.WriteLine($"SAN (DNS) : {string.Join(", ", san.EnumerateDnsNames())}");
break;
case X509EnhancedKeyUsageExtension eku:
Console.WriteLine($"EKU : {string.Join(", ", eku.EnhancedKeyUsages.Cast<System.Security.Cryptography.Oid>().Select(o => $"{o.FriendlyName} ({o.Value})"))}");
break;
case X509KeyUsageExtension ku:
Console.WriteLine($"KeyUsage : {ku.KeyUsages}");
break;
case X509BasicConstraintsExtension bc:
Console.WriteLine($"CA : {bc.CertificateAuthority}");
break;
}
}
}
$ dotnet Bookstore.CertInspector.dll inspect /pki/bookstore.example.crt
Subject : CN=bookstore.example
Issuer : CN=Bookstore Test Issuing CA 1, O=Bookstore Test, C=TR
Serial : 2989CC0EADE733481F4F57571B266AFE21B1F938
Valid : 2026-11-13 21:12:32Z -> 2027-02-11 21:12:32Z
Key : ECC, HasPrivateKey=False
Thumbprint : A18CAED1ABDF5A2CB8EDF4B27FA013D333AF1F67
CA : False
KeyUsage : DigitalSignature
EKU : TLS Web Server Authentication (1.3.6.1.5.5.7.3.1)
SAN (DNS) : bookstore.example, www.bookstore.example
$ dotnet Bookstore.CertInspector.dll pfx /pki/bookstore.example.pfx "$PFX_PASSWORD" | head -5
Subject : CN=bookstore.example
Issuer : CN=Bookstore Test Issuing CA 1, O=Bookstore Test, C=TR
Serial : 2989CC0EADE733481F4F57571B266AFE21B1F938
Valid : 2026-11-13 21:12:32Z -> 2027-02-11 21:12:32Z
Key : ECC, HasPrivateKey=True
$ dotnet Bookstore.CertInspector.dll inspect /pki/bookstore.example.key 2>&1 | head -1
Unhandled exception. System.Security.Cryptography.CryptographicException: ASN1 corrupted data.
PEM certificate'ta HasPrivateKey=False, PFX'te True. Key dosyasını certificate gibi yüklemeye çalışınca aldığın "ASN1 corrupted data" mesajı da, "yanlış dosyayı verdin" sorununun .NET'teki en yaygın görüntüsü. Sunucu tarafında PEM certificate ile key'i birlikte yüklemek, Kestrel'e vermek ve eski constructor'ların neden obsolete olduğu dördüncü yazının konusu.
Chain'i kodda kurmak da mümkün. X509Chain'e sistem trust store'u yerine yalnızca kendi root'umuza güvenmesini söyleyip intermediate'ı bir kez vermeden, bir kez vererek deniyoruz:
static void BuildChain(string leafPath, string rootPath, string[] extra)
{
using X509Chain chain = new();
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
chain.ChainPolicy.CustomTrustStore.Add(X509CertificateLoader.LoadCertificateFromFile(rootPath));
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.DisableCertificateDownloads = true;
foreach (string path in extra)
{
chain.ChainPolicy.ExtraStore.Add(X509CertificateLoader.LoadCertificateFromFile(path));
}
bool ok = chain.Build(X509CertificateLoader.LoadCertificateFromFile(leafPath));
Console.WriteLine($"Build: {ok}");
foreach (X509ChainElement element in chain.ChainElements)
{
Console.WriteLine($" {element.Certificate.Subject}");
}
foreach (X509ChainStatus status in chain.ChainStatus)
{
Console.WriteLine($" status: {status.Status}");
}
}
$ dotnet Bookstore.CertInspector.dll chain /pki/bookstore.example.crt /pki/root.crt
Build: False
CN=bookstore.example
status: PartialChain
$ dotnet Bookstore.CertInspector.dll chain /pki/bookstore.example.crt /pki/root.crt /pki/issuing-ca-1.crt
Build: True
CN=bookstore.example
CN=Bookstore Test Issuing CA 1, O=Bookstore Test, C=TR
CN=Bookstore Test Root CA, O=Bookstore Test, C=TR
PartialChain, HttpClient hatasında gördüğümüz statüyle aynı: zincir bir root'a ulaşamadı. CustomRootTrust, internal bir CA'ya güvenmenin doğru yolunun da habercisi; bunu dördüncü ve yedinci yazılarda kullanacağız.
Üretimde Dikkat
Sunucuya her zaman fullchain ver: leaf ve intermediate, bu sırayla. Root'u eklemek gerekmez, istemci onu zaten kendi store'undan alır; eklemek yalnızca her handshake'te fazladan bayt gönderir. Certbot'un ürettiği fullchain.pem tam olarak bu dosya. Elle certificate alıyorsan CA'nın gönderdiği "bundle" dosyasını leaf'in arkasına sen eklemelisin.
Deploy sonrası doğrulamayı AIA'ya güvenmeyen bir istemciyle yap. openssl s_client -connect host:443 -servername host çıktısında Verify return code: 0 (ok) görmek, zincirin tam ve isimle uyumlu olduğunu birlikte doğrular. Tarayıcıda yeşil kilit görmek bunu kanıtlamaz.
Egress kısıtlı ortamlarda (Kubernetes NetworkPolicy, kurumsal proxy) .NET'in AIA ve CRL indirmeleri sessizce başarısız olabilir ve el sıkışmaya gecikme ekler. Bir partner'ın sunucusu eksik zincir gönderiyorsa doğru düzeltme onların tarafında; geçici çözüm olarak intermediate'ı kendi container'ının trust store'una root gibi eklemek, o intermediate değiştiğinde yeniden kırılır.
Container image'larını düzenli yeniden build et. Base image güncellemesi ca-certificates'ı da günceller; yeni root'lar (Root YE gibi) ve kaldırılan güvensiz root'lar ancak böyle gelir. Yıllardır yeniden build edilmemiş bir image, bir gün hiçbir kod değişmeden TLS hatası vermeye başlayabilir.
Sık Düşülen Tuzaklar
Yalnızca leaf'i sunmak: Masaüstü tarayıcıda çalışır, curl'de, birçok mobil ve IoT (Internet of Things) istemcisinde ve egress'i kapalı .NET servislerinde kırılır. Nginx'te ssl_certificate her zaman fullchain dosyasını göstermeli.
Yanlış sıra: Fullchain dosyasında önce intermediate, sonra leaf varsa Nginx key ile certificate'ı eşleştiremez ve key values mismatch hatasıyla başlamaz. Sıra her zaman leaf, sonra intermediate'lar.
Hostname'i CN'ye yazıp SAN'ı unutmak: Kendi ürettiğin certificate'larda en sık hata. OpenSSL'de -addext "subjectAltName=DNS:api.internal" veya bir extension dosyası kullan.
Wildcard'ın her şeyi kapsadığını sanmak: *.bookstore.example, apex'i ve iki seviyeli alt domain'leri kapsamaz.
Uzantıya güvenmek: .cer dosyası PEM de olabilir, DER de; yükleyen araç "bozuk dosya" diyorsa önce head -c 64 ile formata bak.
Private key'i certificate gibi taşımak: Key'i e-posta, chat veya repository üzerinden paylaşmak, certificate'ı yeniden almayı gerektirir. Certificate public'tir, key değildir.
Küçük Bir Deneme
★☆☆ Kendi kullandığın üç sitenin certificate'ına openssl s_client -connect site:443 -servername site -showcerts ile bak. Zincir kaç halkalı, hangi CA'dan geliyor, leaf'in süresi kaç gün? openssl x509 -noout -ext certificatePolicies ile DV mi OV mi olduğunu bul.
★★☆ Bu yazıdaki gibi üç katmanlı bir test PKI'ı kur. SAN'sız, yalnızca CN'li bir leaf üret ve hem openssl s_client -verify_hostname hem de HttpClient ile bağlan. Hangi hata mesajlarını alıyorsun?
★★★ Eksik intermediate deneyini tekrarla, ama AIA adresini kapatmak yerine intermediate'ı .NET'in CurrentUser\Intermediate store'una elle ekle. Bağlantı kuruluyor mu? Aynı image'tan yeni bir container başlattığında ne oluyor? Bu "geçici çözümün" production'da neden bir zaman bombası olduğunu bir paragrafla yaz.
Certificate'ın içini ve zincirin nasıl kurulduğunu gördük. Bir sonraki yazıda bu certificate'ın hayatını baştan sona izleyeceğiz: CSR (certificate imzalama isteği) üretimi, CA'nın domain'i nasıl doğruladığı, Certificate Transparency log'ları, revocation'ın neden OCSP'den CRL'e döndüğü ve certificate ömrünün 2029'a kadar 47 güne inmesinin ne anlama geldiği.