javascriptpythonreactjscryptographyecdsa

Cross-Environment ECDSA Signature Verification Fails Between React and Python


I'm working on a project where I need to generate an ECDSA signature in a React application and then verify it in a Python backend. The signature generation and verification work within their respective environments, but the signature generated in React fails verification in Python. I'm using the elliptic library in React and the cryptography library in Python.

React Code (Signature Generation):

import { useEffect } from 'react';
import { ec as EC } from 'elliptic';

const App = () => {
  useEffect(() => {
    const ec = new EC('secp256k1');
    const private_key_hex = "cf63f7ffe346cd800e431b34bdbd45f6aac3c2ac6055ac18195753aff9b9cce8";
    const message_hash_hex = "2f8fc7172db7dcbd71ec70c83263db33a54ff761b02a54480a8d07b9c633d651";

    const privateKey = ec.keyFromPrivate(private_key_hex, 'hex');
    const signature = privateKey.sign(message_hash_hex, 'hex', { canonical: true });
    const signature_der_hex = signature.toDER('hex');

    console.log("Signature (DER hex):", signature_der_hex);
  }, []);

  return <div><h1>ECDSA Signature in React</h1></div>;
};

export default App;

yielding:

Signature (DER hex): 3045022100e5c678f346cdd180815912e580c27d9d70a4a2e71ab6cfb2bdaedfbf4cdf24cc02200bba6ae9b3bb25c886b5cc8549ac6796438f295e91320a1d705f17e25cb7199b

And here the part where I am trying to verify the signature in python:

from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.backends import default_backend

def verify_signature(public_key_hex, signature_der_hex, message_hash_hex):
    public_key_bytes = bytes.fromhex(public_key_hex)
    try:
        public_key = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256K1(), public_key_bytes)
    except Exception as e:
        print(f"Error loading public key: {e}")
        return False

    hashed_message = bytes.fromhex(message_hash_hex)
    try:
        signature_bytes = bytes.fromhex(signature_der_hex)
        public_key.verify(signature_bytes, hashed_message, ec.ECDSA(hashes.SHA256()))
        return True
    except Exception as e:
        print(f"Verification failed: {e}")
        return False

public_key_hex = "03aa4fcbf0792ce77a3be9415bf96bff99a832466f56ecba3bb8ce630877de2701"

# Replace with signature from React
signature_der_hex = "3045022100e5c678f346cdd180815912e580c27d9d70a4a2e71ab6cfb2bdaedfbf4cdf24cc02200bba6ae9b3bb25c886b5cc8549ac6796438f295e91320a1d705f17e25cb7199b"  

message_hash_hex = "2f8fc7172db7dcbd71ec70c83263db33a54ff761b02a54480a8d07b9c633d651"
verification_result = verify_signature(public_key_hex, signature_der_hex, message_hash_hex)
print("Verification:", verification_result)

Problem: When I use the signature generated by the React app in the Python verification code, the verification fails. However, when I run the signing and verification process separately within each environment, it succeeds.

What I've Tried:

I suspect the issue might be with how the signature is encoded or with some nuances in how the libraries handle ECDSA operations. Any insights or suggestions on how to make these two environments compatible would be greatly appreciated.


Solution

  • In contrast to the elliptic NodeJS library, the pyca/cryptography Python library hashes implicitly by default. Since you pass the hash in both cases, the once-hashed message is signed in the NodeJS code, but the twice-hashed message is verified in the Python code, which is why verification fails.
    In order for the verification with Python to be successful, the implicit hashing must be disabled, which is done as follows:

    from cryptography.hazmat.primitives.asymmetric import utils
    ...
    public_key.verify(signature_bytes, hashed_message, ec.ECDSA(utils.Prehashed(hashes.SHA256())))
    

    With this change, verification with the Python code is successful.