Mert Özen Her Satırda Daha İleri
Güvenlik Geliştirici için Kriptografi — #01

Geliştirici için Kriptografi Haritası: Gizlilik, Bütünlük, Kimlik Doğrulama ve .NET'in Provider Modeli

Mert Özen 14 Kas 2026 15 dk 1 görüntülenme
Geliştirici için Kriptografi Haritası: Gizlilik, Bütünlük, Kimlik Doğrulama ve .NET'in Provider Modeli

Kripto kodunda başlangıç noktası algoritma değil hedeftir: gizlilik encryption, bütünlük MAC, inkâr edilemezlik digital signature ister ve .NET bu primitive'leri işletim sisteminin kütüphanesinden (CNG, OpenSSL, Apple) alır. Hedef, primitive ve .NET tipi haritasını çıkarıyor, IsSupported ile platform desteğini açılışta kontrol ediyor, one-shot API'leri, FixedTimeEquals ve ZeroMemory'yi gerçek çıktılarla gösteriyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ve xUnit v3 4.0 ile derlenip denenmiştir; platform çıktıları Ubuntu 24.04 (OpenSSL 3.0) ve Alpine 3.24 (OpenSSL 3.5) container'larında alınmıştır.

Önce Tahmin Et

Aşağıdaki satırlar aynı .NET 10 uygulamasından, aynı build çıktısından geliyor. Tek fark, uygulamanın çalıştığı container image'ı:

Ubuntu 24.04.5 LTS, OpenSSL 3.0.13
  ML-KEM             no
Alpine Linux v3.24, OpenSSL 3.5.9
  ML-KEM             yes

Kod aynı, runtime aynı, NuGet paketi yok. Bir algoritma nasıl bir makinede var, diğerinde yok olabilir? Cevabı bilen biri için bu yazının yarısı tanıdık gelecek; bilmeyen için ise serinin geri kalanını okurken en çok işine yarayacak bilgi bu. .NET'in kripto class'ları algoritmaları kendileri hesaplamıyor, işletim sisteminin kripto kütüphanesine devrediyor. Bu yazıda önce hangi güvenlik hedefinin hangi primitive ile karşılandığını bir haritaya döküyoruz, sonra .NET'in bu primitive'leri nereden aldığına, platform farkını kodla nasıl soracağına ve her kripto kodunda tekrar karşına çıkacak iki yardımcıya bakıyoruz.

