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

.NET Framework'ten .NET 11'e Kriptografi API'leri: Obsolete Tipler, Yeni One-shot API'ler ve Eski Kodu Okumak

Mert Özen 14 Kas 2026 11 dk 1 görüntülenme
.NET Framework'ten .NET 11'e Kriptografi API'leri: Obsolete Tipler, Yeni One-shot API'ler ve Eski Kodu Okumak

Eski kripto kodunu yeni API'lere taşırken uyarıları düzeltmek yetmez; eski constructor'ların gizli varsayılanlarını (SHA-1, 1000 iterasyon, CBC) açıkça yazmazsan arşivdeki veri açılmaz. SYSLIB0021'den SYSLIB0060'a uyarıları, davranışı koruyan taşımayı, .NET 5'ten 10'a gelen API'leri ve .NET 11'in X25519, RFC 3394 AES Key Wrap, HMACSHA256.Verify, HPKE ve composite ML-KEM eklemelerini gerçek çıktılarla gösteriyoruz.

Sürüm notu: Bu yazıdaki kodlar .NET 10 ve xUnit v3 4.0 ile, .NET 11 örneği .NET 11 SDK 11.0.100 ile derlenip denenmiştir; çı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

Sitedeki serilerin ortak örnek uygulaması Bookstore (bir online kitapçı), müşteri listesini muhasebe firmasına yıllarca .NET Framework 4.x ile yazılmış küçük bir export aracıyla gönderdi. Araç .NET 10'a taşındı, derleniyor ve çalışıyor; yalnız beş uyarı veriyor. Ekipten biri uyarıları NoWarn ile susturmayı, bir diğeri ise uyarıların önerdiği yeni API'lere geçmeyi öneriyor. İkinci yol daha temiz görünüyor, ama bir soru var: arşivde yıllar önce şifrelenmiş export dosyaları duruyor. API'yi değiştirince o dosyalar hâlâ açılır mı?

Açılır, ama yalnız eski kodun gizli varsayılanlarını bilerek taşırsan. Bu yazı serinin son yazısı: eski kripto kodunu okumayı, obsolete tiplerin uyarılarını, davranışı koruyarak taşımayı, .NET 5'ten 10'a gelen API'leri ve .NET 11 ile gelenleri gerçek derleme ve çalıştırma çıktılarıyla topluyoruz.

Eski Kodda Ne Görürsün?

Export aracının .NET Framework döneminden kalma hâli, kodun samples/Bookstore.LegacyCrypto klasöründe. Kısaltılmış hâli şöyle:

public static byte[] Export(string password, string csv)
{
    byte[] salt = new byte[16];
    using (RNGCryptoServiceProvider rng = new())
    {
        rng.GetBytes(salt);
    }

    using Rfc2898DeriveBytes kdf = new(password, salt);
    using RijndaelManaged aes = new() { KeySize = 256 };
    aes.Key = kdf.GetBytes(32);
    aes.GenerateIV();
    using ICryptoTransform encryptor = aes.CreateEncryptor();
    byte[] data = Encoding.UTF8.GetBytes(csv);
    byte[] ciphertext = encryptor.TransformFinalBlock(data, 0, data.Length);
    return [.. salt, .. aes.IV, .. ciphertext];
}

Bu proje uyarıları bilerek gösteriyor ve bu yüzden Bookstore.slnx'e eklenmedi; serinin diğer projelerinde TreatWarningsAsErrors açık olduğu için aynı kod orada derlenmezdi:

