使用 Web Cryptography API (crypto.subtle),在瀏覽器中透過抗量子 (ML-DSA) 或傳統 ('Ed25519'、'ECDSA') 非對稱金鑰,簽署及驗證訊息、權杖或構件。數位簽章可確保資料完整性和簽署者真實性,不需共用私密金鑰。如要瞭解如何建立共用密鑰及加密機密酬載,請參閱「使用 WebCrypto 加密用戶端資料」。
1. 選擇演算法並產生金鑰組
ML-DSA (模組格狀數位簽章演算法,NIST FIPS 204 標準) 提供後量子格狀數位簽章,可抵禦傳統和量子電腦的攻擊。
一般用途簽署作業預設使用 'ML-DSA-65'。如果需要透過頻寬受限的管道傳輸較小的公開金鑰和簽章,且 128 位元安全性已足夠,請選擇 'ML-DSA-44';如果安全性政策要求符合 NIST 第 5 類 (CNSA 2.0) 規定,請選擇 'ML-DSA-87'。
| 演算法名稱 | 安全等級和用途 | 公開金鑰 ('raw-public') |
簽名大小 |
|---|---|---|---|
'ML-DSA-44' |
NIST 類別 2 (~128 位元安全性);金鑰和簽章最小 | 1312 個位元組 | 2420 個位元組 |
'ML-DSA-65' |
NIST 類別 3 (約 192 位元安全性);建議預設值 | 1952 個位元組 | 3309 個位元組 |
'ML-DSA-87' |
NIST 第 5 類 (約 256 位元安全性);最高安全邊際 | 2592 位元組 | 4627 個位元組 |
- 傳遞
'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'],
);
2. 匯出及匯入金鑰
'ML-DSA-44'、'ML-DSA-65' 和 'ML-DSA-87' 會拒絕 importKey() 和 exportKey() 中的舊版 'raw' 格式,並傳回 NotSupportedError。匯出或匯入金鑰時,一律要使用明確格式:
'raw-public':公開金鑰位元組 (1312、1952或2592位元組)。匯入公開金鑰時,請將['verify']做為金鑰用途傳遞。'raw-seed':所有三種 ML-DSA 演算法的 32 位元組私密金鑰種子。'raw'和'raw-private'都會在 ML-DSA 上擲回NotSupportedError。請使用'raw-seed'、'pkcs8'(54 位元組) 或'jwk'(kty: 'AKP') 儲存或傳輸私密金鑰。匯入私密金鑰時,請傳遞['sign']做為金鑰用途 (將['sign', 'verify']傳遞至importKey()會擲回SyntaxError)。'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,請使用 ['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。
請勿使用單純的演算法名稱字串,而是將 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,
);
- 255 位元組限制:
context接受0至255位元組 (省略context相當於傳遞空白new Uint8Array(0))。傳遞超過255位元組會遭到OperationError拒絕。 - 完全比對:只有在驗證者傳遞與
crypto.subtle.sign()期間使用的context位元組完全相同時,crypto.subtle.verify()才會傳回true。
備用策略
如果基準目標尚未支援 SubtleCrypto.supports() 或 ML-DSA,請使用靜態 SubtleCrypto.supports() 方法偵測支援,並改用傳統 'Ed25519' (如果目標瀏覽器不支援 'Ed25519',則改用 'ECDSA' 搭配 'P-256' 和 'SHA-256'):
- 在
SubtleCrypto建構函式上呼叫SubtleCrypto.supports(),而不是在crypto.subtle上呼叫:globalThis.SubtleCrypto?.supports?.('sign', 'ML-DSA-65')是同步靜態方法,會傳回布林值 (crypto.subtle.supports為undefined)。探測'sign'、'verify'、'generateKey'、'importKey'、'exportKey'或'getPublicKey'(或傳遞ContextParams字典,例如SubtleCrypto.supports('sign', { name: 'ML-DSA-65', context }))。 - 備用金鑰格式 (
'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);
}