ソースコードだけ見たい人のために
鍵ペア作成
$ openssl ecparam -genkey -name prime256v1 -out p256.keypair
$ openssl ec -in p256.keypair -outform PEM -pubout -out p256.publickey.pem
read EC key
writing EC key
$ openssl ec -in p256.keypair -outform PEM -out p256.privatekey.pem
read EC key
writing EC key
$ openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in p256.privatekey.pem -out p256.pkcs8.pem
生成されたモノ:
- p256.keypair: 鍵ペア(秘密鍵)
- p256.publickey.pem: 公開鍵 --- 署名検証に使う
- p256.privatekey.pem: 秘密鍵
- p256.pkcs8.pem: 秘密鍵(PKCS#8) --- 署名に使う
ソースコード
<html>
<head>
<meta http-eqix="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript"src="jquery-3.6.1.js"></script>
<script>
$(function() {
function stob(str) {
return Uint8Array.from(str, c => c.charCodeAt(0));
}
// String to ArrayBuffer
function s2buf(str) {
const buf = new ArrayBuffer(str.length);
const view = new Uint8Array(buf);
for (let i = 0, len = str.length; i < len; i++) {
view[i] = str.charCodeAt(i);
}
return buf;
}
// ArrayByffer to base64
function buf2b(buffer) {
return btoa(String.fromCharCode.apply(null, new Uint8Array(buffer)));
}
// base64 to ArrayBuffer
function b2buf(str) {
return Uint8Array.from(atob(str), c => c.charCodeAt(0));
}
// 署名
$('#submit-sign').on('click', event => {
console.log('# submit sign');
var reader = new FileReader();
reader.readAsText($('#input-privatekey').prop('files')[0]);
reader.onload = async () => {
console.log("private key (pem): \n" + reader.result);
var privatekey = reader.result
.replace(/-----BEGIN .*PRIVATE KEY-----/, "")
.replace(/-----END .*PRIVATE KEY-----/, "")
.replace(/\r?\n/g, '');
console.log("private key (base64): " + privatekey);
var privatekeySpec = await window.crypto.subtle.importKey(
'pkcs8',
s2buf(atob(privatekey)),
{
name: "ECDSA",
namedCurve: "P-256",
hash: "SHA-256"
},
true,
["sign"]
);
console.log("private key (spec): " + privatekeySpec);
var sig = await window.crypto.subtle.sign(
{name: "ECDSA", hash: {name: "SHA-256"}},
privatekeySpec,
stob($('#input-challenge1').val())
);
console.log(buf2b(sig));
$('#input-sign').val(buf2b(sig));
};
});
// 検証
$('#submit-verify').on('click', event => {
console.log('# submit verify');
var reader = new FileReader();
reader.readAsText($('#input-publickey').prop('files')[0]);
reader.onload = async () => {
console.log("public key (pem): \n" + reader.result);
var publickey = reader.result
.replace(/-----BEGIN .*PUBLIC KEY-----/, "")
.replace(/-----END .*PUBLIC KEY-----/, "")
.replace(/\r?\n/g, '');
console.log("public key (base64): " + publickey);
var publickeySpec = await window.crypto.subtle.importKey(
'spki',
s2buf(atob(publickey)),
{
name: "ECDSA",
namedCurve: "P-256",
hash: "SHA-256"
},
true,
["verify"]
);
console.log("public key (spec): " + publickeySpec);
var result = await window.crypto.subtle.verify(
{name: "ECDSA", hash: {name: "SHA-256"}},
publickeySpec,
b2buf($('#input-sign').val()),
stob($('#input-challenge1').val())
);
console.log(result);
};
});
});
</script>
</head>
<body>
<h1>署名</h1>
<form id='f1'>
<table>
<tr><td>チャレンジ: </td><td><input type='text' id='input-challenge1' name='challenge' size='60' value='a1234'></td></tr>
<tr><td>秘密鍵(pkcs#8): </td><td><input type='file' id='input-privatekey'></td></tr>
</table>
<input type='button' id='submit-sign' value='署名する'>
</form>
<h1>検証</h1>
<form id='f2'>
<table>
<tr><td>チャレンジ: </td><td><input type='text' id='input-challenge2' name='challenge' size='60' value='a1234'></td></tr>
<tr><td>署名(base64): </td><td><input type='text' id='input-sign' name='sign' size='100'></td></tr>
<tr><td>公開鍵: </td><td><input type='file' id='input-publickey'></td></tr>
</table>
<input type='button' id='submit-verify' value='検証する'>
</form>
</body>
</html>
実行結果
解説
はじめに
PEM形式の鍵を読み込み、Web Crypto APIを使い、楕円曲線暗号での署名と検証を行います。
手順は、
-
crypto.subtle.importKey()
で鍵を読み込み、 -
crypto.subtle.sign()
で署名をして、 -
crypto.subtle.verify()
で検証する。
だけなのですが、関数の引数の型がキモくて型変換が必要な部分が難しい点です。
[署名・検証] 鍵ペア生成
OpenSSLを使って楕円曲線暗号の鍵ペア(PEM形式)を生成します。ちなみに「鍵ペア」と呼んでいますが、公開鍵暗号方式の場合、秘密鍵=鍵情報すべて、公開鍵=鍵情報の一部、であるため、実態は鍵ペア≒秘密鍵であり、秘密鍵から公開鍵を取り出すことができます。
$ openssl ecparam -genkey -name prime256v1 -out p256.keypair
-name
は曲線名で、使用できる曲線一覧は openssl ecparam -list_curves
で表示できます。この記事では prime256v1
(P-256
) を使います。
生成した鍵ペアから公開鍵(PEM形式)と秘密鍵(PEM形式)を取り出します。
$ openssl ec -in p256.keypair -outform PEM -pubout -out p256.publickey.pem
read EC key
writing EC key
$ openssl ec -in p256.keypair -outform PEM -out p256.privatekey.pem
read EC key
writing EC key
Web Crypto APIでは、秘密鍵はPKCS#8しか取り扱えないので、取り出した秘密鍵をPKCS#8形式に変換します。ちなみにPKCS#8とは、秘密鍵を格納する一般的なフォーマットのことで、おおよそ「秘密鍵」+「秘密鍵のアルゴリズムなどのメタ情報」が格納されている、と思っておけばいいです。
$ openssl pkcs8 -topk8 -inform PEM -outform PEM -nocrypt -in p256.privatekey.pem -out p256.pkcs8.pem
PKCS#8の見分け方
PEM形式のファイルは、中身を見ることで判別することができます。ヘッダーとフッター部分で判別します。
- PKCS#8
-----BEGIN PRIVATE KEY-----
(Base64エンコード)
-----END PRIVATE KEY-----
- 楕円曲線暗号の秘密鍵
-----BEGIN EC PRIVATE KEY-----
(Base64エンコード)
-----END EC PRIVATE KEY-----
- RSA暗号の秘密鍵(PKCS#1)
-----BEGIN RSA PRIVATE KEY-----
(Base64エンコード)
-----END RSA PRIVATE KEY-----
PEM形式とDER形式
- DER形式:バイナリ形式。
- PEM形式:DER形式をBase64エンコードし、ヘッダーとフッターを付けた形式。
PEMとDERは、単にBase64エンコードされているか否かであるため、お互い変換可能です。
[署名] 秘密鍵の読み込み
まず、PEM形式のヘッダーとフッターと改行を取り、Base64部分のみを取り出します。
var reader = new FileReader();
reader.readAsText($('#input-privatekey').prop('files')[0]);
reader.onload = async () => {
var privatekey = reader.result
.replace(/-----BEGIN .*PRIVATE KEY-----/, "")
.replace(/-----END .*PRIVATE KEY-----/, "")
.replace(/\r?\n/g, '');
次に、crypto.subtle.importKey()
で秘密鍵を読み込みます。
- 第1引数は鍵のフォーマット名で、秘密鍵は
pkcs8
を指定します。 - 第2引数は鍵データですが、型は
ArrayBuffer
(*)にする必要があります。そのため、読み込んだ秘密鍵(Base64)をatob()
でバイナリにし、s2buf()
でArrayBuffer
に変換します。 - 第3引数はアルゴリズムですが、楕円曲線の場合は単に名前(文字列)だけでなく曲線名も必要なため、
EcKeyImportParams
型で指定する必要があります。 - 第4引数は、鍵がエクスポート可能かどうかを指定します。
- 第5引数は鍵の用途で、署名に使うため
sign
を指定します。それ以外に指定できる値はこちらを参照してください。 - 戻り値は、
CryptoKey
型(のPromise
)で、これを署名に使います。
(*) 実際は、ArrayBuffer
、TypedArray
、DataView
、JsonWebKey
のいずれかの型で良いです。
var privatekeySpec = await window.crypto.subtle.importKey(
'pkcs8',
s2buf(atob(privatekey)),
{
name: "ECDSA",
namedCurve: "P-256",
hash: "SHA-256"
},
true,
["sign"]
);
[署名] 署名する
crypto.subtle.sign()
で署名します。
- 第1引数はアルゴリズムで、楕円曲線暗号の場合は
EcdsaParams
型を指定します。 - 第2引数は鍵データで、先ほど
crypto.subtle.importKey()
で読み込んだ鍵を指定します。 - 第3引数は署名元データですが、
ArrayBuffer
型(*)であるため、stob()
でArrayBuffer
型に変換しています。 - 戻り値は署名ですが、
ArrayBuffer
型であるため、buf2b()
でBase64エンコードして表示しています。
(*) 実際は、ArrayBuffer
、TypedArray
、DataView
のいずれかの型で良いです。
var sig = await window.crypto.subtle.sign(
{name: "ECDSA", hash: {name: "SHA-256"}},
privatekeySpec,
stob($('#input-challenge1').val())
);
$('#input-sign').val(buf2b(sig));
[検証] 公開鍵の読み込み
秘密鍵と同様に、まず、PEM形式のヘッダーとフッターと改行を取り、Base64部分のみを取り出します。
var reader = new FileReader();
reader.readAsText($('#input-publickey').prop('files')[0]);
reader.onload = async () => {
var publickey = reader.result
.replace(/-----BEGIN .*PUBLIC KEY-----/, "")
.replace(/-----END .*PUBLIC KEY-----/, "")
.replace(/\r?\n/g, '');
次に、こちらも秘密鍵と同様に、crypto.subtle.importKey()
で公開鍵を読み込みます。引数も秘密鍵の読み込みと同じです。
- 第1引数は鍵のフォーマット名で、公開鍵は
spki
を指定します。 - 第2引数は鍵データで、型は
ArrayBuffer
にする必要があります。秘密鍵と同様に、読み込んだ秘密鍵(Base64)をatob()
でバイナリにし、s2buf()
でArrayBuffer
に変換します。 - 第3引数はアルゴリズムで、秘密鍵と同様に、楕円曲線の場合は単に名前(文字列)だけでなく曲線名も必要なため、
EcKeyImportParams
型で指定する必要があります。 - 第4引数は、鍵がエクスポート可能かどうかを指定します。
- 第5引数は鍵の用途で、検証に使うため
verify
を指定します。 - 戻り値は、
CryptoKey
型(のPromise
)で、これを検証に使います。
var publickeySpec = await window.crypto.subtle.importKey(
'spki',
s2buf(atob(publickey)),
{
name: "ECDSA",
namedCurve: "P-256",
hash: "SHA-256"
},
true,
["verify"]
);
[検証] 署名を検証する
crypto.subtle.verify()
で検証します。
- 第1引数はアルゴリズムで、楕円曲線暗号の場合は
EcdsaParams
型を指定します。 - 第2引数は鍵データで、先ほど
crypto.subtle.importKey()
で読み込んだ鍵を指定します。 - 第3引数は署名ですが、
ArrayBuffer
型であるため、b2buf()
でBase64からArrayBuffer
型に変換しています。 - 第4引数は署名元データですが、
ArrayBuffer
型であるため、stob()
でArrayBuffer
型に変換しています。 - 戻り値は検証結果で
boolean
型(のPromise
)が返ります。
var result = await window.crypto.subtle.verify(
{name: "ECDSA", hash: {name: "SHA-256"}},
publickeySpec,
b2buf($('#input-sign').val()),
stob($('#input-challenge1').val())
);
参考
- MDN
-
Zennの記事
- RSA暗号だが、Web Crypto APIを使ってPEM形式の読み込み方法を書いたサイトはここしかない。