【问题标题】:Signing payload in JS (Frontend) using EC and validating in Python使用 EC 在 JS(前端)中签名有效负载并在 Python 中进行验证
【发布时间】:2021-12-17 14:48:38
【问题描述】:

我有一个 Python 后端,它生成公钥/私钥,生成一个有效负载,然后需要让该有效负载由客户端(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)

任何帮助/建议将不胜感激。

【问题讨论】:

  • 在 SO 上,图书馆的推荐是题外话。您需要一个支持 secp256k1 的 ECDSA 的 JavaScript 库。 CryptoJS 和 WebCrypto 都不这样做。 CryptoJS 不支持非对称加密,WebCrypto 不支持 secp256k1(但仅支持 secp256r1 aka P-256)。
  • 在我的问题中,我简单地提到我尝试了这两种算法,并且我也愿意使用其他算法。我只是找不到任何有关在两种语言之间签名数据的文档。
  • 考虑到我改用 secp256r1,您能否就上述内容提供任何指导? (似乎在 Python 中受支持,无需对代码进行任何重大更改)。
  • 在这里您可以找到使用 WebCrypto/ECDSA 进行签名的示例:github.com/diafygi/webcrypto-examples#ecdsa---sign(以及其他示例,例如密钥生成或密钥导入/导出)。 WebCrypto 有点麻烦,所以如果您有问题,请发布您的代码并描述问题。如果 NodeJS 也是一种选择,NodeJS 的加密模块是另一种选择:nodejs.org/api/crypto.html#signsignprivatekey-outputencoding
  • 一个选项可以是在浏览器端使用浏览器扩展。参考stackoverflow.com/a/68556286/9659885stackoverflow.com/a/63173083/9659885

标签: javascript python cryptography cryptojs webcrypto-api


【解决方案1】:

CryptoJS 仅支持对称加密,因此不支持 ECDSA。 WebCrypto 支持 ECDSA,但不支持 secp256k1。
WebCrypto 的优点是所有主流浏览器都支持它。由于您可以根据您的评论使用其他曲线,因此我将描述一个 WebCrypto 支持的曲线的解决方案。
否则,sjcl 也将是一个替代方案,一个支持 ECDSA 尤其是 secp256k1、s.here 的纯 JavaScript 库。

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


以下 JavaScript 代码为 P-256 生成一个密钥对,以 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() 代替 load_pem_public_key()

此外,WebCrypto 以 IEEE P1363 格式返回签名,但作为连接的 ArrayBuffer r|s,因此需要将两个部分转换为整数,以允许使用 @987654336 将格式转换为 ASN.1/DER @。

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

【讨论】:

  • 太棒了!这个例子是我一直在寻找的。根据最初的 cmets,我已经达到了可以工作的程度,但验证一直失败。现在我看到这是在 2 之间传递的数据格式的问题。你的例子就像一个魅力!现在我正在尝试找到一种在 JS 中存储/加载密钥的方法(可能会与 IndexedDB + 用户可以保留的“文本”副本一起使用)。对此有何想法/建议?
  • @Waelmas - 密钥管理是一个复杂的话题,值得单独提出一个问题。讨论了用于密钥的 WebCrypto 和索引数据库,例如herehere.
  • 感谢您提供的信息!会尝试一下,如果我遇到困难,我可能会问一个新问题。
猜你喜欢
  • 1970-01-01
  • 2018-08-08
  • 1970-01-01
  • 2019-08-22
  • 2019-04-23
  • 2017-12-08
  • 2020-09-29
  • 2021-09-05
  • 1970-01-01
相关资源
最近更新 更多