Sử dụng Web Cryptography API (crypto.subtle) để ký và xác minh thông báo, mã thông báo hoặc cấu phần phần mềm trong trình duyệt bằng khoá bất đối xứng kháng lượng tử (ML-DSA) hoặc khoá bất đối xứng cổ điển ('Ed25519', 'ECDSA'). Chữ ký số đảm bảo tính toàn vẹn của dữ liệu và tính xác thực của người ký mà không cần khoá bí mật dùng chung. Để thiết lập khoá bí mật dùng chung và mã hoá tải trọng bí mật, hãy xem bài viết Mã hoá dữ liệu phía máy khách bằng WebCrypto.
1. Chọn một thuật toán và tạo một cặp khoá
ML-DSA (Giải thuật ký số dựa trên mạng tinh thể mô-đun, được chuẩn hoá trong NIST FIPS 204) cung cấp chữ ký số dựa trên mạng tinh thể hậu lượng tử có khả năng chống lại các cuộc tấn công của cả máy tính cổ điển và máy tính lượng tử.
Sử dụng 'ML-DSA-65' theo mặc định cho việc ký chung. Chọn 'ML-DSA-44' khi bạn cần khoá công khai và chữ ký nhỏ hơn trên các kênh có băng thông hạn chế và mức bảo mật 128 bit là đủ, hoặc chọn 'ML-DSA-87' khi yêu cầu theo chính sách bảo mật yêu cầu tuân thủ NIST Danh mục 5 (CNSA 2.0).
| Tên thuật toán | Mức an ninh và trường hợp sử dụng | Khoá công khai ('raw-public') |
Kích thước chữ ký |
|---|---|---|---|
'ML-DSA-44' |
NIST Loại 2 (bảo mật ~128 bit); khoá và chữ ký nhỏ nhất | 1312 byte | 2420 byte |
'ML-DSA-65' |
NIST Loại 3 (bảo mật ~192 bit); mặc định được đề xuất | 1952 byte | 3309 byte |
'ML-DSA-87' |
NIST Loại 5 (bảo mật ~256 bit); biên độ bảo mật cao nhất | 2592 byte | 4627 byte |
- Truyền
'ML-DSA-44','ML-DSA-65'hoặc'ML-DSA-87'làm tên thuật toán (tên trước khi chuẩn hoá như'Dilithium'hoặc'CRYSTALS-Dilithium'sẽ gửiNotSupportedError). - Cả ML-DSA và
'Ed25519'đều không nhận tham sốhash. - Truyền
['sign', 'verify']khi tạo cặp khoá bằnggenerateKey():
const signerKeyPair = await crypto.subtle.generateKey(
'ML-DSA-65',
false,
['sign', 'verify'],
);
2. Xuất và nhập khoá
'ML-DSA-44', 'ML-DSA-65' và 'ML-DSA-87' từ chối định dạng 'raw' cũ trong importKey() và exportKey() bằng NotSupportedError. Luôn xuất hoặc nhập khoá bằng định dạng rõ ràng:
'raw-public': Byte khoá công khai (1312,1952hoặc2592byte). Truyền['verify']làm mục đích sử dụng khoá khi nhập khoá công khai.'raw-seed': Hạt giống khoá riêng tư 32 byte cho cả ba thuật toán ML-DSA. Cả'raw'và'raw-private'đều gửiNotSupportedErrortrên ML-DSA – lưu trữ hoặc truyền khoá riêng tư bằng'raw-seed','pkcs8'(54 byte) hoặc'jwk'(kty: 'AKP'). Truyền['sign']làm mức sử dụng khoá khi nhập khoá riêng tư (truyền['sign', 'verify']đếnimportKey()sẽ gửiSyntaxError).'spki','pkcs8'và'jwk': Được hỗ trợ cho cả ba thuật toán ML-DSA.
Luôn xuất khoá công khai xác minh bằng 'raw-public' để có thể chuyển đổi tuần tự hoặc chia sẻ byte khoá công khai với trình xác minh:
// 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'],
);
Nếu bạn chỉ giữ một CryptoKey ký riêng tư (ngay cả với extractable: false) và cần CryptoKey công khai tương ứng, hãy gọi crypto.subtle.getPublicKey() bằng ['verify']:
const publicKey = await crypto.subtle.getPublicKey(
signerKeyPair.privateKey,
['verify'],
);
3. Ký và xác minh tải trọng
Gọi crypto.subtle.sign() bằng khoá riêng tư để tạo chữ ký và crypto.subtle.verify() bằng khoá công khai để kiểm tra chữ ký:
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,
);
Phân tách miền bằng ContextParams
Để ngăn chữ ký được tạo cho một mục đích được phát lại trong một phần khác của ứng dụng hoặc giao thức có cùng cặp khoá, hãy liên kết chữ ký với context phân tách miền.
Thay vì một chuỗi tên thuật toán đơn thuần, hãy truyền một từ điển ContextParams ({ name, context }) đến sign() và verify(), trong đó context là BufferSource (Uint8Array hoặc 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,
);
- Giới hạn 255 byte:
contextchấp nhận từ0đến255byte (việc bỏ quacontexttương đương với việc truyền mộtnew Uint8Array(0)trống). Việc truyền nhiều hơn255byte sẽ bị từ chối bằngOperationError. - Yêu cầu khớp chính xác:
crypto.subtle.verify()chỉ trả vềtruekhi trình xác minh truyền chính xác cùng một bytecontextđược dùng trongcrypto.subtle.sign().
Chiến lược dự phòng
Nếu mục tiêu Baseline của bạn chưa hỗ trợ SubtleCrypto.supports() hoặc ML-DSA, hãy phát hiện khả năng hỗ trợ bằng phương thức static SubtleCrypto.supports() và quay lại 'Ed25519' cổ điển (hoặc 'ECDSA' với 'P-256' và 'SHA-256' khi nhắm đến các trình duyệt trước khi hỗ trợ 'Ed25519'):
- Gọi
SubtleCrypto.supports()trên hàm khởi tạoSubtleCrypto, không phải trêncrypto.subtle:globalThis.SubtleCrypto?.supports?.('sign', 'ML-DSA-65')là một phương thức tĩnh đồng bộ trả về một boolean (crypto.subtle.supportslàundefined). Thăm dò'sign','verify','generateKey','importKey','exportKey'hoặc'getPublicKey'(hoặc truyền một từ điểnContextParamsnhưSubtleCrypto.supports('sign', { name: 'ML-DSA-65', context })). - Định dạng khoá trong phương án dự phòng (
'raw'): Mặc dù các trình duyệt hỗ trợ WebCrypto hiện đại cũng chấp nhận'raw-public'trên'Ed25519'và'ECDSA', nhưng hãy sử dụng'raw'trong các đường dẫn dự phòng nhắm đến các trình duyệt cũ. - Tách miền trong phương án dự phòng: Các thuật toán WebCrypto cổ điển (
'Ed25519'và'ECDSA') không đọc thuộc tínhcontexttừ từ điển thuật toán (WebIDL tự động bỏ qua các thuộc tính từ điển không xác định trênAlgorithmvàEcdsaParams). Thêm tiêu đềcontextcó tiền tố độ dài vào tải trọng trong đường dẫn dự phòng để việc tách miền hoạt động nhất quán trên cả hai bộ thuật toán.
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);
}