$ dotnet build samples/Bookstore.LegacyCrypto
LegacyExportTool.cs(13,16): warning SYSLIB0023: 'RNGCryptoServiceProvider' is obsolete: 'RNGCryptoServiceProvider is obsolete. To generate a random number, use one of the RandomNumberGenerator static methods instead.' (https://aka.ms/dotnet-warnings/SYSLIB0023)
LegacyExportTool.cs(18,40): warning SYSLIB0060: 'Rfc2898DeriveBytes.Rfc2898DeriveBytes(string, byte[])' is obsolete: 'The constructors on Rfc2898DeriveBytes are obsolete. Use the static Pbkdf2 method instead.' (https://aka.ms/dotnet-warnings/SYSLIB0060)
LegacyExportTool.cs(19,15): warning SYSLIB0022: 'RijndaelManaged' is obsolete: 'The Rijndael and RijndaelManaged types are obsolete. Use Aes instead.' (https://aka.ms/dotnet-warnings/SYSLIB0022)
LegacyExportTool.cs(30,15): warning SYSLIB0021: 'SHA256Managed' is obsolete: 'Derived cryptographic types are obsolete. Use the Create method on the base type instead.' (https://aka.ms/dotnet-warnings/SYSLIB0021)
LegacyExportTool.cs(34,91): warning SYSLIB0057: 'X509Certificate2.X509Certificate2(byte[], string?)' is obsolete: 'Loading certificate data through the constructor or Import is obsolete. Use X509CertificateLoader instead to load certificates.' (https://aka.ms/dotnet-warnings/SYSLIB0057)

Bir ayrıntı: eski kaynaklarda Rfc2898DeriveBytes constructor'ı için SYSLIB0041 geçer. O uyarı .NET 7'de yalnız varsayılan hash ve iterasyon kullanan constructor'ları işaretliyordu; .NET 10 bütün constructor'ları SYSLIB0060 ile obsolete yaptı ve .NET 10 runtime'ında SYSLIB0041 artık yok. Uyarı numaralarını hafızadan değil, derleyicinin çıktısından oku. Eski tiplerin karşılıkları:

Eski kodUyarıBugünkü karşılığı
SHA256Managed, *CryptoServiceProvider türevleriSYSLIB0021SHA256.HashData, Aes.Create()
RijndaelManagedSYSLIB0022Aes.Create(), tek seferlik EncryptCbc/DecryptCbc
RNGCryptoServiceProviderSYSLIB0023RandomNumberGenerator.GetBytes
new Rfc2898DeriveBytes(...)SYSLIB0060Rfc2898DeriveBytes.Pbkdf2
new X509Certificate2(byte[])SYSLIB0057X509CertificateLoader.LoadCertificate, LoadPkcs12
new AesGcm(key)SYSLIB0053new AesGcm(key, tagSizeInBytes)

Davranışı Koruyarak Taşımak

Uyarıları düzeltmenin tuzağı şurada: eski kodda yazılmamış ama kullanılmış değerler var. new Rfc2898DeriveBytes(password, salt) hash olarak SHA (Secure Hash Algorithm; standart hash algoritmalarının ortak adı) ailesinden SHA-1, iterasyon olarak 1000 kullanıyor; RijndaelManaged varsayılan olarak CBC (Cipher Block Chaining; blokları zincirleyen eski şifreleme modu) modu ve PKCS (Public-Key Cryptography Standards; public key kriptografisi için standart dizisi) #7 padding. Yeni API'ye geçerken bunları açıkça yazmazsan, arşivdeki dosyalar açılmaz. Okuma yolu için ayrı, küçük bir class yazdık:

// Eski .NET Framework export aracının yazdığı dosyaları okur:
// salt(16) | iv(16) | AES-256-CBC ciphertext, anahtar = PBKDF2-HMAC-SHA1, 1000 iterasyon
// (eski Rfc2898DeriveBytes(password, salt) constructor'ının varsayılanları).
// Yalnız okuma yolu: bu formatta yeni hiçbir şey yazılmaz.
public static class LegacyExportCipher
{
    private const int LegacyIterations = 1000;

    public static byte[] Decrypt(string password, ReadOnlySpan<byte> file)
    {
        ReadOnlySpan<byte> salt = file[..16];
        ReadOnlySpan<byte> iv = file[16..32];
        byte[] key = Rfc2898DeriveBytes.Pbkdf2(password, salt, LegacyIterations, HashAlgorithmName.SHA1, 32);
        try
        {
            using Aes aes = Aes.Create();
            aes.Key = key;
            return aes.DecryptCbc(file[32..], iv);
        }
        finally
        {
            CryptographicOperations.ZeroMemory(key);
        }
    }
}

SHA-1 ve 1000 iterasyon bugün bir parola için kabul edilebilir değerler değil; parola saklamadaki güncel değerler Web Güvenliği (OWASP Top 10) serisinin "OWASP A04: Cryptographic Failures (2)" yazısında. Ama burada amaç yeni veri korumak değil, eski veriyi bir kez okuyup daha iyi bir formata taşımak. Sample bunu baştan sona yapıyor: eski yoldan bir dosya üretiyor, yeni API'lerle okuyor ve telefon numaralarını üçüncü yazıdaki sürümlü formata, yani AES (standart blok şifre) ve GCM (kimlik doğrulamalı şifreleme modu) kullanan formata yeniden şifreliyor:

$ dotnet run --project samples/Bookstore.LegacyCrypto
legacy export: 128 bytes, checksum BCDE64A17D5A59F7...
read with Rfc2898DeriveBytes.Pbkdf2 + Aes.DecryptCbc: 2 customers, first: 1001;Ada Lovelace;+90 555 010 1001
SHA256Managed == SHA256.HashData: True
customer 1001: v1 keyId 1, AQHBmiA49dcw2716IYp7...
customer 1002: v1 keyId 1, AQGpeF3g+h13bzm3n344...

Bu eşdeğerliği bir testle kilitledik: test eski API'lerle, uyarıları yalnız o satırlar için #pragma ile kapatarak bir dosya üretiyor ve LegacyExportCipher'ın onu okuduğunu doğruluyor. Eski formatın bir zayıflığını da testte not ettik: CBC kimlik doğrulama yapmadığı için yanlış parola çoğu zaman padding hatası veriyor, ama bazen anlamsız bir metin döndürebiliyor. Yeni formatta AES-GCM'in tag'i bu belirsizliği ortadan kaldırıyor.

Taşıyamayacağın bir durum da var: eski kodda RijndaelManaged'in BlockSize'ı 256 yapılmışsa bu AES değil. .NET 10'da denediğimde PlatformNotSupportedException: BlockSize must be 128 in this implementation. aldım; böyle bir arşivi okumak için ya eski bir .NET Framework ortamında bir kez açıp taşıman ya da üçüncü parti bir kütüphane kullanman gerekiyor.

Sürüm Sürüm Kripto API'leri

Eski kodu okurken "bu API ne zaman geldi" sorusu çok çıkıyor. Aşağıdaki tabloyu Microsoft Learn'deki "what's new" sayfalarından ve elimdeki .NET 6, 7, 9, 10 ve 11 runtime'larının kripto assembly'lerinde ilgili tip ve uyarı adlarını arayarak doğruladım:

SürümGelenler
.NET 5 ve 6Tek seferlik static API'ler: SHA256.HashData, HKDF, Rfc2898DeriveBytes.Pbkdf2, Aes.EncryptCbc, RandomNumberGenerator.GetBytes(int); SYSLIB0021, 0022, 0023
.NET 7SYSLIB0041: varsayılan hash ve iterasyonlu Rfc2898DeriveBytes constructor'ları
.NET 8SHA-3 ailesi (SHA3_256.IsSupported), AesGcm'de tag boyutu zorunluluğu (SYSLIB0053)
.NET 9KMAC (Keccak tabanlı, anahtarlı doğrulama kodu), X509CertificateLoader ve SYSLIB0057
.NET 10Post-quantum key encapsulation (ML-KEM), hash tabanlı imza (SLH-DSA), ML-DSA ve composite ML-DSA; AES-KWP; PemEncoding.FindUtf8; PKCS#12 export algoritması; SHA-256 thumbprint ile arama; SYSLIB0058 ve SYSLIB0060
.NET 11X25519DiffieHellman, padding'siz AES Key Wrap, HMACSHA256.Verify, HPKE (Hybrid Public Key Encryption; standartlaşmış hybrid şifreleme şeması) ve composite ML-KEM (deneysel), SYSLIB0064 ve SYSLIB0065

Bir runtime'da bir adın geçmesi, onun o sürümde public API olarak geldiğinin tek başına kanıtı değil; bu yüzden her satırı Learn'deki sürüm notlarıyla birlikte okudum. .NET 5'i elimde runtime olarak denemediğim için ilk satırı bilerek ".NET 5 ve 6" diye birleştirdim.

.NET 11 ile Gelenler

.NET 11, 10 Kasım 2026'da çıktı; seri .NET 10 (LTS) üzerinde kaldığı için .NET 11 örneği ayrı bir projede: samples/Bookstore.Net11Crypto, net11.0 hedefliyor, .NET 11 SDK gerektiriyor ve kökteki dotnet build ile dotnet test .NET 10 SDK ile çalışmaya devam etsin diye solution'da değil. Bütün denemeleri tek bir programda topladık, .NET 11 SDK ile derledik ve Alpine 3.24 container'ında .NET 11 runtime'ıyla çalıştırdık:

$ dotnet build samples/Bookstore.Net11Crypto
$ dotnet samples/Bookstore.Net11Crypto/bin/Debug/net11.0/Bookstore.Net11Crypto.dll
.NET 11.0.0, Alpine Linux v3.24
-- X25519DiffieHellman.IsSupported: True
public key 32 B (SPKI 44 B), shared secret 32 B, same on both sides: True
-- AES-KW: 1FA68B0A8112B447AEF34BD8FB5A7B829D3E862371D2CFE5 (RFC 3394 4.1: True)
19-byte key, padded (RFC 5649): 32 B
19-byte key, unpadded: ArgumentException
-- HMACSHA256.Verify: True, one byte changed: False
FixedTimeEquals(span, byte): [0x42] vs 0x42 True, [0x42, 0x42] vs 0x42 True, [0x41] vs 0x42 False
-- HPKE DHKEM_X25519_HKDF_SHA256: encapsulated 32 B, ciphertext 101 B for 85 B, opened: True
-- HPKE MLKEM_768: not supported here
-- HPKE MLKEM768_P256: not supported here
-- MLKEM768-X25519-SHA3-256: encapsulation key 1216 B, ciphertext 1120 B, secret 32 B, decapsulated equal: True

Satır satır:

X25519DiffieHellman, serinin dördüncü ve beşinci yazılarında "son yazıda" diye ertelediğimiz class. Curve25519 üzerinde key agreement artık BCL'de (Base Class Library; .NET'in temel class kütüphanesi); public key 32 byte, P-256'nın SPKI (public key'in standart ikili yapısı) biçimindeki 91 byte'ıyla karşılaştır. Ed25519 imzası ise .NET 11'de de yok; .NET 11 runtime'ındaki public tipleri reflection ile yeniden taradım ve Ed25519 ya da EdDSA adında bir tip bulamadım.

AES Key Wrap'in padding'siz sürümü RFC (Request for Comments; internet standartlarının yayımlandığı belge serisi) 3394'ü uyguluyor ve çıktı RFC'nin 4.1 test vektörüyle birebir aynı. Padding'siz sürüm 8'in katı olmayan bir anahtarı ArgumentException ile reddediyor; üçüncü yazıda kullandığımız padding'li AES-KWP aynı 19 byte'ı 32 byte'a sarıyor. Bir HSM (Hardware Security Module; anahtarı dışarı vermeden işlem yapan donanım) ya da başka bir sistem RFC 3394 bekliyorsa artık üçüncü parti koda gerek yok.

HMACSHA256.Verify, ikinci yazıdaki webhook doğrulamasını tek satıra indiriyor ve karşılaştırmayı sabit zamanda yapıyor. FixedTimeEquals'ın yeni overload'u ise bir span'in bütün byte'larının verilen değere eşit olup olmadığını sabit zamanda kontrol ediyor; çıktıdaki iki byte'lık satır bunu gösteriyor.

Hpke, RFC 9180'deki HPKE'yi uyguluyor ve SYSLIB5009 ile deneysel. Dördüncü yazıda sipariş dosyası için kendi envelope'umuzu kurmuştuk; HPKE aynı işi standart bir formatla yapıyor ve 85 byte'lık sipariş için yalnız 32 byte'lık bir kapsül ve 16 byte'lık tag ekliyor. Enum'da ML-KEM tabanlı KEM'ler (key encapsulation mechanism; anahtar kapsülleme algoritması) de var, ama bu ortamda Hpke.IsSupported bunların hiçbir KDF (anahtar türetme fonksiyonu) ve AEAD (kimlik doğrulamalı şifreleme) kombinasyonu için true dönmedi. Bu yüzden sekizinci yazıdaki hybrid envelope şimdilik yerinde kalıyor.

CompositeMLKem, ML-KEM'i klasik bir key agreement ile tek bir KEM'de birleştiriyor; SYSLIB5006 ile deneysel. Sekizinci yazıdaki hybrid envelope'un yaptığını standart bir yapı olarak sunuyor: 1216 byte'lık anahtar, TLS'teki (HTTPS'in altındaki şifreli bağlantı protokolü) X25519MLKEM768 key share'iyle aynı boyutta.

Çıktıda görünmeyen ama bilmen gereken değişiklikler de var. .NET 10'da tamamen deneysel olan MLDsa, .NET 11'de stabil; yalnız pre-hash ve external mu method'ları hâlâ deneysel. MLKem'in SPKI method'ları da artık deneysel değil. SlhDsa ve CompositeMLDsa deneysel kalıyor; bunu .NET 11 SDK ile reflection'la kontrol ettim. Microsoft Learn'e göre EnvelopedCms ML-KEM alıcılarıyla şifreleyebiliyor, ama yalnız Windows dışındaki platformlarda; bunu denemedim. Learn'deki bir uyumluluk notuna göre .NET 11'de SslStream, sunucu rolünde client certificate doğrularken AIA (Authority Information Access; eksik intermediate'ın indirileceği adres) ile sertifika indirmeyi varsayılan olarak kapatıyor; altıncı yazıdaki doğrulayıcımız bunu zaten DisableCertificateDownloads ile yapıyordu. Yeni obsoletion'lar da var: SYSLIB0064 RSACryptoServiceProvider.Encrypt(byte[], bool) overload'unu, SYSLIB0065 ise AsnEncodedData.RawData'nın setter'ını işaretliyor.

Taşıma Checklist'i

Bookstore'un export aracını taşırken izlediğimiz sıra, başka eski kod için de iş görüyor:

  • Önce envanter: kodda hangi algoritma, hangi mod, hangi iterasyon ve hangi anahtar kaynağı kullanılıyor; varsayılanları da yaz.
  • Uyarıları susturma, çöz; bastırman gerekiyorsa #pragma ile yalnız o satırda yap, projenin tamamında NoWarn kullanma.
  • Eski formatı okuyan kodu ayrı bir class'a al, yalnız okuma yolunda kullan ve eşdeğerliği eski API ile üretilmiş veriyle test et.
  • Yeni veriyi sürüm alanı ve keyId taşıyan bir formatla yaz; eski kayıtları arka planda yeniden şifrele ve bitince eski okuma yolunu kaldır.
  • Platform yeteneğini açılışta kontrol et ve CI'da hem Windows'u hem de üretimdeki Linux image'ını çalıştır.

Üretimde Dikkat

Eski bir formatı okuyan kod sonsuza kadar yaşamamalı. Yeniden şifreleme işinin ilerlemesini ölç: kaç kayıt eski formatta kaldı, kaçı taşındı. Sayı sıfıra indiğinde LegacyExportCipher'ı ve ona bağlı testleri sil; aksi hâlde SHA-1 ve 1000 iterasyonla şifrelenmiş yeni bir dosyanın sisteme girebileceği bir kapı açık kalır.

.NET 11'in deneysel API'lerini production'a almadan önce .NET 12'ye kadar değişebileceklerini hesaba kat. Hpke ve CompositeMLKem'i kendi küçük arayüzünün arkasına koy; sekizinci yazıdaki EnvelopeReader gibi algoritma adıyla seçim yapan bir yapı, kendi envelope'umuzdan HPKE'ye geçişi de bir izin listesi değişikliğine indiriyor.

LTS ile STS (Standard Term Support; daha kısa destek süreli sürüm) arasındaki seçimi kripto API'leri belirlememeli. .NET 11'e yalnız X25519DiffieHellman için geçmek yerine, ona ihtiyacın varsa o kodu ayrı bir servise ya da ayrı bir projeye almak, ana uygulamanın LTS'te kalmasını sağlar.

Sık Düşülen Tuzaklar

Birinci tuzak, Önce Tahmin Et'teki: uyarıyı düzeltirken eski kodun varsayılanlarını kaybetmek. Rfc2898DeriveBytes.Pbkdf2'ye SHA-256 ve 600.000 iterasyon verirsen kodun "modern" görünür ama arşivdeki hiçbir dosya açılmaz.

İkincisi, tersine bir hata: eski parametreleri yeni veri için de kullanmaya devam etmek. Okuma yolu eski parametreleri bilir, yazma yolu bilmez.

Üçüncüsü, NoWarn ile SYSLIB (.NET'teki obsolete API uyarılarının ön eki) uyarılarını projenin tamamında kapatmak. Böylece ileride gelen yeni bir obsoletion da sessizce kaybolur; .NET 11'deki SYSLIB0064 gibi.

Dördüncüsü, "kripto kodu" sayılmayan yerleri unutmak: sertifika yükleme, TLS sürüm ayarı, HTTP client'ın doğrulama callback'i. Serinin altıncı ve yedinci yazılarındaki ayarlar da bu envanterin parçası.

Beşincisi, bir API'nin sürümünü hafızadan söylemek. Eski kaynaklardaki SYSLIB0041'i beklerken derleyici SYSLIB0060 verdi; doğru cevap derleyicinin ve sürüm notlarının elinde.

Küçük Bir Deneme

★☆☆ Legacy sample'a new AesGcm(key) kullanan bir method ekle ve SYSLIB0053 uyarısını gör. Sonra new AesGcm(key, 16) ile düzelt.

★★☆ LegacyExportCipher için bir "yeniden şifreleme" job'ı yaz: bir klasördeki eski export dosyalarını okuyup her satırı FieldEncryptor formatına çevirsin, ilerlemeyi saysın ve her dosyayı yalnız başarılı taşımadan sonra silsin. Yarıda kesilen bir koşunun tekrar çalıştırıldığında veri kaybetmediğini test et.

★★★ samples/Bookstore.Net11Crypto'ya HPKE ile çalışan bir sipariş envelope'u ekle ve sekizinci yazıdaki EnvelopeReader'a yeni bir algoritma adı olarak bağla. Ardından aynı siparişi üç yöntemle, yani ECDH (eliptik eğri üzerinde anahtar anlaşması) envelope'u, hybrid ML-KEM envelope'u ve HPKE ile şifreleyip boyutlarını karşılaştır.

Serinin sonuna geldik. Dokuz yazıda doğru primitive'i doğru çağırmayı gördük: hash, HMAC (anahtarlı hash ile üretilen doğrulama kodu) ve HKDF (HMAC tabanlı anahtar türetme); AES-GCM ile alan şifreleme ve envelope encryption; asymmetric şifreleme ve hybrid encryption; digital signature ve imzalı lisans; X.509 ve mTLS; TLS'in içi; post-quantum geçiş ve eski kodu taşımak. Hepsinin ortak dersi aynı: algoritmayı kendin yazma, ama ne çağırdığını, hangi varsayılanla çağırdığını ve bir gün nasıl değiştireceğini bil.