Passkey KEK türetimi: deriveBits vs deriveKey

PayCal passkey oturumu, WebCrypto API kullanılarak bir Web Worker içinde türetilen key encryption key (KEK) üzerine kuruludur. Mayıs 2026’da bu türetimi deriveKey() yerine deriveBits() + importKey() kullanacak şekilde değiştirdik; amaç Safari / WebKit içinde bilinen bir hang yolundan kaçınmaktı. Bu makale değişikliği, iki yolun neden kriptografik olarak aynı olduğunu ve bunu nasıl doğruladığımızı açıklar.

Özet

Değişiklik tarihi 18 Mayıs 2026
Değişen dosya html/js/calendar/crypto-worker.js
Etkilenen fonksiyon derivePasskeyKEK()
Algoritma HKDF-SHA-256 → AES-GCM-256 (değişmedi)
Neden Safari / WebKit içindeki bilinen deriveKey() Worker hang yolundan kaçınmak
Kriptografik etki Yok. İki yol da aynı anahtar materyalini üretir.
Doğrulama Parite testi tests/crypto/hkdf-equivalence.mjs aynı decrypt davranışını doğrular

Arka plan: passkey KEK mimarisi

PayCal, kullanıcı başına verileri şifrelenmiş olarak saklanan ve hiçbir zaman düz metin olmayan bir data encryption key (DEK) ile korur. Login sırasında bunu çözmek için PayCal, kullanıcının passkey credential değerinden bir key encryption key (KEK) türetir.

Türetim, kriptografik işlemleri ana tarayıcı thread’inden izole eden sandboxed bir arka plan thread’i olan Web Worker içinde çalışır. Worker, HKDF key derivation adımını yapmak için tarayıcının WebCrypto API’sini (crypto.subtle) çağırır.

Türetimin girdisi, WebAuthn passkey credential içindeki kararlı credentialId değeridir; imza değildir. ECDSA imzaları deterministik değildir, bu yüzden imzadan türetilen bir KEK her login işleminde değişir ve DEK unwrap başarısız olur. credentialId kullanımı, kullanıcı aynı cihazla her authenticate olduğunda aynı KEK’in üretilmesini garanti eder.

HKDF parametreleri şunlardır:

  • Algoritma: HKDF-SHA-256
  • IKM: credentialId için UTF-8 encoding
  • Salt: Kullanıcı başına server-side saklanan rastgele 32-byte değer
  • Info: Sabit domain string paycal-passkey-kek
  • Çıktı: 256-bit AES-GCM anahtarı (non-extractable)

WebCrypto API: aynı anahtara iki yol

WebCrypto API, HKDF girdisinden türetilmiş anahtar üretmek için iki yol sunar:

crypto.subtle.deriveKey(algorithm, baseKey, derivedKeyType, extractable, usages)
HKDF türetimini yapar ve doğrudan bir CryptoKey nesnesi döndürür. Daha kompakt çağrıdır: çağıran taraf AES-GCM-256 gibi çıktı anahtarı algoritmasını tek adımda belirtir.
crypto.subtle.deriveBits(algorithm, baseKey, length) + crypto.subtle.importKey(format, keyData, algorithm, extractable, usages)
HKDF türetimini yapar ve önce raw bytes döndürür. Çağıran taraf sonra bu byte’ları ikinci bir importKey çağrısıyla CryptoKey olarak import eder. Bu iki adımlı eşdeğerdir: oluşan anahtar nesnesi tür, uzunluk, extractability ve izinli usages bakımından aynıdır.

İki yol da aynı HKDF-SHA-256 fonksiyonunu çağırır, aynı IKM, salt ve info parametrelerini kullanır ve aynı 256 bit anahtar materyalini üretir. Tek fark, Web Worker runtime’ın çağrıyı dahili olarak nasıl işlediğidir.

Sorun: Safari / WebKit Web Workers içinde gözlenen deriveKey() hang

Safari / WebKit içinde çağrı bir Web Worker’dan yapıldığında operasyonel olarak gözlenen bir deriveKey() hang yolu vardır. Safari kullanıcıları, Worker türetimi tamamlasın diye beklerken hata, timeout veya görünür geri bildirim olmadan duran bir login akışı yaşayabiliyordu.

Aynı çıktıyı üretmek için kullanılan deriveBits() çağrısı bu hang davranışını göstermez. Sorun Worker execution context içindeki deriveKey() code path’e özgüdür; alttaki HKDF algoritmasını etkilemez.

