🔑
Node.jsでECDSA署名してブラウザで検証する(標準APIのみを使用)
やりたいこと
- Node.jsで
crypto
モジュールを使ってECDSA(SHA-256, P-256曲線)で署名 - Node.jsとWebブラウザ上で上記の署名を検証
- Node.jsでは
crypto
モジュールを使い、WebブラウザではWeb Crypto API
を使う
- Node.jsでは
勘の良い人であればすぐに気づくと思うが、 JWT(厳密にはJWA)のES256相当 の署名アルゴリズムを外部ライブラリを使わず、標準APIのみで実装したいという話である。
手っ取り早く済ませるのであれば、適当なJWTもしくはJWSを扱えるライブラリを使えばいいのだが、ちょっとした部分で署名を使いたいだけなので、JWSは大げさだろうということで実装してみた。
秘密鍵/公開鍵の生成
鍵を作るのは別にopensslを叩いてもいいのだが、折角なのでNode.jsで生成してみた。
import { generateKeyPairSync } from 'node:crypto';
import { writeFile } from 'node:fs/promises';
const { privateKey, publicKey } = generateKeyPairSync('ec', {
namedCurve: 'P-256',
privateKeyEncoding: { format: 'pem', type: 'pkcs8' },
publicKeyEncoding: { format: 'pem', type: 'spki' }
});
await writeFile('privatekey', privateKey);
await writeFile('publickey', publicKey);
Node.jsで署名
要点として、署名する際に dsaEncoding: 'ieee-p1363'
を指定しなければならない。これは crypto
モジュールでは署名をDERでエンコードするのが規定であるのに対して、Web Crypto APIはIEEE P1363エンコードを前提としているためである。
- https://nodejs.org/api/crypto.html#signsignprivatekey-outputencoding
- https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/sign#ecdsa
import { createPrivateKey, createSign, } from 'node:crypto';
import { readFile } from 'node:fs/promises';
const keystring = await readFile('privatekey', 'utf8');
const privatekey = createPrivateKey(keystring);
// データは別なんでもいいが、JWSに倣ってbase64化したJSONを例として使う
const data = Buffer.from(JSON.stringify({
name: 'taro',
age: 20
})).toString('base64');
const signature = createSign('sha256')
.update(data)
.sign({key: privatekey, dsaEncoding: 'ieee-p1363'}, 'base64');
console.log(`data : ${data}`);
console.log(`signature : ${signature}`);
このスクリプトを実行すると、データと署名が出力されるので、これを検証プログラムに入力する。
Node.jsで検証
検証でも署名と同様に dsaEncoding: 'ieee-p1363'
を指定しなければならない。
import { createPublicKey, createVerify } from 'node:crypto';
import { readFile } from 'node:fs/promises';
// 署名スクリプトから出力された値を設定
const data = '...';
const signature = '...';
const keystring = await readFile('privatekey', 'utf8');
const publickey = createPublicKey(keystring);
const result = createVerify('sha256')
.update(data)
.verify({key: publickey, dsaEncoding: 'ieee-p1363'}, Buffer.from(signature, 'base64'));
console.log(result);
Webブラウザ(Web Crypto API)で検証
若干ユーティリティ関数に手を加えているが、MDNに記載されているサンプルコードの通り。
// Node.jsでもv15以降であればこの部分のimportをコメントインすれば動作する。
// ただし、v18以下では`Experimental`扱いなので注意する。
// import { webcrypto as crypto } from 'node:crypto';
// 生成した公開鍵を設定
const keystring = `-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----`;
// 署名スクリプトから出力された値を設定
const data = '...';
const signature = '...';
const encoder = new TextEncoder();
function decodeBase64(str) {
return Uint8Array.from(atob(str), c => c.charCodeAt(0));
}
function removePemBoundary(pem) {
const pemHeader = '-----BEGIN PRIVATE KEY-----';
const pemFooter = '-----END PRIVATE KEY-----';
return pem.trim().substring(pemHeader.length, pem.length - pemFooter.length);
}
const publickey = await crypto.subtle.importKey(
'spki',
decodeBase64(removePemBoundary(keystring)),
{name: 'ECDSA', namedCurve: 'P-256'},
true,
['verify']
);
const result = await crypto.subtle.verify(
{name: 'ECDSA', hash: {name: 'SHA-256'}},
publickey,
decodeBase64(signature),
encoder.encode(data)
);
console.log(result);
補足
Node.js v20で Web Crypto API
がstableになるので、v20以降であれば crypto
モジュールを使わずに全部 Web Crypto API
を使ってしまうのが楽だと思う。(とはいえ、v18のEoLが 2025-04-30
の予定なので結構先は長い…。)
Web Crypto API
のみで鍵の生成から署名/検証までを行う例は全てMDNにある。
Discussion