WebCrypto की मदद से, डेटा के लिए हस्ताक्षर और पुष्टि करना

वेब की सुविधाएं: वेब क्रिप्टोग्राफ़ी

Web Cryptography API (crypto.subtle) का इस्तेमाल करके, ब्राउज़र में मैसेज, टोकन या आर्टफ़ैक्ट पर हस्ताक्षर करें और उनकी पुष्टि करें. इसके लिए, क्वांटम-रेज़िस्टेंट (एमएल-डीएसए) या क्लासिकल ('Ed25519', 'ECDSA') एसिमेट्रिक कुंजियों का इस्तेमाल करें. डिजिटल सिग्नेचर, डेटा की सुरक्षा और हस्ताक्षर करने वाले की प्रामाणिकता की गारंटी देते हैं. इसके लिए, शेयर की गई सीक्रेट कुंजियों की ज़रूरत नहीं होती. शेयर किए गए सीक्रेट सेट अप करने और गोपनीय पेलोड को एन्क्रिप्ट करने के लिए, WebCrypto की मदद से क्लाइंट-साइड पर डेटा एन्क्रिप्ट करना लेख पढ़ें.

1. कोई एल्गोरिदम चुनें और कुंजी का जोड़ा जनरेट करें

एमएल-डीएसए (मॉड्यूल-लैटिस-बेस्ड डिजिटल सिग्नेचर एल्गोरिदम, जिसे एनआईएसटी एफ़आईपीएस 204 में स्टैंडर्ड के तौर पर शामिल किया गया है) पोस्ट-क्वांटम लैटिस-बेस्ड डिजिटल सिग्नेचर उपलब्ध कराता है. यह क्लासिकल और क्वांटम कंप्यूटर, दोनों से होने वाले हमलों से सुरक्षित रखता है.

सामान्य कामों के लिए, डिफ़ॉल्ट रूप से 'ML-DSA-65' का इस्तेमाल करें. 'ML-DSA-44' को तब चुनें, जब आपको कम बैंडविड्थ वाले चैनलों पर छोटे सार्वजनिक पासकोड और हस्ताक्षर की ज़रूरत हो और 128-बिट की सुरक्षा काफ़ी हो. इसके अलावा, 'ML-DSA-87' को तब चुनें, जब सुरक्षा नीति के लिए एनआईएसटी कैटगरी 5 (सीएनएसए 2.0) का पालन करना ज़रूरी हो.

एल्गोरिदम का नाम सुरक्षा का स्तर और इस्तेमाल का उदाहरण सार्वजनिक पासकोड ('raw-public') हस्ताक्षर का साइज़
'ML-DSA-44' एनआईएसटी कैटगरी 2 (~128-बिट सुरक्षा); सबसे छोटी कुंजियां और हस्ताक्षर 1312 बाइट 2420 बाइट
'ML-DSA-65' एनआईएसटी कैटगरी 3 (~192-बिट सुरक्षा); डिफ़ॉल्ट रूप से सुझाई गई 1952 बाइट 3309 बाइट
'ML-DSA-87' एनआईएसटी कैटगरी 5 (~256-बिट सुरक्षा); सबसे ज़्यादा सुरक्षा मार्जिन 2592 बाइट 4627 बाइट
  • एल्गोरिदम के नाम के तौर पर 'ML-DSA-44', 'ML-DSA-65' या 'ML-DSA-87' पास करें. स्टैंडर्डाइज़ेशन से पहले के नाम, जैसे कि 'Dilithium' या 'CRYSTALS-Dilithium', NotSupportedError थ्रो करते हैं.
  • एमएल-डीएसए और 'Ed25519', दोनों में से किसी के लिए भी hash पैरामीटर की ज़रूरत नहीं होती.
  • generateKey() की मदद से कुंजी का जोड़ा जनरेट करते समय, ['sign', 'verify'] पास करें:
const signerKeyPair = await crypto.subtle.generateKey(
  'ML-DSA-65',
  false,
  ['sign', 'verify'],
);

2. पासकोड एक्सपोर्ट और इंपोर्ट करना

'ML-DSA-44', 'ML-DSA-65', और 'ML-DSA-87', importKey() और exportKey() में पुराने 'raw' फ़ॉर्मैट को अस्वीकार कर देते हैं. साथ ही, NotSupportedError दिखाते हैं. कुंजियों को हमेशा किसी खास फ़ॉर्मैट का इस्तेमाल करके एक्सपोर्ट या इंपोर्ट करें:

  • 'raw-public': सार्वजनिक पासकोड के बाइट (1312, 1952 या 2592 बाइट). सार्वजनिक पासकोड इंपोर्ट करते समय, पासकोड के इस्तेमाल के तौर पर ['verify'] पास करें.
  • 'raw-seed': तीनों एमएल-डीएसए एल्गोरिदम के लिए, 32-बाइट की निजी पासकोड सीड. 'raw' और 'raw-private', दोनों ही ML-DSA पर NotSupportedError थ्रो करते हैं—निजी पासकोड को 'raw-seed', 'pkcs8' (54 बाइट) या 'jwk' (kty: 'AKP') का इस्तेमाल करके सेव करें या ट्रांसमिट करें. निजी पासकोड इंपोर्ट करते समय, पासकोड के इस्तेमाल के तौर पर ['sign'] पास करें (['sign', 'verify'] को importKey() पर पास करने से SyntaxError थ्रो होता है).
  • 'spki', 'pkcs8', और 'jwk': तीनों एमएल-डीएसए एल्गोरिदम के लिए काम करता है.