deriveKey() ve deriveBits(), Apple tarafından belgelenen ve W3C tarafından belirtilen birinci sınıf WebCrypto API’leridir. Bu use case için semantikleri aynıdır. Hang bir runtime regression’dır, iki çağrı arasında tasarım farkı değildir.

Bu exact Worker + HKDF + deriveKey() kombinasyonu için public indexed canonical Apple bug ticket yoktur. Bu durum, operasyonel olarak keşfedilen, production’da workaround edilen ve public ticket ile resmi olarak izlenmeyen Safari/WebKit edge-case crypto regression sınıfıyla tutarlıdır. Engineering kararımız, WebKit sürümünden bağımsız olarak problemli çağrı yolundan tamamen kaçınmak ve runtime’da browser state tespiti yapmamaktır.

Ne değiştirdik

html/js/calendar/crypto-worker.js içindeki derivePasskeyKEK() fonksiyonu, tek türetim yolu olarak deriveBits() + importKey() kullanacak şekilde güncellendi. deriveKey() çağrısı tamamen kaldırıldı.

Yeni türetim sırası:

  1. IKM byte’larını HKDF base key olarak importKey('raw', ikmMaterial, 'HKDF', false, ['deriveBits']) ile import et
  2. deriveBits(hkdfParams, ikm, 256) ile 256 raw bit türet
  3. Bu bitleri AES-GCM-256 anahtarı olarak importKey('raw', keyBits, { name: 'AES-GCM', length: 256 }, false, ['encrypt', 'decrypt']) ile import et

HKDF parametreleri (SHA-256 hash, kullanıcı başına salt, sabit info string paycal-passkey-kek) değişmedi. Çıktı anahtar türü, uzunluğu ve non-extractable flag değişmedi. Eski code path’i daha önce kullanan kullanıcılar için stored wrapped DEKs tamamen uyumludur: re-wrapping veya re-enrollment gerekmez.

Kriptografik eşdeğerlik kanıtı

Eşdeğerliği yalnızca akıl yürütmeyle iddia etmiyoruz. tests/crypto/hkdf-equivalence.mjs dosyası şu parite testini içerir:

  1. Aynı HKDF parametreleriyle Yol A (deriveKey()) üzerinden anahtar türetir
  2. Aynı HKDF parametreleriyle Yol B (deriveBits() + importKey()) üzerinden anahtar türetir
  3. Yol A anahtarıyla test payload’ını encrypt eder
  4. Bu ciphertext’i Yol B anahtarıyla decrypt eder
  5. Decrypted bytes değerlerinin original plaintext ile birebir aynı olduğunu doğrular
  6. Tersini tekrarlar: Yol B ile encrypt, Yol A ile decrypt

İki decrypt işlemi de başarılı olur ve aynı byte’ları üretirse anahtarlar operasyonel olarak aynıdır. Test Node.js 18+ üzerinde geçer ve SOC 2 false-positive adjudication ledger içinde CRYPTO-001 evidence item olarak referanslanır.

Ne değişmedi

  • Anahtar türetme algoritması hâlâ HKDF-SHA-256
  • Çıktı anahtarı hâlâ 256-bit AES-GCM
  • Çıktı anahtarı Worker context içinden hâlâ non-extractable
  • IKM kaynağı hâlâ kararlı credentialId (ECDSA imzası değil)
  • HKDF info string hâlâ paycal-passkey-kek
  • Kullanıcı başına salts değişmedi
  • Kullanıcı aksiyonu, re-enrollment veya credential rotation gerekmez
  • Eski KEK altında wrapped data encryption key (DEK), yeni KEK ile tamamen uyumludur: byte’lar aynı anahtardır

Bunu neden yayınlıyoruz

Bu değişiklik güvenliği etkilemez. Ancak kodumuzun belirli bir browser engine üzerindeki davranışını etkiler ve client-side cryptography stack’imizin en güvenlik hassas kısmına dokunur.

Bunu yayınlıyoruz çünkü transparency policy’miz, key derivation logic içindeki her değişikliğin, davranış olarak nötr olsa bile, teknik gerekçe ve verification evidence ile birlikte kullanıcılar ve auditorlar tarafından incelenebilir şekilde public olarak belgelenmesini gerektirir.

Kriptografik değişiklikler, kasıtlı olarak inert olsalar bile asla görünmez olmamalıdır.

Referanslar