本文へスキップ
ecdsa.com

ライブラリ相互運用チートシート

ライブラリ A で署名し、ライブラリ B で検証すると、何が問題になるのでしょうか。両側のライブラリを選ぶと、署名のエンコーディング、high-S の扱い、公開鍵の形式、決定的 nonce について具体的な不一致のチェックリストが表示され、それぞれに修正用のコードが 1 行ずつ添えられます。

静的な参照データのみで、入力もネットワーク通信もなく、外部に送信されるものは何もありません

openssl dgst / openssl pkeyutl

ブラウザー、Deno、Node の webcrypto

署名者
検証者
ライブラリ
OpenSSL CLI
WebCrypto(crypto.subtle)
署名
ASN.1 DER
生の r‖s(P1363)
low-S
両方を受理 · 正規化せずに署名
両方を受理 · 正規化せずに署名
RFC 6979
オプトイン
不使用(ランダムな nonce)
公開鍵
SPKI PEM
JWK
曲線
P-256, P-384, P-521, secp256k1
P-256, P-384, P-521

OpenSSL CLI で署名 → WebCrypto(crypto.subtle) で検証

動作させるには 2 件の修正が必要
動作しない

署名形式の不一致:出力は「ASN.1 DER」、想定は「生の r‖s」

OpenSSL CLI はデフォルトで「ASN.1 DER」を出力し、WebCrypto(crypto.subtle) は「生の r‖s」を想定しています。鍵や計算に誤りはなく、単に検証者がバイト列を読み取れないだけです。可変長の DER のバイト列は、固定長を前提とするパーサーにとって長さが合いません。

どちらの側で修正してもかまいません。2 つのエンコーディングが持つ情報は同一なので、変換で情報が失われることはなく、暗号処理も一切伴いません。

署名側OpenSSL CLI から「生の r‖s」を出力

組み込みのオプションなし

CLI には ECDSA 署名を P1363 形式で出力する機能がありません。DER 構造から 2 つの INTEGER を取り出し、それぞれ曲線のサイズ(P-256 では 32 バイト)になるよう左側をゼロで埋めてください。`openssl asn1parse -inform DER -in sig.der` で r と s を表示できるので、変換結果を目視で確認できます。

検証側WebCrypto(crypto.subtle) で「ASN.1 DER」を受理

組み込みのオプションなし

DER モードはありません。固定長の出力を半分に分け、それぞれの先頭のゼロを取り除いてから 2 つの整数を DER の SEQUENCE で包み、OpenSSL 系の検証者に渡してください。

† 動作はバージョンやビルドによって異なります。お使いのバージョンで確認してください。

関連情報: DER ⇄ 生形式の変換ツール · ECDSA 署名の形式:DER と生形式の比較

要確認

公開鍵の形式の不一致:「SPKI PEM」と「JWK」

OpenSSL CLI は公開鍵を「SPKI PEM」でエクスポートし、WebCrypto(crypto.subtle) は「JWK」を想定しています。これらは実際に異なる入れ物です。SPKI は点を曲線の識別子と一緒に包み、JWK は x 座標と y 座標を base64url のフィールドとして記述し、SEC1 形式の点は座標のバイト列そのものです。中身の鍵素材は 3 つとも同一です。

OpenSSL CLI:`openssl ec -pubin -in pub.pem -pubout -outform DER` で同じ鍵を生の SPKI DER として取得でき、`openssl ec -pubin -in pub.pem -text -noout` で SEC1 形式の非圧縮の点を 16 進数で表示できます。

WebCrypto(crypto.subtle):importKey("spki", derBytes, …) が受け付けるのは SPKI の *DER* です。PEM のテキストそのものは拒否されるため、先に PEM のヘッダーとフッターを取り除き、base64 デコードしてください。

関連情報: ECDSA 署名検証ツール

参考情報

P-256 / P-384 / P-521 を使用(両側が対応する曲線)

対応する曲線が一致しません:secp256k1 は OpenSSL CLI のみ。共通の曲線上の鍵を選べば、この問題は起きません。

OpenSSL CLI:ビルドで有効になっているすべての曲線に対応します。NIST 素体曲線、secp256k1、Brainpool などです。お使いのバイナリが対応する曲線の正確な一覧は `openssl ecparam -list_curves` で確認できます。

