WebCrypto ile verileri imzalama ve doğrulama

Web özellikleri: Web Cryptography

Kuantuma dayanıklı (ML-DSA) veya klasik ('Ed25519', 'ECDSA') asimetrik anahtarlar kullanarak tarayıcıda mesajları, jetonları veya yapıları imzalamak ve doğrulamak için Web Cryptography API'yi (crypto.subtle) kullanın. Dijital imzalar, paylaşılan gizli anahtarlara gerek kalmadan veri bütünlüğünü ve imzalayanın kimliğini garanti eder. Paylaşılan gizli anahtarlar oluşturma ve gizli yükleri şifreleme hakkında bilgi edinmek için WebCrypto ile istemci tarafında verileri şifreleme başlıklı makaleyi inceleyin.

1. Algoritma seçme ve anahtar çifti oluşturma

ML-DSA (NIST FIPS 204'te standartlaştırılmış Modül Kafes Tabanlı Dijital İmza Algoritması), hem klasik hem de kuantum bilgisayarların saldırılarına karşı dayanıklı, kuantum sonrası kafes tabanlı dijital imzalar sağlar.

Genel amaçlı imzalama için varsayılan olarak 'ML-DSA-65' kullanın. Bant genişliği kısıtlaması olan kanallarda daha küçük ortak anahtarlara ve imzalara ihtiyacınız varsa ve 128 bit güvenlik yeterliyse 'ML-DSA-44''i, güvenlik politikası NIST Kategori 5 (CNSA 2.0) uyumluluğu gerektiriyorsa 'ML-DSA-87''i seçin.

Algoritma adı Güvenlik seviyesi ve kullanım alanı Ortak anahtar ('raw-public') İmza boyutu
'ML-DSA-44' NIST Kategori 2 (~128 bit güvenlik); en küçük anahtarlar ve imzalar 1312 bayt 2.420 bayt
'ML-DSA-65' NIST Kategori 3 (~192 bit güvenlik); önerilen varsayılan 1.952 bayt 3309 bayt
'ML-DSA-87' NIST Kategori 5 (~256 bit güvenlik); en yüksek güvenlik marjı 2592 bayt 4.627 bayt
  • Algoritma adı olarak 'ML-DSA-44', 'ML-DSA-65' veya 'ML-DSA-87' değerini iletin ('Dilithium' veya 'CRYSTALS-Dilithium' gibi standartlaştırma öncesi adlar NotSupportedError hatasına neden olur).
  • ML-DSA ve 'Ed25519', hash parametresi almaz.
  • generateKey() ile anahtar çifti oluştururken ['sign', 'verify'] parametresini iletin:
const signerKeyPair = await crypto.subtle.generateKey(
  'ML-DSA-65',
  false,
  ['sign', 'verify'],
);

2. Anahtarları dışa ve içe aktarma

'ML-DSA-44', 'ML-DSA-65' ve 'ML-DSA-87', importKey() ve exportKey()'te eski 'raw' biçimini reddeder ve NotSupportedError döndürür. Anahtarları her zaman açık bir biçim kullanarak dışa veya içe aktarın:

  • 'raw-public': Ortak anahtar baytları (1312, 1952 veya 2592 bayt). Ortak anahtar içe aktarılırken anahtar kullanımı olarak ['verify'] değerini iletin.
  • 'raw-seed': Üç ML-DSA algoritmasının tümü için 32 baytlık özel anahtar çekirdeği. Hem 'raw' hem de 'raw-private', ML-DSA'da NotSupportedError hatası verir. Özel anahtarları 'raw-seed', 'pkcs8' (54 bayt) veya 'jwk' (kty: 'AKP') kullanarak depolayın ya da iletin. Özel anahtar içe aktarırken anahtar kullanımı olarak ['sign'] değerini iletin (importKey() değerine ['sign', 'verify'] iletildiğinde SyntaxError hatası verilir).
  • 'spki', 'pkcs8' ve 'jwk': Üç ML-DSA algoritması için de desteklenir.

Ortak anahtar baytlarının serileştirilebilmesi veya doğrulayıcılarla paylaşılabilmesi için doğrulama ortak anahtarını her zaman 'raw-public' ile dışa aktarın:

// Export the public key bytes on the signer
const publicKeyBytes = await crypto.subtle.exportKey(
  'raw-public',
  signerKeyPair.publicKey,
);

// Import the public key bytes on the verifier (usage must be ['verify'] only)
const verifierPublicKey = await crypto.subtle.importKey(
  'raw-public',
  publicKeyBytes,
  'ML-DSA-65',
  true,
  ['verify'],
);

Yalnızca özel bir imzalama CryptoKey anahtarınız varsa (extractable: false ile bile) ve buna karşılık gelen herkese açık CryptoKey anahtarına ihtiyacınız varsa ['verify'] ile crypto.subtle.getPublicKey() komutunu çağırın:

const publicKey = await crypto.subtle.getPublicKey(
  signerKeyPair.privateKey,
  ['verify'],
);

3. Yükleri imzalama ve doğrulama

İmza oluşturmak için özel anahtarla crypto.subtle.sign(), imzayı kontrol etmek için ortak anahtarla crypto.subtle.verify() yöntemini çağırın:

const payloadBytes = new TextEncoder().encode(
  '{"artifact":"release-v2.4.0.tar.gz","sha256":"9f86d08..."}',
);

const signatureBuffer = await crypto.subtle.sign(
  'ML-DSA-65',
  signerKeyPair.privateKey,
  payloadBytes,
);

const isValid = await crypto.subtle.verify(
  'ML-DSA-65',
  verifierPublicKey,
  signatureBuffer,
  payloadBytes,
);

ContextParams ile alan adı ayırma

Bir amaç için oluşturulan imzanın, uygulamanızın veya protokolünüzün aynı anahtar çiftini paylaşan başka bir bölümünde yeniden kullanılmasını önlemek için imzayı bir alan adı ayırma context değerine bağlayın.

Yalnızca algoritma adı dizesi yerine sign() ve verify()'ye bir ContextParams sözlüğü ({ name, context }) iletin. Burada context, bir BufferSource (Uint8Array veya ArrayBuffer) olur:

const contextBytes = new TextEncoder().encode('release-manifest-v1');

const signatureBuffer = await crypto.subtle.sign(
  {
    name: 'ML-DSA-65',
    context: contextBytes,
  },
  signerKeyPair.privateKey,
  payloadBytes,
);

const isValid = await crypto.subtle.verify(
  {
    name: 'ML-DSA-65',
    context: contextBytes,
  },
  verifierPublicKey,
  signatureBuffer,
  payloadBytes,
);
  • 255 bayt sınırı: context, 0 ila 255 bayt kabul eder (context'nin atlanması, boş bir new Uint8Array(0) iletilmesine eşdeğerdir). 255 bayttan fazlası iletildiğinde OperationError ile reddedilir.
  • Tam eşleşme gerekir: crypto.subtle.verify(), yalnızca doğrulayıcı crypto.subtle.sign() sırasında kullanılan context baytın aynısını ilettiğinde true değerini döndürür.

Yedek stratejiler

Temel hedefiniz henüz SubtleCrypto.supports() veya ML-DSA'yı desteklemiyorsa statik SubtleCrypto.supports() yöntemini kullanarak desteği tespit edin ve klasik 'Ed25519''ya (veya 'Ed25519' desteğinden önceki tarayıcıları hedeflerken 'P-256' ve 'SHA-256' ile 'ECDSA''ya) geri dönün:

  • SubtleCrypto.supports() yöntemini crypto.subtle üzerinde değil, SubtleCrypto oluşturucusunda çağırın: globalThis.SubtleCrypto?.supports?.('sign', 'ML-DSA-65'), boole değeri döndüren eşzamanlı bir statik yöntemdir (crypto.subtle.supports, undefined değeridir). 'sign', 'verify', 'generateKey', 'importKey', 'exportKey' veya 'getPublicKey' yöntemini inceleyin (ya da SubtleCrypto.supports('sign', { name: 'ML-DSA-65', context }) gibi bir ContextParams sözlüğü iletin).
  • Yedek anahtar biçimi ('raw'): Modern WebCrypto'yu destekleyen tarayıcılar 'Ed25519' ve 'ECDSA' üzerinde 'raw-public''yi de kabul ederken, eski tarayıcıları hedefleyen yedek yollarda 'raw''yi kullanın.
  • Yedek çözümde alan ayrımı: Klasik WebCrypto algoritmaları ('Ed25519' ve 'ECDSA'), algoritma sözlüğünden bir context özelliğini okumaz (WebIDL, Algorithm ve EcdsaParams üzerindeki bilinmeyen sözlük özelliklerini sessizce yoksayar). Alan ayrımının her iki pakette de tutarlı bir şekilde davranması için yedek yolunuzdaki yüke uzunluk ön ekli bir context başlığı ekleyin.
const ML_DSA_PUBLIC_KEY_ALGORITHMS = {
  1312: 'ML-DSA-44',
  1952: 'ML-DSA-65',
  2592: 'ML-DSA-87',
};

export function supportsPostQuantumSignatures() {
  return (
    typeof globalThis.SubtleCrypto?.supports === 'function' &&
    SubtleCrypto.supports('sign', 'ML-DSA-65')
  );
}

export async function generateSigningKeyPair() {
  if (supportsPostQuantumSignatures()) {
    const keyPair = await crypto.subtle.generateKey('ML-DSA-65', false, ['sign', 'verify']);
    const publicKeyBytes = new Uint8Array(
      await crypto.subtle.exportKey('raw-public', keyPair.publicKey),
    );
    return {
      suite: 'ML-DSA-65',
      keyFormat: 'raw-public',
      keyPair,
      publicKeyBytes,
    };
  }

  // Classical fallback: Ed25519
  const keyPair = await crypto.subtle.generateKey('Ed25519', false, ['sign', 'verify']);
  const publicKeyBytes = new Uint8Array(await crypto.subtle.exportKey('raw', keyPair.publicKey));
  return {
    suite: 'Ed25519',
    keyFormat: 'raw',
    keyPair,
    publicKeyBytes,
  };
}

function frameFallbackInput(payloadBytes, contextBytes) {
  if (contextBytes.byteLength > 255) {
    throw new DOMException('Context must not exceed 255 bytes', 'OperationError');
  }
  const framed = new Uint8Array(1 + contextBytes.byteLength + payloadBytes.byteLength);
  framed[0] = contextBytes.byteLength;
  framed.set(contextBytes, 1);
  framed.set(payloadBytes, 1 + contextBytes.byteLength);
  return framed;
}

export async function signPayload(
  privateKey,
  payloadBytes,
  contextBytes = new Uint8Array(0),
) {
  if (privateKey.algorithm.name.startsWith('ML-DSA')) {
    const sig = await crypto.subtle.sign(
      { name: privateKey.algorithm.name, context: contextBytes },
      privateKey,
      payloadBytes,
    );
    return new Uint8Array(sig);
  }

  const framed = frameFallbackInput(payloadBytes, contextBytes);
  const sig = await crypto.subtle.sign('Ed25519', privateKey, framed);
  return new Uint8Array(sig);
}

export async function verifyPayload(
  publicKeyOrBytes,
  signatureBytes,
  payloadBytes,
  contextBytes = new Uint8Array(0),
) {
  const publicKey =
    publicKeyOrBytes instanceof CryptoKey
      ? publicKeyOrBytes
      : await (() => {
          const bytes =
            publicKeyOrBytes instanceof Uint8Array
              ? publicKeyOrBytes
              : new Uint8Array(publicKeyOrBytes);
          const mlDsaName = ML_DSA_PUBLIC_KEY_ALGORITHMS[bytes.byteLength];
          return mlDsaName
            ? crypto.subtle.importKey('raw-public', bytes, mlDsaName, true, ['verify'])
            : crypto.subtle.importKey('raw', bytes, 'Ed25519', true, ['verify']);
        })();

  if (publicKey.algorithm.name.startsWith('ML-DSA')) {
    return crypto.subtle.verify(
      { name: publicKey.algorithm.name, context: contextBytes },
      publicKey,
      signatureBytes,
      payloadBytes,
    );
  }

  const framed = frameFallbackInput(payloadBytes, contextBytes);
  return crypto.subtle.verify('Ed25519', publicKey, signatureBytes, framed);
}