ECSDA用Python签名,用JS验证

ECSDA sign with Python, verify with JS

我正在尝试实现与 完全相反的效果,我需要使用 ECDSA 在 Python 中签署有效负载并能够在 JS 中验证签名。

这是我的尝试,但我很确定我遗漏了两端或两端的数据转换。

(密钥类型与上述问题的答案相同)

我尝试了一些其他变体,但到目前为止没有任何效果。

(JS上的验证returns错误)

Python:

import os
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import (
    encode_dss_signature,
    decode_dss_signature
)
from cryptography.hazmat.primitives.serialization import load_der_public_key, load_pem_private_key, load_der_private_key

from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives import hashes
from cryptography.exceptions import InvalidSignature
import base64
import json
from hashlib import sha256



def order_dict(dictionary):
    return {k: order_dict(v) if isinstance(v, dict) else v
            for k, v in sorted(dictionary.items())}


async def sign_payload(private_key, data):
    """
    Generate a signature based on the data using the local private key.
    """
    data = order_dict(data)

    # Separators prevent adding whitespaces around commas and :
    payload = json.dumps(data, separators=(',', ':')).encode('utf-8')

    # payload = base64.b64decode(json.dumps(data, separators=(',', ':')))

    sig = private_key.sign(
        payload,
        ec.ECDSA(hashes.SHA256())
    )

    return sig

JS:

export function b642ab(base64_string){
  return Uint8Array.from(window.atob(base64_string), c => c.charCodeAt(0));
}


export async function verifySignature(signature, public_key, data_in) {
  // Sorting alphabetically to avoid signature mismatch with BE
  const sorted_data_in = sortObjKeysAlphabetically(data_in);

  var dataStr = JSON.stringify(sorted_data_in)
  console.log(dataStr)

  var dataBuf = new TextEncoder().encode(dataStr)

  return window.crypto.subtle.verify(
    {
      name: "ECDSA",
      namedCurve: "P-256",
      hash: { name: "SHA-256" },
    },
    public_key,
    b642ab(utf8.decode(signature)),
    dataBuf
  );
}

await sign_payload(private_dsa_key, generated_payload)

主要问题是两个代码使用不同的签名格式:
Python代码中的sign_payload()生成ASN.1/DER格式的ECDSA签名。另一方面,WebCrypto API 只能处理 IEEE P1363 格式。
由于 Python 密码库比低级 WebCrypto API 更方便,因此在 Python 代码中进行转换是有意义的。

以下Python代码基于您的代码,但在最后额外进行了IEEE P1363格式的转换:

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import decode_dss_signature
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.hazmat.primitives import hashes
import base64
import json

#def order_dict(dictionary):
#    return {k: order_dict(v) if isinstance(v, dict) else v
#            for k, v in sorted(dictionary.items())}

def sign_payload(private_key, data):
    """
    Generate a signature based on the data using the local private key.
    """    
    
    #order_dict(data) # not considered!

    # Separators prevent adding whitespaces around commas and :
    payload = json.dumps(data, separators=(',', ':')).encode('utf-8')
    print(payload.decode('utf-8'))

    sig = private_key.sign(
        payload,
        ec.ECDSA(hashes.SHA256())
    )

    return sig

privateKeyPem = b'''-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgrW9XiIs4/Kb0q8kl
TmF3oIwSn4NO3xAjs08F0lJ/5UOhRANCAAQykdP4c0ozvOOHHSNkMfLNCWRstXTG
TQf9MWjqB9PbeKyHnxuU82FisUjnVD9zO+QDAK0tnP/qzWf8zxoD0vVW
-----END PRIVATE KEY-----'''

privateKey = load_pem_private_key(privateKeyPem, password=None, backend=default_backend())
data = {"key1": "value1", "key2": "value2"}
signatureDER = sign_payload(privateKey, data)

# Convert signature format
(r, s) = decode_dss_signature(signatureDER)
signatureP1363 = r.to_bytes(32, byteorder='big') + s.to_bytes(32, byteorder='big')
print(base64.b64encode(signatureP1363).decode('utf-8'))

可能的输出是:

{"key1":"value1","key2":"value2"}
KIkBK4pxSFq/UdsPb/mYCC3y7iAJlULC/jizNp9DrvFFIvZaUjx/M0SAQC7CeBIlLmKzfkGx1fOr7OJ8VlwAdg==

请注意,对于此测试,order_dict(data) 调用已被注释掉,因为 JavaScript 对应的调用未发布。


在JavaScript代码中,去掉Base64解码签名时的utf8.decode()。除此之外,代码还可以。以下 JavaScript 代码基于您的代码,添加了密钥导入:

(async () => {

     var x509pem = `-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEMpHT+HNKM7zjhx0jZDHyzQlkbLV0
xk0H/TFo6gfT23ish58blPNhYrFI51Q/czvkAwCtLZz/6s1n/M8aA9L1Vg==
-----END PUBLIC KEY-----`
     var public_key = await importPublicKey(x509pem)
     var data_in = {
         key1: "value1",
         key2: "value2"
     }
     var signature = "KIkBK4pxSFq/UdsPb/mYCC3y7iAJlULC/jizNp9DrvFFIvZaUjx/M0SAQC7CeBIlLmKzfkGx1fOr7OJ8VlwAdg=="
     var verified = await verifySignature(signature, public_key, data_in)
     console.log(verified);
  
})();

function b642ab(base64_string){
     return Uint8Array.from(window.atob(base64_string), c => c.charCodeAt(0));
}

async function verifySignature(signature, public_key, data_in) {
    // Sorting alphabetically to avoid signature mismatch with BE
  
    //const sorted_data_in = sortObjKeysAlphabetically(data_in);
    //var dataStr = JSON.stringify(sorted_data_in)
    var dataStr = JSON.stringify(data_in)
    console.log(dataStr)

    var dataBuf = new TextEncoder().encode(dataStr)
  
    return window.crypto.subtle.verify(
        {
            name: "ECDSA",
            namedCurve: "P-256",
            hash: { name: "SHA-256" },
        },
        public_key,
        b642ab(signature),
        dataBuf
    );
}

async function importPublicKey(spkiPem) {   
    return await window.crypto.subtle.importKey(
        "spki",
        getSpkiDer(spkiPem),
        {name: "ECDSA", namedCurve: "P-256"},
        false,
        ["verify"]
    );
}

function getSpkiDer(spkiPem){
    const pemHeader = "-----BEGIN PUBLIC KEY-----";
    const pemFooter = "-----END PUBLIC KEY-----";
    var pemContents = spkiPem.substring(pemHeader.length, spkiPem.length - pemFooter.length);
    var binaryDerString = window.atob(pemContents);
    return str2ab(binaryDerString); 
}

function str2ab(str) {
    const buf = new ArrayBuffer(str.length);
    const bufView = new Uint8Array(buf);
    for (let i = 0, strLen = str.length; i < strLen; i++) {
        bufView[i] = str.charCodeAt(i);
    }
    return buf;
}

使用 Python 代码生成的签名可以通过 JavaScript 代码成功验证消息。

请注意 - 类似于 Python 代码 - sortObjKeysAlphabetically() 由于缺少实现而被注释掉。