पुष्टि करने के लिए इस्तेमाल होने वाले सार्वजनिक पासकोड को हमेशा 'raw-public' के साथ एक्सपोर्ट करें, ताकि सार्वजनिक पासकोड के बाइट को क्रम से लगाया जा सके या पुष्टि करने वाले लोगों के साथ शेयर किया जा सके:

// 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'],
);

अगर आपके पास सिर्फ़ निजी साइनिंग CryptoKey है (भले ही, extractable: false के साथ हो) और आपको इससे जुड़ा सार्वजनिक CryptoKey चाहिए, तो ['verify'] के साथ crypto.subtle.getPublicKey() को कॉल करें:

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

3. पेलोड पर हस्ताक्षर करना और उनकी पुष्टि करना

हस्ताक्षर जनरेट करने के लिए, निजी पासकोड के साथ crypto.subtle.sign() को कॉल करें. साथ ही, हस्ताक्षर की जांच करने के लिए, सार्वजनिक पासकोड के साथ crypto.subtle.verify() को कॉल करें:

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 के साथ डोमेन सेपरेशन

एक मकसद के लिए बनाए गए हस्ताक्षर को, आपके ऐप्लिकेशन या प्रोटोकॉल के किसी दूसरे हिस्से में फिर से इस्तेमाल होने से रोकने के लिए, हस्ताक्षर को डोमेन-सेपरेशन context से बाइंड करें. ऐसा तब करें, जब ऐप्लिकेशन या प्रोटोकॉल का दूसरा हिस्सा, उसी कुंजी के जोड़े को शेयर करता हो.

एल्गोरिदम के नाम की स्ट्रिंग के बजाय, sign() और verify() को ContextParams डिक्शनरी ({ name, context }) पास करें. इसमें context, BufferSource (Uint8Array या ArrayBuffer) है:

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 बाइट की सीमा: context में 0 से 255 बाइट तक की वैल्यू डाली जा सकती है. context को छोड़ने का मतलब है कि खाली new Uint8Array(0) पास किया गया है. 255 बाइट से ज़्यादा की वैल्यू डालने पर, OperationError के साथ अस्वीकार कर दिया जाता है.
  • एग्ज़ैक्ट मैच ज़रूरी है: crypto.subtle.verify(), true को सिर्फ़ तब दिखाता है, जब पुष्टि करने वाला व्यक्ति crypto.subtle.sign() के दौरान इस्तेमाल किए गए context बाइट को पास करता है.

फ़ॉलबैक रणनीतियां

अगर आपका Baseline टारगेट अब तक SubtleCrypto.supports() या एमएल-डीएसए के साथ काम नहीं करता है, तो स्टैटिक SubtleCrypto.supports() तरीके का इस्तेमाल करके, यह पता लगाएं कि यह काम करता है या नहीं. इसके बाद, क्लासिक 'Ed25519' (या 'Ed25519' के साथ काम करने से पहले के ब्राउज़र को टारगेट करते समय, 'P-256' और 'SHA-256' के साथ 'ECDSA') पर वापस जाएं:

  • crypto.subtle के बजाय SubtleCrypto कंस्ट्रक्टर पर SubtleCrypto.supports() को कॉल करें: globalThis.SubtleCrypto?.supports?.('sign', 'ML-DSA-65') एक सिंक्रोनस स्टैटिक तरीका है, जो बूलियन (crypto.subtle.supports, undefined है) दिखाता है. 'sign', 'verify', 'generateKey', 'importKey', 'exportKey' या 'getPublicKey' की जांच करें (या SubtleCrypto.supports('sign', { name: 'ML-DSA-65', context }) जैसी ContextParams डिक्शनरी पास करें).
  • फ़ॉलबैक ('raw') में मुख्य फ़ॉर्मैट: मॉडर्न WebCrypto के साथ काम करने वाले ब्राउज़र, 'Ed25519' और 'ECDSA' पर 'raw-public' को भी स्वीकार करते हैं. हालांकि, पुराने ब्राउज़र को टारगेट करने वाले फ़ॉलबैक पाथ में 'raw' का इस्तेमाल करें.
  • फ़ॉलबैक में डोमेन सेपरेशन: क्लासिकल WebCrypto एल्गोरिदम ('Ed25519' और 'ECDSA'), एल्गोरिदम डिक्शनरी से context प्रॉपर्टी को नहीं पढ़ते हैं. WebIDL, Algorithm और EcdsaParams पर मौजूद डिक्शनरी की अनजान प्रॉपर्टी को अनदेखा कर देता है. अपने फ़ॉलबैक पाथ में पेलोड के लिए, लंबाई के हिसाब से तय किया गया context हेडर जोड़ें, ताकि डोमेन सेपरेशन दोनों सुइट में एक जैसा काम करे.
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);
}