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