エラーメッセージ
java.security.SignatureException: Invalid encoding for signature
`Caused by` の詳細は、どのバイトで DER の解析が失敗したかによって変わります(たとえば「Invalid lenByte」や「Not the correct tag」)。逆の間違いはエラーになりません。DER 形式の署名を `SHA256withECDSAinP1363Format` に渡しても例外はスローされず、検証結果が `false` になるだけです。
Java (JDK) — Signature.verify with SHA256withECDSA (SunEC)
このエラーの意味
`SHA256withECDSA` は ASN.1 DER 形式(`30 … 02 r 02 s`)の ECDSA 署名を前提としていますが、渡されたバイト列は DER ではありません。ほとんどの場合、それはもう一方の標準エンコーディング、つまり JWS、WebCrypto、Java 以外の多くのライブラリが使う固定長の r‖s 連結(IEEE P1363)で、P-256 では 64 バイトです。JDK はこの形式も、アルゴリズム名 `SHA256withECDSAinP1363Format` で検証できます。
発生する理由
生の r‖s 署名を DER として検証している
よくあるJWT(ES256)、`crypto.subtle.sign`、r と s を手作業で連結するコード、ブロックチェーンのツールから得た署名は、64/96/132 バイトの生のバイト列です。Java のデフォルトの ECDSA アルゴリズム名は DER を前提としています。
署名のバイト列が正しくデコードされていない
ときどきbase64url の JWS セグメントを(文字の置き換えを誤ったまま)Basic デコーダーでデコードしたり、16 進数を base64 としてデコードしたりすると、署名とはまったく関係のないバイト列になります。
RSA 署名を ECDSA の検証者に渡している
ときどき256 バイトの RSA 署名も DER ではありません。検証者を選ぶ前に、トークンや文書のアルゴリズムを確認してください。
修正方法
- 1.
生の署名は P1363 のアルゴリズムで検証する
JDK 9 以降の SunEC プロバイダーで、SHA-1/224/256/384/512 と ECDSA の組み合わせに対して利用できます。変換は不要です。
java // JWS (ES256) and WebCrypto subtle.sign() produce raw r||s; WebAuthn and X.509 use DER. Signature verifier = Signature.getInstance("SHA256withECDSAinP1363Format"); verifier.initVerify(publicKey); verifier.update(signedBytes); boolean ok = verifier.verify(rawSignature); // 64 bytes for P-256 - 2.
または r‖s を DER に変換する
API が DER を必須とする場合(たとえば HSM プロバイダー)は、r と s を最小長の DER INTEGER として SEQUENCE に格納してください。DER ⇄ raw 変換ツールで、変換結果を確認できます。
java static byte[] p1363ToDer(byte[] raw) { int n = raw.length / 2; byte[] r = derInteger(java.util.Arrays.copyOfRange(raw, 0, n)); byte[] s = derInteger(java.util.Arrays.copyOfRange(raw, n, raw.length)); var out = new java.io.ByteArrayOutputStream(); out.write(0x30); out.write(r.length + s.length); // < 128 for P-256 and P-384 out.writeBytes(r); out.writeBytes(s); return out.toByteArray(); } static byte[] derInteger(byte[] v) { int i = 0; while (i < v.length - 1 && v[i] == 0) i++; boolean pad = (v[i] & 0x80) != 0; byte[] out = new byte[2 + (pad ? 1 : 0) + v.length - i]; out[0] = 0x02; out[1] = (byte) (out.length - 2); System.arraycopy(v, i, out, pad ? 3 : 2, v.length - i); return out; }