از Web Cryptography API (crypto.subtle) برای امضا و تأیید پیامها، توکنها، یا مصنوعات در مرورگر بااستفاده از کلیدهای نامتقارن مقاوم دربرابر کوانتوم (ML-DSA) یا کلاسیک ('Ed25519'، 'ECDSA') استفاده کنید. امضاهای دیجیتال تمامیت دادهها و اصالت امضاکننده را بدون نیاز به کلیدهای محرمانه مشترک تضمین میکنند. برای ایجاد رمزهای مشترک و رمزگذاری محمولههای محرمانه، به رمزگذاری دادهها در سمت کارخواه با WebCrypto مراجعه کنید.
۱. الگوریتمی را انتخاب کنید و جفت کلید ایجاد کنید
ML-DSA (الگوریتم امضای دیجیتال مبتنی بر شبکه ماژول، استانداردشده در NIST FIPS 204) امضاهای دیجیتال مبتنی بر شبکه پساکوانتومی را ارائه میدهد که دربرابر حملات رایانههای کلاسیک و کوانتومی مقاوم هستند.
بهطور پیشفرض از 'ML-DSA-65' برای امضای عمومی استفاده کنید. اگر به کلیدهای عمومی و امضاهای کوچکتر در کانالهای با پهنای باند محدود نیاز دارید و امنیت ۱۲۸ بیتی کافی است، 'ML-DSA-44' را انتخاب کنید، یا اگر خطمشی امنیتی به رعایت «دسته ۵ NIST» (CNSA 2.0) نیاز دارد، 'ML-DSA-87' را انتخاب کنید.
| نام الگوریتم | سطح امنیتی و کاربرد | کلید عمومی ('raw-public') |
اندازه امضا |
|---|---|---|---|
'ML-DSA-44' |
NIST Category 2 (امنیت تقریباً ۱۲۸ بیتی)؛ کوچکترین کلیدها و امضاها | ۱۳۱۲ بایت | ۲۴۲۰ بایت |
'ML-DSA-65' |
NIST Category 3 (امنیت تقریباً ۱۹۲ بیتی)؛ پیشفرض توصیهشده | ۱۹۵۲ بایت | 3309 bytes |
'ML-DSA-87' |
NIST Category 5 (~256-bit security); highest security margin | ۲۵۹۲ بایت | 4627 bytes |
-
'ML-DSA-44'،'ML-DSA-65'، یا'ML-DSA-87'را بهعنوان نام الگوریتم ارسال کنید (نامهای پیشاز استانداردسازی مانند'Dilithium'یا'CRYSTALS-Dilithium'باعث ایجادNotSupportedErrorمیشوند). - نه ML-DSA و نه
'Ed25519'پارامترhashرا نمیپذیرند. - هنگام تولید جفت کلید با
generateKey()،['sign', 'verify']را ارسال کنید:
const signerKeyPair = await crypto.subtle.generateKey(
'ML-DSA-65',
false,
['sign', 'verify'],
);
۲. صادر و وارد کردن کلیدها
'ML-DSA-44'، 'ML-DSA-65'، و 'ML-DSA-87' قالب قدیمی 'raw' را در importKey() و exportKey() با NotSupportedError رد میکنند. همیشه کلیدها را بااستفاده از قالب صریح صادر یا وارد کنید:
'raw-public': بایتهای کلید عمومی (1312،1952، یا2592بایت). هنگام وارد کردن کلید عمومی،['verify']را بهعنوان کاربرد کلید ارسال کنید.'raw-seed': دانه کلید خصوصی ۳۲ بایتی برای هر سه الگوریتم ML-DSA. Both'raw'and'raw-private'throwNotSupportedErroron ML-DSA—store or transmit private keys using'raw-seed','pkcs8'(54 bytes), or'jwk'(kty: 'AKP'). Pass['sign']as the key usage when importing a private key (passing['sign', 'verify']toimportKey()throwsSyntaxError).'spki'،'pkcs8'، و'jwk': برای هر سه الگوریتم ML-DSA پشتیبانی میشود.
همیشه کلید عمومی درستیسنجی را با '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 عمومی مربوطه نیاز دارید، crypto.subtle.getPublicKey() را با ['verify'] فراخوانی کنید:
const publicKey = await crypto.subtle.getPublicKey(
signerKeyPair.privateKey,
['verify'],
);
۳. امضا و درستیسنجی بار دادهها
با کلید خصوصی 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 جداسازی دامنه متصل کنید.
بهجای رشته نام الگوریتم ساده، یک واژهنامه ContextParams ({ name, context }) را به sign() و verify() ارسال کنید، که در آن 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,
);
- محدودیت ۲۵۵ بایتی:
contextاز0تا255بایت را میپذیرد (حذفcontextمعادل ارسالnew Uint8Array(0)خالی است). ارسال بیشاز255بایت باOperationErrorرد میشود. - مطابقت دقیق لازم است:
crypto.subtle.verify()فقط زمانیtrueرا برمیگرداند که درستیسنج دقیقاً همان بایتهایcontextرا که درطولcrypto.subtle.sign()استفاده شده است ارسال کند.
راهبردهای جایگزین
اگر هدف Baseline شما هنوز از SubtleCrypto.supports() یا ML-DSA پشتیبانی نمیکند، پشتیبانی را بااستفاده از روش static SubtleCrypto.supports() شناسایی کنید و به 'Ed25519' کلاسیک (یا 'ECDSA' با 'P-256' و 'SHA-256' هنگام هدفیابی مرورگرهای قبلاز پشتیبانی 'Ed25519') برگردید:
- Call
SubtleCrypto.supports()on theSubtleCryptoconstructor, not oncrypto.subtle:globalThis.SubtleCrypto?.supports?.('sign', 'ML-DSA-65')is a synchronous static method returning a boolean (crypto.subtle.supportsisundefined). Probe'sign','verify','generateKey','importKey','exportKey', or'getPublicKey'(or pass aContextParamsdictionary such asSubtleCrypto.supports('sign', { name: 'ML-DSA-65', context })). - قالب کلید در حالت بازگشت (
'raw'): اگرچه مرورگرهایی که از Modern WebCrypto پشتیبانی میکنند'raw-public'را در'Ed25519'و'ECDSA'نیز میپذیرند، از'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);
}