4

I have a Python backend that generates public/private keys, generates a payload, then needs to get that payload signed by the client (ReactJS or pure JS), which is later verified.

The implementation in Python looks like this:

Imports

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

Generate keys:

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')

Sign:

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())
    ))

Verify:

@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

What I need now is to load (or even generate) the PEM keys on the client-side, then upon request, sign a JSON payload that can later be verified from the Python backend.

I tried looking into the usage of web cryptography and cryptoJS but had no luck.

I'm okay using another algorithm that is more compatible, but at the very least I need the signing functionality fully working.

I also tried compiling Python to JS using Brython and Pyodide but both could not support all the required packages.

In simple terms, I am looking for the following:

Generate Payload (Python) -----> Sign Payload (JS) -----> Verify Signature (Python)

Any help/advice would be greatly appreciated.

Topaco
  • 40,594
  • 4
  • 35
  • 62
Waelmas
  • 1,894
  • 1
  • 9
  • 19
  • On SO the recommendation of libraries is off topic. You need a JavaScript library that supports ECDSA for secp256k1. CryptoJS and WebCrypto both don't do that. [CryptoJS](https://cryptojs.gitbook.io/docs/#ciphers) does not support asymmetric encryption, [WebCrypto](https://developer.mozilla.org/en-US/docs/Web/API/EcKeyImportParams#properties) does not support secp256k1 (but only secp256r1 aka P-256). – Topaco Dec 17 '21 at 16:30
  • In my question, I mentioned simply that I tried both and that I am willing to use other algorithms as well. I just could not find any documentation regarding signing data between the 2 languages. – Waelmas Dec 17 '21 at 18:40
  • Could you provide any guidance on the above considering I use secp256r1 instead? (Seems supported in Python without any major changes to the code). – Waelmas Dec 17 '21 at 18:43
  • Here you can find an example for signing with WebCrypto/ECDSA: https://github.com/diafygi/webcrypto-examples#ecdsa---sign (as well as other examples e.g. for key generation or key import/export). WebCrypto is a bit cumbersome, so if you have problems, post your code and describe the problem. If NodeJS is also an option, the crypto module of NodeJS is another alternative: https://nodejs.org/api/crypto.html#signsignprivatekey-outputencoding – Topaco Dec 17 '21 at 18:58
  • One option can be using browser extension on browser side. Refer https://stackoverflow.com/a/68556286/9659885 and https://stackoverflow.com/a/63173083/9659885 – Bharat Vasant Dec 18 '21 at 06:04

1 Answers1

1

CryptoJS only supports symmetric encryption and therefore not ECDSA. WebCrypto supports ECDSA, but not secp256k1.
WebCrypto has the advantage that it is supported by all major browsers. Since you can use other curves according to your comment, I will describe a solution with a curve supported by WebCrypto.
Otherwise, sjcl would also be an alternative, a pure JavaScript library that supports ECDSA and especially secp256k1, s.here.

WebCrypto is a low level API that provides the functionality you need like key generation, key export and signing. Regarding ECDSA WebCrypto supports the curves P-256 (aka secp256r1), P-384 (aka secp384r1) and p-521 (aka secp521r1). In the following I use P-256.


The following JavaScript code generates a key pair for P-256, exports the public key in X.509/SPKI format, DER encoded (so it can be sent to the Python site), and signs a message:

(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>

A possible output is:

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

On the Python side a successful verification would be possible with:

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")

Where, unlike the posted Python code, load_der_public_key() is used instead of load_pem_public_key().

Also, WebCrypto returns the signature in IEEE P1363 format, but as a concatenated ArrayBuffer r|s, so a conversion of both parts to an integer is necessary to allow a format conversion to ASN.1/DER with encode_dss_signature().

Regarding JSON the separators have to be redefined to the most compact representation (but this depends on the settings on the JavaScript side).

Topaco
  • 40,594
  • 4
  • 35
  • 62
  • Amazing! This example was what I've been looking for. Based on the initial comments I had reached a point where it works but verification kept failing. Now I see it's a matter of the format of the data being passed between the 2. Your example works like a charm! Now I'm trying to find a way to store/load the keys in JS (probably will go with IndexedDB + a "text" copy the user can keep). Any ideas/suggestions on that? – Waelmas Dec 18 '21 at 15:50
  • 1
    @Waelmas - Key management is a complex topic and worth a separate question. WebCrypto and Indexed DB for keys is discussed e.g. [here](https://crypto.stackexchange.com/a/52488) and [here](https://security.stackexchange.com/a/169399). – Topaco Dec 18 '21 at 17:15
  • Thank you for the info! Will experiment a bit and if I get stuck I might ask a new question. – Waelmas Dec 18 '21 at 17:31
  • Any chance you could shed some light on doing the opposite similarly to the above? What data transformations need to occur on both ends? Shall I post a new question on how to achieve the opposite? (Sign with Python, verify with JS) – Waelmas Apr 10 '22 at 15:54
  • Here is a question with all details @Topaco https://stackoverflow.com/questions/71818496/ecsda-sign-with-python-verify-with-js – Waelmas Apr 10 '22 at 16:26