1
0

JavaScriptでECDSA署名したらPythonで検証できなかった

JavaScriptはWeb Crypto APIで署名

SubtleCrypto: sign()とSubtleCrypto: verify()

PythonはCryptographyライブラリで検証

cryptography.hazmat.primitives.asymmetric.ec.ECDSA

上手くいかない。
JavaScript同士、Python同士は問題ないのでお互いのデータのやり取りがおかしい。

SubtleCrypto: sign()の署名値は

署名は RFC 6090 で指定されている s1 と s2 の値(RFC 4754 ではそれぞれ r と s として知られている)をビッグエンディアンのバイト配列でエンコードしたものです。
これらの値はこの順番で連結されます。
このエンコーダーは IEEE 1363-2000 標準でも 提案されており、 IEEE P1363 形式と呼ばれることもあります。
これは X.509 の署名構造とは異なり、 OpenSSL などのいくつかのツールやライブラリーでは既定で生成される形式です。

と書いてあります。
出てきた署名値をopensslでチェックしても検証失敗するけど?

X.509 の署名構造とは異なり
OpenSSL などのいくつかのツールやライブラリーでは既定で生成される

規定なのに・・・?

ドキュメントが嘘でした

IEEE P1363はOpenSSLで規定で生成されるどころか対応していません。
翻訳ミス?というか英語でも

This encoding was also proposed by the IEEE 1363-2000 standard, and is sometimes referred to as the IEEE P1363 format.
It differs from the X.509 signature structure, which is the default format produced by some tools and libraries such as OpenSSL.

幾つかの機械翻訳にかけみたら

これはX.509署名構造とは異なり、OpenSSLなどのいくつかのツールやライブラリが生成するデフォルトのフォーマットである。

これはX.509署名構造とは異なり、これはOpenSSLなどのいくつかのツールやライブラリが生成するデフォルトのフォーマットである。

これは、OpenSSL などの一部のツールやライブラリによって生成されるデフォルトの形式である X.509 署名構造とは異なります。

それはX.509署名構造と異なります。X.509はOpenSSLのようなツールやライブラリによって生成されるデフォルトの形式です。

といった感じで、“which is”が指し示すのはX.509かIEEE P1363なのかややこしい?

それはそれとして、 Cryptographyのリファレンスみれば

ちゃんと

DER encoded as described in RFC 3279.

って書いてあるので、違う形式だろって気づけるんですけど。

ちゃんと対応しよう

サポートする形式が合わない以上はどちらかに合わせることとなる。
ここではOpenSSLでもいけるRFC 3279 (X.509)にする。

これは他にも理由があって変換コードを書くのに不慣れなPythonより慣れてるTypeScriptで書きたかった。

簡単に言うとP1363は署名値二つのバイト列を結合しているだけ。
RFC 3279は署名値二つをDERで格納している。

かなり雑に作っているけどコードはこんな感じ

const asn1toEcSig = (der:Uint8Array,keySize:number) => {
    if(der[0] !== 0x30){
        return;
    }
    const filedLen = getDerLen(der);
    const seq = der.subarray(2+filedLen[0],2+filedLen[0]+filedLen[1]);
    const rPointLen = getDerLen(seq);
    const rPointDat = paddingInt(seq.subarray(2+rPointLen[0],2+rPointLen[0]+rPointLen[1]),keySize);
    const nextSeq = seq.subarray(2+rPointLen[0]+rPointLen[1]);
    const sPointLen = getDerLen(nextSeq);
    const sPointDat = paddingInt(nextSeq.subarray(2+sPointLen[0],2+sPointLen[0]+sPointLen[1]),keySize);
    return new Uint8Array([...rPointDat,...sPointDat]);
}
const ecSigToASN1Sig = (sig:ArrayBufferLike) => {
    if(sig.byteLength % 2 != 0){
        return;
    }
    const rPointDat = trimArray(new Uint8Array(sig.slice(0,sig.byteLength/2)));
    const rPointLen = getASN1Len(rPointDat.byteLength);
    const rPointBlk = new Uint8Array(1+rPointLen.byteLength+rPointDat.byteLength);
    rPointBlk.set([0x02],0);
    rPointBlk.set(rPointLen,1);
    rPointBlk.set(rPointDat,1+rPointLen.byteLength);
    const sPointDat = trimArray(new Uint8Array(sig.slice(sig.byteLength/2)));
    const sPointLen = getASN1Len(sPointDat.byteLength);
    const sPointBlk = new Uint8Array(1+sPointLen.byteLength+sPointDat.byteLength);
    sPointBlk.set([0x02],0);
    sPointBlk.set(sPointLen,1);
    sPointBlk.set(sPointDat,1+sPointLen.byteLength);
    const seqLen = getASN1Len(rPointBlk.byteLength+sPointBlk.byteLength);
    const ans1Bytes = new Uint8Array(1+seqLen.byteLength+rPointBlk.byteLength+sPointBlk.byteLength);
    let seq = 0;
    ans1Bytes.set([0x30],seq);seq++;
    ans1Bytes.set(seqLen,seq);seq += seqLen.byteLength;
    ans1Bytes.set(rPointBlk,seq);seq += rPointBlk.byteLength;
    ans1Bytes.set(sPointBlk,seq);seq += sPointBlk.byteLength;
    return ans1Bytes;
}
const getASN1Len = (len:number) => {
    if(len<128){
        return new Uint8Array([len]);
    }else{
        const lenlen = Math.floor(len/256)+1;
        const lenBuf = new Uint8Array(1+lenlen);
        lenBuf[0] = 128+lenlen;
        for(let p=1;p<=lenlen;p++){
            lenBuf[p] = len&0xFF
            len = len >> 8;
        }
        return lenBuf;
    }
}
const getDerLen = (der:Uint8Array) => {
    if(der[1]<128){
        return [0,der[1]];
    }else{
        const lenlen = der[1] & 0x7f;
        const size = der.subarray(2,2+lenlen);
        const len = size.reverse().reduce((p,c,i)=>{
            p += c*(256**i);
            return p;
        },0);
        return [lenlen,len]
    }
}
const trimArray = (buf:Uint8Array) => {
    const _temp = [];
    let trim = false;
    for(let i=0;i<buf.byteLength;i++){
        if(buf[i] === 0x00){
            trim = true;
            continue;
        }
        _temp.push(...buf.subarray(i));
        break;
    }
    if(!trim || _temp[0] < 0x80){
        return new Uint8Array(_temp);
    }else{
        return new Uint8Array([0x00,..._temp]);
    }
}
const paddingInt = (uint:Uint8Array,sizeBit:number) => {
    const byteLen = Math.floor((sizeBit+7) / 8);
    const byteDat = new Uint8Array(byteLen);
    byteDat.set(uint,byteLen-uint.byteLength);
    return byteDat;
}
1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0