Web Cryptography API(crypto.subtle)を使用して、耐量子(ML-DSA)または従来型('Ed25519'、'ECDSA')の非対称鍵を使用して、ブラウザでメッセージ、トークン、アーティファクトに署名し、検証します。デジタル署名は、共有秘密鍵を必要とせずに、データの完全性と署名者の真正性を保証します。共有シークレットを確立して機密ペイロードを暗号化する方法については、WebCrypto を使用してクライアントサイドでデータを暗号化するをご覧ください。
1. アルゴリズムを選択して鍵ペアを生成する
ML-DSA(Module-Lattice-Based Digital Signature Algorithm、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 ビットのセキュリティ)。最小の鍵と署名 | 1,312 バイト | 2,420 バイト |
'ML-DSA-65' |
NIST カテゴリ 3(約 192 ビットのセキュリティ)。推奨されるデフォルト | 1,952 バイト | 3,309 バイト |
'ML-DSA-87' |
NIST カテゴリ 5(約 256 ビットのセキュリティ)、最高レベルのセキュリティ マージン | 2,592 バイト | 4,627 バイト |
- アルゴリズム名として
'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': 3 つの ML-DSA アルゴリズムすべてで使用される 32 バイトの秘密鍵シード。'raw'と'raw-private'の両方が ML-DSA でNotSupportedErrorをスローします。'raw-seed'、'pkcs8'(54 バイト)、または'jwk'(kty: 'AKP')を使用して秘密鍵を保存または送信します。秘密鍵をインポートするときに、鍵の使用方法として['sign']を渡します(['sign', 'verify']をimportKey()に渡すとSyntaxErrorがスローされます)。'spki'、'pkcs8'、'jwk': 3 つの 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.verify()は、検証ツールがcrypto.subtle.sign()で使用されたものと完全に一致するcontextバイトを渡した場合にのみtrueを返します。
代替戦略
Baseline ターゲットが SubtleCrypto.supports() または ML-DSA をまだサポートしていない場合は、静的 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);
}