WebCrypto(crypto.subtle):NIST の 3 つの曲線のみです。secp256k1 は仕様に含まれていないため、Bitcoin や Ethereum の鍵には代わりに JavaScript ライブラリが必要です。

参考情報

OpenSSL CLI は RFC 6979 にオプトインしない限りランダムな nonce を使用

通常の署名処理ではランダムな nonce を使います。3.2 以降は、署名パラメーター `nonce-type:1` で決定的 ECDSA(RFC 6979)を選択できます。その際はダイジェストを明示的に指定し、`openssl dgst -sha256 -sign key.pem -sigopt nonce-type:1` または `pkeyutl -pkeyopt nonce-type:1 -pkeyopt digest:sha256` のように実行します。

これはバグではなく正しい動作です。ただし、同じ鍵とメッセージでも実行ごとに異なるバイト列になるため、署名をバイト単位で比較するテストは失敗します。代わりに検証結果を比較してください。

† 動作はバージョンやビルドによって異なります。お使いのバージョンで確認してください。

すべてのライブラリを並べて比較

デフォルトの動作のみを示しています。どの行も、フラグや別の呼び出しで変更できます。すべての列を見るには横にスクロールしてください。

ライブラリデフォルトの署名形式切り替え方法low-S ポリシーRFC 6979デフォルトの公開鍵形式
OpenSSL CLI
openssl dgst / openssl pkeyutl
署名者
ASN.1 DER
ASN.1 DER 形式です。2 つの INTEGER からなる SEQUENCE であるため、P-256 の署名は通常 70〜72 バイトで、長さは署名ごとに変わります。
CLI には生の r‖s を出力するモードがありません。DER の出力を外部で変換してください。†
両方を受理
署名・検証のどちらにも low-S ポリシーはありません。約半数の署名で s が上半分の範囲に入り、検証時にもそれは一切問題にされません。
オプトイン
通常の署名処理ではランダムな nonce を使います。3.2 以降は、署名パラメーター `nonce-type:1` で決定的 ECDSA(RFC 6979)を選択できます。その際はダイジェストを明示的に指定し、`openssl dgst -sha256 -sign key.pem -sigopt nonce-type:1` または `pkeyutl -pkeyopt nonce-type:1 -pkeyopt digest:sha256` のように実行します。†
SPKI PEM
SPKI PEM:`openssl ec -pubout` が出力する `-----BEGIN PUBLIC KEY-----` ブロックです。なお、`-----BEGIN EC PUBLIC KEY-----` というブロックは存在しません。EC のラベルが付いたブロックは秘密鍵です。
Node.js node:crypto
crypto.sign / crypto.verify / KeyObject
ASN.1 DER
もう一方のエンコーディングを明示的に指定しない限り、ASN.1 DER です。
鍵オブジェクトに `dsaEncoding: "ieee-p1363"` を指定します。署名時と検証時の*両方*で設定してください。
両方を受理
s がどちらの半分にあっても受理し、正規化は一切行いません。内部は OpenSSL です。
不使用(ランダムな nonce)
ランダムな nonce を使います。同じ鍵とメッセージでも、呼び出すたびに署名のバイト列が変わります。
SPKI PEM
一般的なのは `publicKey.export({ type: "spki", format: "pem" })` の形式です。JWK も `export({ format: "jwk" })` を 1 回呼び出すだけで得られます。
WebCrypto(crypto.subtle)
ブラウザー、Deno、Node の webcrypto
検証者
生の r‖s(P1363)
生の r‖s のみで、長さは固定です(P-256 では 64 バイト、P-384 では 96 バイト、P-521 では 132 バイト)。仕様では他のエンコーディングは定義されていません。
切り替えるものはありません。WebCrypto は DER を一切扱いません。API の外で変換してください。
両方を受理
署名時にも検証時にも low-S の規則はありません。
不使用(ランダムな nonce)
ランダムな nonce を使い、独自の nonce を渡す手段は API にありません。
JWK
インポートとエクスポートのたびに形式を指定します。「jwk」、「spki」(DER のバイト列)、「raw」(SEC1 形式の非圧縮の点)のいずれかです。Web のコードで最もよく受け渡されるのは JWK です。
Go crypto/ecdsa
SignASN1 / VerifyASN1、または big.Int のペア
ASN.1 DER
`SignASN1`(`crypto.Signer` が使う経路)は ASN.1 DER を返します。低レベルの `Sign` は r と s を `*big.Int` として返し、エンコードするまではどちらのワイヤーフォーマットでもありません。
DER には `SignASN1`/`VerifyASN1` を、固定長の生形式には `Sign`/`Verify` と `FillBytes` を使います。
両方を受理
low-S の処理は一切ありません。検証では範囲のどちらの半分も受理します。
不使用(ランダムな nonce)
RFC 6979 とは互換性がありません。最近のバージョンは鍵、メッセージ、新しい乱数を組み合わせて nonce を導出するため、署名のバイト列は実行ごとに異なり、テストで固定値として扱うことはできません。†
SPKI DER
`x509.MarshalPKIXPublicKey` は SPKI DER を生成します。PEM ブロックには `pem.EncodeToMemory` を 1 回呼び出すだけで変換できます。
Python cryptography(pyca)
hazmat.primitives.asymmetric.ec
ASN.1 DER
DER です。`private_key.sign(...)` は ASN.1 の SEQUENCE を返します。
`decode_dss_signature` / `encode_dss_signature` で DER と整数のペアを相互に変換できます。固定長にするためのパディングは自分で追加する必要があります。
両方を受理
どちらの半分も受理し、s を正規化する処理はありません。
オプトイン
デフォルトではランダムな nonce を使います。最近のリリースでは ECDSA オブジェクトに決定的署名のフラグを指定できますが、この経路では内部に新しい OpenSSL が必要です。頼る前に、お使いの wheel がどの OpenSSL に対してビルドされたかを確認してください。†
SPKI PEM
`public_bytes(Encoding.PEM, PublicFormat.SubjectPublicKeyInfo)` で、OpenSSL が出力するものと同じ PEM ブロックを得られます。
Python ecdsa(純 Python 実装)
SigningKey / VerifyingKey、sigencode= と sigdecode=
生の r‖s(P1363)
生の r‖s:`sign()` は 2 つの値を曲線のサイズに合わせてゼロパディングし、連結して返します。
署名と検証の呼び出しごとに、`sigencode=` と `sigdecode=` 引数でエンコーディングを選びます。
両方を受理
どちらの半分も受理します。このライブラリには s を必ず下半分に収める正規化エンコーダーも同梱されていますが、通常のエンコーダーと並んで用意されているだけなので、明示的に選ぶ必要があります。†
オプトイン
RFC 6979 の決定的署名は、別の署名メソッドとして利用できます。通常の sign() 呼び出しは乱数を使います。
SEC1 形式の点
鍵は点そのものとして受け渡されます。`VerifyingKey.to_string()` はデフォルトで 0x04 プレフィックスのない x‖y を返すため、SPKI を前提とするスタックはそのままでは受け付けません。†
Java JCA(SunEC)
java.security.Signature
ASN.1 DER
`SHA256withECDSA` は ASN.1 DER を生成します。
アルゴリズム名の末尾に `inP1363Format` を付けます。クラスは同じで、ワイヤーフォーマットだけが変わります。
両方を受理
署名・検証のどちらにも low-S ポリシーはありません。
不使用(ランダムな nonce)
SunEC はランダムな nonce で署名します。決定的 ECDSA を使うには、Bouncy Castle などのサードパーティのプロバイダーを追加します。†
SPKI DER
`publicKey.getEncoded()` は X.509 SPKI DER を返します。JDK は PEM に対応していないため、base64 で包む処理は自分で行う必要があります。
Rust p256 / k256(RustCrypto)
ecdsa::Signature, SigningKey, VerifyingKey
生の r‖s(P1363)
デフォルトの `Signature` 型は固定長のペアで、P-256 と secp256k1 では 64 バイトです。DER には別途、明示的な変換が必要です。†
固定長のペアがデフォルトの型です。DER との変換は、どちらの方向も明示的に行います。†
両方を受理
secp256k1 用のクレートは署名時に s を下半分に正規化し、Bitcoin の慣例に合わせています。NIST 曲線用のクレートは正規化しません。検証はどちらも受理するため、相手側が厳格な場合は署名側で正規化してください。†
デフォルトで使用
デフォルトで RFC 6979 の決定的 nonce を使います。同じ鍵とメッセージからは常に同じバイト列が得られます。ランダム化した版はオプトインで利用できます。
SEC1 形式の点
`VerifyingKey` は SEC1 でエンコードされた点にシリアライズされます(圧縮形式で 33 バイト、非圧縮形式で 65 バイト)。SPKI DER と PEM は、付随する SPKI/PKCS#8 のトレイトで利用できます。†
libsecp256k1(Bitcoin Core)
secp256k1_ecdsa_sign / _verify
自分で選択
シリアライズするまではどちらでもありません。署名は内部の不透明な構造体に保持され、出力時にコンパクト形式(生の r‖s)か DER かを選びます。
生の r‖s にはコンパクト形式、Bitcoin のワイヤーフォーマットには DER でシリアライズします。どちらにするかは呼び出しごとに選びます。
low-S を強制
厳格なライブラリです。署名は常に low-S で出力され、検証では high-S の署名を即座に拒否します。汎用の署名者から届いた署名を修正できるよう、このライブラリにはまさにそのための正規化関数が用意されています。
デフォルトで使用
デフォルトで RFC 6979 を使います。必要なら別の nonce 関数を渡せます。
SEC1 形式の点
SEC1 形式の点です(圧縮形式で 33 バイト、非圧縮形式で 65 バイト)。API のどこにも SPKI、PEM、JWK はありません。
PHP openssl_*
openssl_sign / openssl_verify
ASN.1 DER
DER です。`openssl_sign()` は OpenSSL が生成したものをそのまま返します。
生形式のモードは一切ありません。ユーザーランドで変換するか、変換を行う JOSE ライブラリを使ってください。
両方を受理
low-S ポリシーはありません。OpenSSL の動作をそのまま引き継いでいます。
不使用(ランダムな nonce)
ランダムな nonce を使います。拡張モジュールには決定的なモードがありません。
SPKI PEM
入出力とも PEM 文字列です。`openssl_pkey_get_public()` は `-----BEGIN PUBLIC KEY-----` ブロックか証明書全体を受け取ります。
Ruby OpenSSL
OpenSSL::PKey::EC
ASN.1 DER
DER です。`key.sign(digest, data)` は ASN.1 の SEQUENCE を返します。
組み込みの生形式モードはありません。OpenSSL::ASN1 を使って手作業で変換するか、JOSE の gem に任せてください。
両方を受理
low-S ポリシーはありません。OpenSSL の動作そのままです。
不使用(ランダムな nonce)
ランダムな nonce を使います。決定的署名のオプションは用意されていません。
SPKI PEM
SPKI PEM です。最近のバージョンには公開鍵をエクスポートする専用のメソッドがあり、使う価値があります。秘密鍵の部分を持つ鍵に対して単に `to_pem` を呼ぶと、秘密鍵が書き出されるためです。†

† 動作はバージョンやビルドによって異なります。お使いのバージョンで確認してください。

さらに詳しく

How it works

A signature that verifies in one library and fails in another is almost never wrong mathematically: the two sides simply disagree about packaging. An ECDSA signature is a pair of integers (r, s), and each ecosystem picked its own way to serialize them — ASN.1 DER in the OpenSSL and X.509 lineage, fixed-width r‖s in WebCrypto, JWS and most blockchain stacks. On top of that sit two more sources of disagreement: whether s must be in the lower half of the range (the low-S rule that Bitcoin enforces and general-purpose libraries ignore), and how the public key itself is wrapped — an SPKI structure, a JWK object, or the bare SEC1 point.

This page compares those defaults for a pair of libraries and lists only the differences that actually cause a failure, together with the switch or conversion that removes each one. Every row describes what a library does when you call the obvious API without extra arguments — not what it is capable of. Nearly all of it is configurable, which is exactly why two teams reading the same documentation end up incompatible, and why the fix is usually one option rather than new code.