Encoding, hashing ve encryption arasındaki fark, Random ile RandomNumberGenerator, MD5 (1990'ların hash algoritması) ile SHA (Secure Hash Algorithm; standart hash algoritmalarının ortak adı) ailesinin ilk üyesi SHA-1'in neden bırakıldığı ve HTTPS ile HSTS (tarayıcıya siteye yalnız HTTPS ile gelmesini söyleyen header) konusu Web Güvenliği (OWASP Top 10) serisinin "OWASP A04: Cryptographic Failures (1)" yazısında; parola saklama ve Data Protection'a giriş de aynı serinin ikinci kripto yazısında anlatıldı. Burada bunları tekrar etmiyoruz, oradan devam ediyoruz.

Güvenlik Hedefleri ve Primitive'ler

Kripto kodu yazarken ilk soru "hangi algoritmayı kullanayım?" değil, "neyi korumaya çalışıyorum?" olmalı. Dört hedef var ve her biri farklı bir araçla karşılanıyor:

  • Confidentiality (gizlilik): veriyi yalnız anahtarı olan okuyabilir. Araç: encryption.
  • Integrity (bütünlük): veri yolda ya da diskte değiştiyse fark edilir. Araç: hash tek başına yetmez, anahtarlı bir MAC (Message Authentication Code; mesajın anahtarı bilen biri tarafından üretildiğini kanıtlayan kısa değer) gerekir.
  • Authenticity (kaynak doğrulama): verinin beklediğin taraftan geldiği kanıtlanır. Araç: MAC (taraflar aynı secret'ı paylaşıyorsa) ya da digital signature.
  • Non-repudiation (inkâr edilemezlik): imzalayan taraf sonradan "ben üretmedim" diyemez. Araç: yalnız digital signature, çünkü MAC'te doğrulayan taraf da aynı anahtarla aynı değeri üretebilir.

Bu dört hedefi .NET tipleriyle eşleştirdiğimizde serinin haritası ortaya çıkıyor. Tabloda ve seride sık geçecek kısaltmalar: SHA-2 ve SHA-3 (SHA ailesinin bugün önerilen üyeleri), SHAKE (SHA-3 ailesinden, istenen uzunlukta çıktı üreten fonksiyon), HMAC (hash tabanlı MAC), KMAC (SHA-3 tabanlı MAC), KDF (Key Derivation Function; bir anahtardan ya da paroladan yeni anahtar üreten fonksiyon), HKDF (HMAC tabanlı KDF), AES (Advanced Encryption Standard; bugünün standart blok şifresi), AEAD (Authenticated Encryption with Associated Data; şifrelerken bütünlüğü de doğrulayan encryption), GCM (Galois/Counter Mode; AES'in en yaygın AEAD modu), CCM (AES'in daha az kullanılan, sayaç tabanlı AEAD modu), RSA (adını mucitlerinden alan public key algoritması), OAEP (RSA şifrelemesi için güvenli padding şeması), PSS (RSA imzası için önerilen padding şeması), ECDH (elliptic curve üzerinde iki tarafın ortak sır ürettiği key agreement), ECDSA (elliptic curve tabanlı imza algoritması), TLS (Transport Layer Security; HTTPS'in altındaki şifreli kanal protokolü), son olarak kuantum bilgisayarlara dayanıklı yeni algoritmalar: ML (Module-Lattice; kafes tabanlı matematik) ailesinden KEM (Key Encapsulation Mechanism; anahtar kapsülleme) ve DSA (Digital Signature Algorithm; imza algoritması) olan ML-KEM ve ML-DSA, hash tabanlı imza olan SLH (Stateless Hash-based; durumsuz, hash tabanlı) DSA, kısaca SLH-DSA.

HedefPrimitive.NET tipiSeride
Bütünlük (anahtarsız, yalnız kontrol)Hash: SHA-2, SHA-3SHA256, SHA3_25602
Bütünlük + kaynak (paylaşılan secret)MAC: HMAC, KMACHMACSHA256, Kmac25602
Bir anahtardan alt anahtar türetmekKDF: HKDFHKDF02
Gizlilik + bütünlükAEAD: AES-GCM, ChaCha20-Poly1305AesGcm, ChaCha20Poly130503
Anahtar sarmakAES Key Wrap with PaddingAes.EncryptKeyWrapPadded03
Anahtar dağıtımıRSA-OAEP, ECDHRSA, ECDiffieHellman04
Kaynak + inkâr edilemezlikDigital signature: RSA-PSS, ECDSARSA, ECDsa05
Public key'in sahibini bağlamakX.509 sertifika, chainX509Certificate2, X509Chain06
Aktarımdaki veriTLSSslStream07
Kuantum sonrası anahtar ve imzaML-KEM, ML-DSA, SLH-DSAMLKem, MLDsa, SlhDsa08

Tablonun bir yan etkisi de var: bir hedefe yanlış araçla yaklaşınca hata kendini hemen göstermiyor. Webhook'u düz SHA-256 ile "imzalayan" kod çalışır, testler geçer; eksik olan şey, hash'i herkesin hesaplayabilmesi. Bir alanı AES-CBC (bütünlük kontrolü olmayan eski bir mod) ile şifreleyen kod da çalışır; eksik olan bütünlük. Serinin her yazısı tabloda bir satırı alıp "doğru çağrı nasıl görünür, yanlışı nasıl fark edilir" sorusuna cevap veriyor.

Kerckhoffs İlkesi: Kendi Kriptonu Yazma, Ama Ne Çağırdığını Bil

Auguste Kerckhoffs 1883'te askeri şifreleme üzerine yazdığı makalelerde, bir sistemin güvenliğinin algoritmanın gizli kalmasına değil, yalnız anahtarın gizli kalmasına dayanması gerektiğini savundu. Bugün AES, RSA ya da TLS'in her ayrıntısı herkese açık ve güvenlikleri bundan zarar görmüyor; tersine, açık oldukları için yıllarca incelendiler. Bunun geliştiriciye pratik karşılığı iki kural.

Birincisi, kendi algoritmanı, kendi modunu ya da kendi "karıştırma" fonksiyonunu yazma. "Bit'leri sabit bir değerle karıştırıp Base64 yapalım" diye başlayan her kod, gizliliğini kodun gizli kalmasına bağlıyor; kod bir decompiler'dan geçtiği an gizlilik bitiyor. İkincisi, ve daha az konuşulanı: hazır primitive'i çağırmak da tek başına yetmiyor. AesGcm doğru bir primitive, ama aynı anahtarla aynı nonce'u iki kez kullanırsan gizliliği de bütünlüğü de kaybedersin. RSA doğru bir primitive, ama yanlış padding seçersen bilinen bir saldırı sınıfına açık kalırsın. Hatalar çoğunlukla algoritmada değil, çağıran kodda: anahtarın nereden geldiğinde, nonce'un nasıl üretildiğinde, sonucun nasıl karşılaştırıldığında, hangi byte'ların imzalandığında.

Bu seri ikinci kurala odaklanıyor. Matematiğe yalnız bir parametrenin neden önemli olduğunu açıklamak için giriyoruz; asıl iş, doğru primitive'i doğru parametreyle çağırmak ve bunu testle kanıtlamak.

.NET'in Provider Modeli: Aynı Kod, Farklı Kütüphane

.NET (Core ve .NET 5 sonrası) kripto algoritmalarını kendisi gerçeklemiyor. Aes, RSA, SHA256 gibi class'lar platformun kripto kütüphanesine giden ince bir katman: Windows'ta CNG (Cryptography API: Next Generation; Windows'un yerleşik kripto kütüphanesi), Linux'ta sistemde kurulu OpenSSL, macOS'ta Apple'ın kütüphaneleri. Bu yaklaşımın iki faydası var: güvenlik yamaları işletim sistemi güncellemesiyle gelir ve işletim sisteminin kütüphanesi FIPS (Federal Information Processing Standards; ABD kamu kurumlarının kripto modülü sertifikasyonu) onaylıysa uygulama da onaylı algoritmaları kullanmış olur. Bedeli ise şu: uygulama yalnız platformun desteklediği algoritmaları kullanabilir.

Factory method'ların ne döndürdüğüne bakalım:

public static void Providers()
{
    using RSA rsa = RSA.Create(2048);
    using Aes aes = Aes.Create();
    using SHA256 sha256 = SHA256.Create();
    Console.WriteLine(rsa.GetType().FullName);
    Console.WriteLine(aes.GetType().FullName);
    Console.WriteLine(sha256.GetType().FullName);

    // Platforma özel tip: bu guard olmadan CA1416 analyzer'ı build'i kırar
    if (OperatingSystem.IsWindows())
    {
        using RSACng cng = new(2048);
        Console.WriteLine($"RSACng, provider: {cng.Key.Provider}");
    }
    else
    {
        Console.WriteLine("RSACng: Windows only, skipped");
    }
}
$ dotnet run --project src/Bookstore.CryptoLab -- providers
System.Security.Cryptography.RSAWrapper
System.Security.Cryptography.AesImplementation
System.Security.Cryptography.SHA256+Implementation
RSACng: Windows only, skipped

Linux'ta RSA.Create() bir RSAWrapper döndürüyor; içinde OpenSSL'e giden RSAOpenSsl var. Windows'ta aynı satır CNG tabanlı bir implementation döndürür. Microsoft Learn'deki "Cross-platform cryptography" sayfası bunun bir implementation ayrıntısı olduğunu ve değişebileceğini açıkça söylüyor. Senin kodun için çıkarım basit: değişkeni her zaman soyut tipte tut (RSA, Aes, ECDsa) ve nesneyi factory method'la oluştur.

RSACng, ECDsaOpenSsl gibi platforma özel tipler yalnız native interop gerektiğinde, örneğin Windows'taki bir CNG key container'ını açarken anlamlı. Guard'ı kaldırırsan derleyici seni uyarıyor; bu projede TreatWarningsAsErrors açık olduğu için build kırılıyor:

error CA1416: This call site is reachable on all platforms. 'RSACng' is only supported on: 'windows'.

Bu analyzer'ı susturma; OperatingSystem.IsWindows() guard'ı hem analyzer'ı tatmin ediyor hem de diğer platformda kodun ne yapacağını açıkça yazmaya zorluyor.

Bir not daha: AES-GCM ve Shake128 gibi yeni primitive'ler bu inheritance modelini kullanmıyor; sealed class'lar. Kendi soyutlamanı kurmak istersen, dokümantasyonun da söylediği gibi, o katmanı senin yazman gerekiyor. Serinin ilerleyen yazılarında tam olarak bunu yapacağız: Bookstore.Crypto library'si, uygulamanın geri kalanına "alanı şifrele", "webhook'u doğrula" gibi niyet seviyesinde method'lar sunacak.

IsSupported: Platform Desteğini Kodla Sormak

Platforma bağlı olabilecek her algoritmanın static bir IsSupported property'si var. Bookstore'un kripto library'sine bunları tek yerde toplayan küçük bir probe ekliyoruz. Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı); bu seride solution'a Bookstore.Crypto library'si, deneme komutlarını çalıştıran Bookstore.CryptoLab console uygulaması ve Bookstore.Crypto.Tests test projesi ekleniyor.

public sealed record CryptoCapability(string Name, bool IsSupported);

public static class CryptoCapabilities
{
    public static IReadOnlyList<CryptoCapability> Probe() =>
    [
        new("AES-GCM", AesGcm.IsSupported),
        new("AES-CCM", AesCcm.IsSupported),
        new("ChaCha20-Poly1305", ChaCha20Poly1305.IsSupported),
        new("SHA3-256", SHA3_256.IsSupported),
        new("SHAKE128", Shake128.IsSupported),
        new("KMAC256", Kmac256.IsSupported),
        new("ML-KEM", MLKem.IsSupported),
#pragma warning disable SYSLIB5006 // MLDsa ve SlhDsa .NET 10'da [Experimental]
        new("ML-DSA", MLDsa.IsSupported),
        new("SLH-DSA", SlhDsa.IsSupported),
#pragma warning restore SYSLIB5006
    ];

    public static string DescribePlatform()
    {
        string os = RuntimeInformation.OSDescription;
        if (!OperatingSystem.IsLinux())
        {
            return os;
        }

        // OpenSSL 3 sürüm numarası düzeni: 0xMNN00PP0
        long v = SafeEvpPKeyHandle.OpenSslVersion;
        return $"{os}, OpenSSL {v >> 28}.{(v >> 20) & 0xFF}.{(v >> 4) & 0xFF}";
    }
}

MLDsa ve SlhDsa .NET 10'da [Experimental] olarak işaretli; kullanmak için SYSLIB5006 uyarısını bilinçli olarak kapatmak gerekiyor. Post-quantum tarafını serinin sekizinci yazısında açacağız; burada yalnız platform farkını görmek için listede.

Uygulamayı bir kez publish edip aynı çıktıyı iki farklı Linux image'ında çalıştırıyoruz. Windows'ta Docker Desktop ile PowerShell'de de aynı komutlar çalışır:

$ dotnet publish src/Bookstore.CryptoLab -c Release -o out
$ docker run --rm -v "${PWD}/out:/app" -w /app mcr.microsoft.com/dotnet/runtime:10.0-noble dotnet Bookstore.CryptoLab.dll capabilities
Ubuntu 24.04.5 LTS, OpenSSL 3.0.13
  AES-GCM            yes
  AES-CCM            yes
  ChaCha20-Poly1305  yes
  SHA3-256           yes
  SHAKE128           yes
  KMAC256            yes
  ML-KEM             no
  ML-DSA             no
  SLH-DSA            no
Bookstore requirements: OK
$ docker run --rm -v "${PWD}/out:/app" -w /app mcr.microsoft.com/dotnet/runtime:10.0-alpine dotnet Bookstore.CryptoLab.dll capabilities
Alpine Linux v3.24, OpenSSL 3.5.9
  AES-GCM            yes
  AES-CCM            yes
  ChaCha20-Poly1305  yes
  SHA3-256           yes
  SHAKE128           yes
  KMAC256            yes
  ML-KEM             yes
  ML-DSA             yes
  SLH-DSA            yes
Bookstore requirements: OK

Girişteki sorunun cevabı bu: post-quantum algoritmalar OpenSSL 3.5 ile geldi. Ubuntu 24.04 image'ı OpenSSL 3.0 taşıdığı için ML-KEM yok, Alpine 3.24 image'ı OpenSSL 3.5 taşıdığı için var. Aynı build çıktısı, farklı base image, farklı yetenek. Dockerfile'da base image'ı "daha küçük olsun" diye değiştirmek, farkında olmadan uygulamanın kripto yeteneğini de değiştirebilir.

Windows tarafında çalıştırma imkânım olmadı; bu yüzden çıktı yerine Microsoft Learn'deki "Cross-platform cryptography" tablosunu aktarıyorum. SHA-3 ve HMAC-SHA3 için Windows 11 build 25324 ve sonrası, KMAC için build 26016 ve sonrası, ChaCha20-Poly1305 için Windows 10 build 20142 ve sonrası gerekiyor; post-quantum algoritmalar için tablo, post-quantum destekli güncel Windows 11 sürümlerini gösteriyor. macOS'ta ise SHA-3, KMAC ve AES-CCM hiç yok (AES-CCM'in macOS'taki OpenSSL üzerinden çalışma yolu .NET 10'da kaldırıldı). Windows'ta kendi makinende aynı komutu dotnet run --project src/Bookstore.CryptoLab -- capabilities ile çalıştırıp sonucu bu tabloyla karşılaştırabilirsin.

Probe'un asıl kullanımı uygulama açılışı. Bookstore'un her ortamda olmazsa olmazı AES-GCM; bu yoksa ilk müşteri kaydında değil, process başlarken düşmek istiyoruz:

public static class CryptoRequirements
{
    // Bookstore'un hiçbir platformda vazgeçemeyeceği algoritmalar
    public static readonly string[] Bookstore = ["AES-GCM"];

    public static void EnsureSupported(IEnumerable<CryptoCapability> capabilities, IEnumerable<string> required)
    {
        HashSet<string> supported = capabilities.Where(c => c.IsSupported).Select(c => c.Name).ToHashSet();
        string[] missing = required.Where(name => !supported.Contains(name)).ToArray();
        if (missing.Length > 0)
        {
            throw new PlatformNotSupportedException(
                $"Missing cryptographic algorithms: {string.Join(", ", missing)}. Platform: {CryptoCapabilities.DescribePlatform()}");
        }
    }
}

Bunun testi, desteklenmeyen bir algoritmanın adının hata mesajında göründüğünü doğruluyor. Probe'u parametre olarak almak, testte "ML-KEM yok" durumunu container değiştirmeden kurmamızı sağlıyor:

[Fact]
public void Missing_algorithm_fails_at_startup_with_its_name()
{
    CryptoCapability[] capabilities = [new("AES-GCM", true), new("ML-KEM", false)];

    PlatformNotSupportedException ex = Assert.Throws<PlatformNotSupportedException>(
        () => CryptoRequirements.EnsureSupported(capabilities, ["AES-GCM", "ML-KEM"]));

    Assert.Contains("ML-KEM", ex.Message);
    Assert.DoesNotContain("AES-GCM,", ex.Message);
}

One-shot API'ler ve Eski Instance API'leri

Eski kodda en sık gördüğün biçim şu: SHA256.Create() ile bir instance oluşturmak, ComputeHash çağırmak, sonra dispose etmek. .NET 5 ile hash ve HMAC için static one-shot method'lar geldi: SHA256.HashData, HMACSHA256.HashData. Dokümantasyon bunların kullanımı daha kolay, daha az allocation yapan, thread-safe ve platformun en iyi implementation'ını seçen API'ler olduğunu söylüyor. Allocation farkını ölçelim. Örnek veri bir ISBN (International Standard Book Number; kitabın uluslararası kimlik numarası):

public static void HashApi()
{
    byte[] isbn = Encoding.UTF8.GetBytes("978-0441013593");
    const int Iterations = 100_000;

    // Eski stil: her seferinde instance oluştur, byte[] dönen ComputeHash çağır
    long before = GC.GetAllocatedBytesForCurrentThread();
    for (int i = 0; i < Iterations; i++)
    {
        using SHA256 sha = SHA256.Create();
        _ = sha.ComputeHash(isbn);
    }
    long instanceBytes = GC.GetAllocatedBytesForCurrentThread() - before;

    // One-shot: static method, sonuç stack'teki buffer'a yazılıyor
    before = GC.GetAllocatedBytesForCurrentThread();
    Span<byte> digest = stackalloc byte[SHA256.HashSizeInBytes];
    for (int i = 0; i < Iterations; i++)
    {
        SHA256.HashData(isbn, digest);
    }
    long oneShotBytes = GC.GetAllocatedBytesForCurrentThread() - before;

    Console.WriteLine($"instance + ComputeHash : {instanceBytes / Iterations,4} bytes/hash");
    Console.WriteLine($"SHA256.HashData (span) : {oneShotBytes / Iterations,4} bytes/hash");
    Console.WriteLine(Convert.ToHexStringLower(digest));
}
$ dotnet run -c Release --project src/Bookstore.CryptoLab -- hash-api
instance + ComputeHash :  248 bytes/hash
SHA256.HashData (span) :    0 bytes/hash
90a07a9c3ca9e1fe92d719e36c5e42469c23e099440d2b9333e940587e3cc6c0

Sonuç aynı hash; fark, her çağrıda 248 byte'a karşı sıfır allocation. Saniyede birkaç bin webhook doğrulayan bir endpoint'te bu, GC (garbage collector; kullanılmayan belleği toplayan runtime bileşeni) üzerinde doğrudan ek yük olarak görünür. Daha önemli fark thread safety: SHA256 ya da HMACSHA256 instance'ları thread-safe değil. Bir instance'ı singleton olarak DI (dependency injection; bağımlılıkların dışarıdan verilmesi) container'ına koyup paralel isteklerde paylaşan kod, yük altında hem exception fırlatır hem de, daha kötüsü, hiçbir hata vermeden yanlış hash üretir; bunu düşük trafikli test ortamında görmek çok zordur. One-shot method'larda paylaşılan bir durum olmadığı için bu hata sınıfı ortadan kalkıyor.

Instance API'ler yine de gerekli olduğu yerde duruyor: veri parça parça geliyorsa IncrementalHash, bir Stream'i hash'lerken SHA256.HashData(Stream) ya da HashDataAsync. AES'in eski, AEAD olmayan modlarında da artık CryptoStream yerine aes.EncryptCbc(...) gibi one-shot method'lar var. Kural şu: tek bir byte bloğu için one-shot, akış için incremental ya da stream overload'u; "her çağrıda Create()" ise yalnız eski kodda.

FixedTimeEquals ve ZeroMemory

System.Security.Cryptography.CryptographicOperations class'ında, serinin hemen her yazısında kullanacağımız iki method var.

FixedTimeEquals, iki byte dizisini karşılaştırırken harcadığı süreyi içeriğe göre değil yalnız uzunluğa göre belirliyor. Normal karşılaştırma (SequenceEqual, bir for döngüsü ya da hex string'lerde ==) ilk farklı byte'ta durur. Gelen bir MAC'i beklenen değerle böyle karşılaştırırsan, yanıt süresi beklenen değerin kaç byte'ının tuttuğu hakkında bilgi sızdırır. Ağ gürültüsü bu sızıntıyı zorlaştırır ama güvenliği gürültüye bırakmak istemiyoruz. Bookstore'da bunu tek bir yere sarıyoruz:

public static class Tags
{
    // Beklenen ile gelen tag'i ilk farklı byte'ta durmadan karşılaştırır
    public static bool AreEqual(ReadOnlySpan<byte> expected, ReadOnlySpan<byte> actual) =>
        CryptographicOperations.FixedTimeEquals(expected, actual);
}
[Fact]
public void One_flipped_bit_or_a_shorter_tag_does_not_match()
{
    byte[] tag = RandomNumberGenerator.GetBytes(32);
    byte[] flipped = tag.ToArray();
    flipped[31] ^= 0x01;

    Assert.False(Tags.AreEqual(tag, flipped));
    Assert.False(Tags.AreEqual(tag, tag.AsSpan(0, 16)));
}

İkinci assert önemli: uzunlukları farklı iki dizi her zaman false. Gelen imzayı kendi beklenen uzunluğuna göre kesip karşılaştıran kod, saldırgana kısa bir tag gönderme imkânı verir; karşılaştırmayı tam uzunlukta yap.

ZeroMemory ise bir buffer'ı sıfırlıyor ve derleyicinin ya da JIT'in (çalışma anında ara kodu makine koduna çeviren derleyici) "bu yazma işlemi sonradan okunmuyor" diye onu atlamasına izin vermiyor. Anahtar materyalini işin bitince bellekten silmek için kullanılır:

public static void Zeroize()
{
    // Pinned array: GC taşımaz, bellekte eski kopyası kalmaz
    byte[] key = GC.AllocateArray<byte>(32, pinned: true);
    RandomNumberGenerator.Fill(key);
    try
    {
        Console.WriteLine($"in use : {Convert.ToHexStringLower(key.AsSpan(0, 8))}...");
        // ... anahtarla şifreleme ya da imzalama burada
    }
    finally
    {
        CryptographicOperations.ZeroMemory(key);
    }

    Console.WriteLine($"cleared: {Convert.ToHexStringLower(key.AsSpan(0, 8))}...");
}
$ dotnet run --project src/Bookstore.CryptoLab -- zeroize
in use : 1f1bf24efdb0bd12...
cleared: 0000000000000000...

Beklentiyi doğru kur: bu bir sertleştirme adımı, garanti değil. GC normal bir array'i compaction sırasında taşıyabilir ve eski adreste bir kopya kalabilir; pinned array bunu engelliyor. string olarak tutulan bir secret ise immutable olduğu için hiç silinemez; anahtarları string yerine byte[] ya da Span<byte> olarak taşımanın bir sebebi de bu. AesGcm gibi class'lar anahtarı kendi içlerinde native tarafa kopyalar ve Dispose'da temizler; o yüzden bu nesneleri using ile kullan.

Bookstore'da Seri Boyunca Ne Yapacağız

Haritadaki her satırı Bookstore'da gerçek bir işe bağlıyoruz. Yazı sırası, bir önceki yazının aracını bir sonrakinde kullanacak şekilde kuruldu:

İkinci yazıda ödeme sağlayıcısından gelen webhook'un HMAC imzasını doğruluyor ve tek bir master key'den HKDF (HMAC tabanlı key derivation function) ile amaca özel alt anahtarlar türetiyoruz. Üçüncü yazıda Customer.Phone alanını AES-GCM ile şifreliyor, şifreli veriyi müşteri kaydına bağlıyor ve envelope encryption ile anahtar rotasyonunu kuruyoruz. Dördüncü yazıda tedarikçiye gönderilen satın alma siparişi dosyasını yalnız tedarikçinin açabileceği şekilde hybrid encryption ile şifreliyoruz. Beşinci yazıda okul ve kütüphane gibi kurumsal müşterilerin offline kurulumu için imzalı bir lisans dosyası üretiyor ve uygulama içinde public key ile doğruluyoruz.

Sonraki yazılarda X.509 sertifikalarını yükleyip chain doğrulamasını kodla yapıyor ve Bookstore.Api ile Ordering servisi arasına mTLS (mutual TLS; iki tarafın da sertifikayla kimliğini kanıtladığı TLS) kuruyoruz; ardından TLS'in key schedule ve record layer tarafına, post-quantum geçişe ve son olarak .NET Framework'ten bugüne kripto API'lerinin evrimine bakıyoruz.

Üretimde Dikkat

Base image bir kripto kararıdır. Ubuntu, Alpine, Azure Linux ya da chiseled image'lar farklı OpenSSL sürümleri taşır ve OpenSSL'in minor sürümü bir algoritmanın varlığını belirleyebiliyor. Base image'ı değiştiren her pull request'te capability çıktısını CI'da yazdır; yukarıdaki capabilities komutu bunun için yeterli. Aynı şekilde, Windows sunucularda desteği belirleyen şey .NET sürümü değil Windows build numarası.

Gerekli algoritma eksikse uygulama açılışta düşmeli, istek sırasında değil. EnsureSupported çağrısını Program.cs'te builder.Build()'den önce yapmak, Kubernetes'te yanlış image'la başlayan pod'un hiç ready olmamasını sağlar. Opsiyonel bir algoritma için ise (örneğin "varsa ML-KEM, yoksa klasik") seçimi açılışta bir kez yap ve logla; her istekte tekrar sorma.

FIPS uyumu gereken ortamlarda .NET'in kendi başına bir şey yapmadığını unutma: uyum, altındaki işletim sistemi kütüphanesinin FIPS modunda çalışmasından geliyor. FIPS modunda bazı algoritmalar, örneğin MD5, hata verir; bunu staging ortamında, production ile aynı image ve aynı OS ayarıyla test et.

Ölçüm tarafında, kripto çağrılarının süresini ayrı bir metrik olarak izlemek çoğu uygulamada gereksiz; ama allocation açısından sıcak yoldaki her Create() + ComputeHash çiftini one-shot'a çevirmek ucuz bir kazanç.

Sık Düşülen Tuzaklar

Birinci tuzak, platforma özel tipe bağlanmak. new RSACryptoServiceProvider() ya da new AesCng() ile yazılmış kod Windows'ta yıllarca çalışır, Linux container'ına taşındığı gün PlatformNotSupportedException fırlatır ya da daha kötüsü, eski ve sınırlı bir davranışla çalışır. Soyut tip ve factory method kullan.

İkincisi, IsSupported'u geliştirici makinesinde kontrol edip production'da kontrol etmemek. Senin Windows 11'inde SHA-3 vardır, müşterinin Windows Server'ında olmayabilir. Kontrolü kodda ve açılışta yap.

Üçüncüsü, MAC ya da imza karşılaştırmasını hex string'lerle yapmak: expectedHex == receivedHex. Hem süre sızdırır hem de büyük/küçük harf farkı gibi encoding sorunları doğurur. Gelen değeri byte'a çevir, FixedTimeEquals ile karşılaştır.

Dördüncüsü, kripto nesnelerini paylaşmak. HMACSHA256 instance'ını static field'da tutup birden çok thread'den kullanmak exception'lara ve, daha tehlikelisi, sessizce yanlış sonuçlara yol açar. One-shot method'u kullan ya da her işlem için yeni instance oluştur.

Beşincisi, hedefi primitive'le karıştırmak: bütünlük için düz hash, gizlilik için AEAD olmayan bir mod, inkâr edilemezlik için HMAC. Tablodaki satırı önce bul, sonra kodu yaz.

Küçük Bir Deneme

★☆☆ capabilities komutunu kendi Windows makinende çalıştır. SHA-3 ve ML-KEM satırları ne gösteriyor? Sonucu Windows build numaranla (winver) ve Microsoft Learn'deki tabloyla karşılaştır.

★★☆ CryptoRequirements.Bookstore listesine "ML-KEM" ekle ve uygulamayı Ubuntu image'ında çalıştır. Hata mesajı sana neyi, hangi platform bilgisiyle söylüyor? Sonra listeyi eski hâline getir ve "zorunlu" ile "opsiyonel" algoritmaları ayıran bir tasarım düşün: opsiyonel olanlar için açılışta seçilen algoritmayı loglayan bir method yaz.

★★★ HMACSHA256 instance'ını static bir field'da paylaşan ve Parallel.For ile 100.000 farklı mesajın MAC'ini hesaplayan bir komut yaz; sonuçları one-shot HMACSHA256.HashData ile karşılaştır ve kaç tanesinin yanlış çıktığını say. Sonra aynı testi one-shot method'la tekrarla.

Bir sonraki yazıda tablonun ilk üç satırına giriyoruz: hash'in hangi özellikleri garanti ettiğini, SHA256(secret + mesaj)'ın neden imza olmadığını, Bookstore'un ödeme webhook'unu HMAC ile doğrulamayı ve tek bir master key'den HKDF ile amaca özel anahtarlar türetmeyi ele alacağız.