Web Cryptography API で PKCS #8 形式でインポートした秘密鍵は検証(verify)には使えない 結論
pkcs8 で秘密鍵をインポートした場合には sign には使えても、verify には使えない。
解決策としては
別途公開鍵をインポートする
pkcs8 でインポートした秘密鍵から無理やり公開鍵を作る
秘密鍵なので下のようにsignであれば動く。しかしverifyは公開鍵の仕事なので動かないということのようだ。しかしながらECDSAであれば秘密鍵から公開鍵が導出できるので、verifyもできてもよさそうに思ったのだが、そうはいかないようだ。
インポートする時点で keyUsage に ['verify'] を入れるとコケる。
(ちゃんとは検証していないが ['sign', 'verify'] のように指定するとインポートまでは動くが、他の操作時コケて挙動がよくわからない)
code:ts
crypto.subtle.importKey(
privateKey,
{ name: "ECDSA", namedCurve: "P-256" }, // 楕円曲線DSA (secp256r1)
true,
)
仕様を読んだらちゃんと書いてあった。
Web Cryptography API は比較的ローレイヤーな設計なのだろう。気が利いた利便性よりも、メソッドの役割の一貫性を揃えているように思う。
そしてそもそも署名と検証とを同一の場所でおこなるユースケース自体が珍しいとも言える。
---
すこし横道に逸れるが、このようなケースになった具体的な状況をいちおう説明しておく。
通常、公開鍵暗号方式の使い方では相手の公開鍵を拾ってきて署名済みメッセージを検証するもの。今回のような署名も検証もどちらも同じ場所でやるシチュエーションでは公開鍵方式を使う利点がなく、共通鍵方式を使えば良いはずである。
しかしこれには理由がある。状況としてはよくあるJWTを掃き出す認可機構なのだが、現状のミニマムな実装では署名するIDPサーバーと検証するAPIサーバーが同一になっているためである。初期実装では多くがこのような状況だとは思う。
一般的にJWTは公開鍵暗号方式を採用しているケースが多いと思うが、それはIDPがサードパーティであったり、自前でIDPサーバーを置いてあったとしてもがAPIサーバーとは分離されていることが多いためである。自前であったとしても検証するだけのサーバーが署名する能力を持つ鍵を保持する必要性は薄い。権限を分離できるのはメリットである。
---
本題に戻る。ひとまず目の前の問題をどうにかしたい。
既存のAPIを利用して秘密鍵から公開鍵を導出できないかと考える。どうやら便利なメソッドは用意されておらず、いろいろ調べてみるとJWKを経由すれば無理やり変換できるというのを見つけた。賢い。
How to derive public key from private key using WebCryptoApi?
コレでたしかに公開鍵の CryptoKey を導出できるようになった。秘密鍵のみのデータを pkcs8 で持っておけばsign,verifyのどちらもできる。
code:ts
export async function derivePublicKey(privateKey: CryptoKey) {
const jwkPrivate = await crypto.subtle.exportKey('jwk', privateKey)
delete jwkPrivate.d
return crypto.subtle.importKey(
'jwk',
jwkPrivate,
{ name: 'ECDSA', namedCurve: 'P-256' },
true,
)
}
無理矢理感がすごい。しかし、やりたいことは達成できる。
---
Web Ctyptography APIはとても便利である。
Webブラウザ, Node.js, Deno それぞれで同じAPIが利用できる。実際、今回の検証のために同じコードを利用して各種ランタイムでエラーになることを検証した。ちゃんと仕様通りに実装されていて感心する。
https://scrapbox.io/files/669f5ec189937f001cc90ac3.png
参考:
How to generate a public key from a private ECDSA key?