使用 EC 在 JS(前端)中签署有效负载并在 Python 中验证

Signing payload in JS (Frontend) using EC and validating in Python

我有一个 Python 后端生成 public/private 密钥,生成有效负载,然后需要让客户端(ReactJS 或纯 JS)签署该有效负载,稍后进行验证。

Python 中的实现如下所示:

进口

import json
import uuid

from backend.config import STARTING_BALANCE
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 import hashes, serialization
from cryptography.exceptions import InvalidSignature

from cryptography.hazmat.primitives.serialization import load_pem_private_key

import base64
import hashlib

生成密钥:

class User:
    def __init__(self):
        self.address = hashlib.sha1(str(str(uuid.uuid4())[0:8]).encode("UTF-8")).hexdigest()
        self.private_key = ec.generate_private_key(
            ec.SECP256K1(),
            
            default_backend()
        )

        self.private_key_return = self.private_key.private_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PrivateFormat.TraditionalOpenSSL,
            encryption_algorithm=serialization.NoEncryption()
        )

        self.public_key = self.private_key.public_key()

        self.serialize_public_key()

    def serialize_public_key():
        """
        Reset the public key to its serialized version.
        """
        self.public_key = self.public_key.public_bytes(
            encoding=serialization.Encoding.PEM,
            format=serialization.PublicFormat.SubjectPublicKeyInfo
        ).decode('utf-8')

签名:

def sign(self, data):
    """
    Generate a signature based on the data using the local private key.
    """
    return decode_dss_signature(self.private_key.sign(
        json.dumps(data).encode('utf-8'),
        ec.ECDSA(hashes.SHA256())
    ))

验证:

@staticmethod
def verify(public_key, data, signature):
    """
    Verify a signature based on the original public key and data.
    """
    deserialized_public_key = serialization.load_pem_public_key(
        public_key.encode('utf-8'),
        default_backend()
    )

    (r, s) = signature

    try:
        deserialized_public_key.verify(
            encode_dss_signature(r, s),
            json.dumps(data).encode('utf-8'),
            ec.ECDSA(hashes.SHA256())    
        )
        return True
    except InvalidSignature:
        return False

我现在需要的是在客户端加载(甚至生成)PEM 密钥,然后根据请求签署一个 JSON 有效载荷,稍后可以从 Python后端。

我尝试研究网络密码学和 cryptoJS 的用法,但没有成功。

我可以使用另一种更兼容的算法,但至少我需要签名功能完全正常工作。

我还尝试使用 Brython 和 Pyodide 将 Python 编译为 JS,但两者都无法支持所有必需的包。

简单来说,我正在寻找以下内容:

生成有效负载 (Python) -----> 签署有效负载 (JS) -----> 验证签名 (Python)

任何 help/advice 将不胜感激。

CryptoJS only supports symmetric encryption and therefore not ECDSA. WebCrypto 支持 ECDSA,但不支持 secp256k1。
WebCrypto 的优点是所有主流浏览器都支持它。由于您可以根据您的评论使用其他曲线,因此我将描述一种使用 WebCrypto 支持的曲线的解决方案。
否则,sjcl would also be an alternative, a pure JavaScript library that supports ECDSA and especially secp256k1, s.here.

WebCrypto 是一个低级别 API,它提供您需要的功能,如密钥生成、密钥导出和签名。关于 ECDSA WebCrypto 支持曲线 P-256(又名 secp256r1)、P-384(又名 secp384r1)和 p-521(又名 secp521r1)。下面我用P-256.


以下JavaScript代码为P-256生成密钥对,导出public X.509/SPKI格式的密钥,DER编码(因此可以发送到Python 站点),并在消息上签名:

(async () => {

    // Generate key pair
    var keypair = await window.crypto.subtle.generateKey(
        {
            name: "ECDSA",
            namedCurve: "P-256", // secp256r1 
        },
        false,
        ["sign", "verify"] 
    );
  
    // Export public key in X.509/SPKI format, DER encoded
    var publicKey = await window.crypto.subtle.exportKey(
        "spki", 
        keypair.publicKey 
    );  
    document.getElementById("pub").innerHTML = "Public key: " + ab2b64(publicKey);
  
    // Sign data
    var data = {
        "data_1":"The quick brown fox",
        "data_2":"jumps over the lazy dog"
    }
    var dataStr = JSON.stringify(data) 
    var dataBuf = new TextEncoder().encode(dataStr).buffer
    var signature = await window.crypto.subtle.sign(
        {
            name: "ECDSA",
            hash: {name: "SHA-256"}, 
        },
        keypair.privateKey, 
        dataBuf 
    ); 
    document.getElementById("sig").innerHTML = "Signature: " + ab2b64(signature);
 
})();

// Helper
function ab2b64(arrayBuffer) {
    return window.btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer)));
}
<p style="font-family:'Courier New', monospace;" id="pub"></p>
<p style="font-family:'Courier New', monospace;" id="sig"></p>

可能的输出是:

Public key: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWzC5lPNifcHNuKL+/jjhrtTi+9gAMbYui9Vv7TjtS7RCt8p6Y6zUmHVpGEowuVMuOSNxfpJYpnGExNT/eWhuwQ==
Signature: XRNTbkHK7H8XPEIJQhS6K6ncLPEuWWrkXLXiNWwv6ImnL2Dm5VHcazJ7QYQNOvWJmB2T3rconRkT0N4BDFapCQ==

在 Python 方面,可以通过以下方式成功验证:

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature
from cryptography.hazmat.primitives.serialization import load_der_public_key
from cryptography.hazmat.primitives import hashes
from cryptography.exceptions import InvalidSignature
import base64
import json

publikKeyDer = base64.b64decode("MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWzC5lPNifcHNuKL+/jjhrtTi+9gAMbYui9Vv7TjtS7RCt8p6Y6zUmHVpGEowuVMuOSNxfpJYpnGExNT/eWhuwQ==")
data = {
  "data_1":"The quick brown fox",
  "data_2":"jumps over the lazy dog"
}
signature = base64.b64decode("XRNTbkHK7H8XPEIJQhS6K6ncLPEuWWrkXLXiNWwv6ImnL2Dm5VHcazJ7QYQNOvWJmB2T3rconRkT0N4BDFapCQ==")

publicKey = load_der_public_key(publikKeyDer, default_backend())
r = int.from_bytes(signature[:32], byteorder='big')
s = int.from_bytes(signature[32:], byteorder='big')

try:
    publicKey.verify(
        encode_dss_signature(r, s),
        json.dumps(data, separators=(',', ':')).encode('utf-8'),
        ec.ECDSA(hashes.SHA256())    
    )
    print("verification succeeded")
except InvalidSignature:
    print("verification failed")

其中,与张贴的 Python 代码不同,load_der_public_key() is used instead of load_pem_public_key().

此外,WebCrypto returns IEEE P1363 格式的签名,但作为串联的 ArrayBuffer r|s,因此必须将两个部分转换为整数才能将格式转换为ASN.1/DER encode_dss_signature().

关于JSON,必须将分隔符重新定义为最紧凑的表示形式(但这取决于JavaScript 端的设置)。