AntonKueltz / fastecdsa

Python library for fast elliptic curve crypto
https://pypi.python.org/pypi/fastecdsa
The Unlicense
264 stars 77 forks source link

-Make message a bytestring to allow for signature of binary messages #25

Closed sirk390 closed 5 years ago

sirk390 commented 5 years ago

Careful with this because it is not backward compatible :) I would like to discuss the possibility of signing binary messages. You could do it by implementing ".encode()" to do nothing, but that seems a hack while signature of binary message is probably the default use-case.

AntonKueltz commented 5 years ago

Probably not a bad idea, I'm going to investigate using instanceof(msg, str) to do conditional encoding for str input to preserve current behavior and not break any behavior for existing users. Will get back to you shortly.

AntonKueltz commented 5 years ago

The following changes should also give the required behavior while being backwards compatible, can you verify that they work as needed?

Thanks, Anton

diff --git a/fastecdsa/ecdsa.py b/fastecdsa/ecdsa.py
index 899a77c..5282e28 100644
--- a/fastecdsa/ecdsa.py
+++ b/fastecdsa/ecdsa.py
@@ -4,7 +4,7 @@ from hashlib import sha256  # Python standard lib SHA2 is already in C
 from fastecdsa import _ecdsa
 from .curve import P256
 from .point import Point
-from .util import RFC6979
+from .util import RFC6979, msg_bytes

 class EcdsaError(Exception):
@@ -19,7 +19,7 @@ def sign(msg, d, curve=P256, hashfunc=sha256):
     refer to http://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.186-4.pdf for more information.

     Args:
-        |  msg (str): A message to be signed.
+        |  msg (str|bytes|bytearray): A message to be signed.
         |  d (long): The ECDSA private key of the signer.
         |  curve (fastecdsa.curve.Curve): The curve to be used to sign the message.
         |  hashfunc (_hashlib.HASH): The hash function used to compress the message.
@@ -28,7 +28,7 @@ def sign(msg, d, curve=P256, hashfunc=sha256):
     rfc6979 = RFC6979(msg, d, curve.q, hashfunc)
     k = rfc6979.gen_nonce()

-    hashed = hashfunc(msg.encode()).hexdigest()
+    hashed = hashfunc(msg_bytes(msg)).hexdigest()
     r, s = _ecdsa.sign(
         hashed,
         str(d),
@@ -51,7 +51,7 @@ def verify(sig, msg, Q, curve=P256, hashfunc=sha256):

     Args:
         |  sig (long, long): The signature for the message.
-        |  msg (str): A message to be signed.
+        |  msg (str|bytes|bytearray): A message to be signed.
         |  Q (fastecdsa.point.Point): The ECDSA public key of the signer.
         |  curve (fastecdsa.curve.Curve): The curve to be used to sign the message.
         |  hashfunc (_hashlib.HASH): The hash function used to compress the message.
@@ -77,7 +77,7 @@ def verify(sig, msg, Q, curve=P256, hashfunc=sha256):
         raise EcdsaError(
             'Invalid Signature: s is not a positive integer smaller than the curve order')

-    hashed = hashfunc(msg.encode()).hexdigest()
+    hashed = hashfunc(msg_bytes(msg)).hexdigest()
     return _ecdsa.verify(
         str(r),
         str(s),
diff --git a/fastecdsa/keys.py b/fastecdsa/keys.py
index 12c1ae8..eb24fc4 100644
--- a/fastecdsa/keys.py
+++ b/fastecdsa/keys.py
@@ -5,7 +5,7 @@ from os import urandom
 from .curve import P256
 from .ecdsa import verify
 from .point import Point
-from .util import mod_sqrt
+from .util import mod_sqrt, msg_bytes

 def gen_keypair(curve):
@@ -83,7 +83,7 @@ def get_public_keys_from_sig(sig, msg, curve=P256, hashfunc=sha256):

     Args:
         |  sig (long, long): A ECDSA signature.
-        |  msg (str): The message corresponding to the signature.
+        |  msg (str|bytes|bytearray): The message corresponding to the signature.
         |  curve (fastecdsa.curve.Curve): The curve used to sign the message.
         |  hashfunc (_hashlib.HASH): The hash function used to compress the message.

@@ -94,7 +94,7 @@ def get_public_keys_from_sig(sig, msg, curve=P256, hashfunc=sha256):
     r, s = sig
     rinv = pow(r, curve.q - 2, curve.q)

-    z = int(hashfunc(msg.encode()).hexdigest(), 16)
+    z = int(hashfunc(msg_bytes(msg)).hexdigest(), 16)
     hash_bit_length = hashfunc().digest_size * 8
     if curve.q.bit_length() < hash_bit_length:
         z >>= (hash_bit_length - curve.q.bit_length())
diff --git a/fastecdsa/util.py b/fastecdsa/util.py
index d7a838a..55aa364 100644
--- a/fastecdsa/util.py
+++ b/fastecdsa/util.py
@@ -122,3 +122,28 @@ def mod_sqrt(a, p):
         return x, (-x % p)
     else:
         return _tonelli_shanks(a, p)
+
+
+def msg_bytes(msg):
+    """Return bytes in a consistent way for a given message.
+
+    The message is expected to be either a string, bytes, or an array of bytes.
+
+    Args:
+        |  msg (str|bytes|bytearray): The data to transform.
+
+    Returns:
+        bytes: The byte encoded data.
+
+    Raises:
+        ValueError: If the data cannot be encoded as bytes.
+    """
+    if isinstance(msg, bytes):
+        return msg
+    elif isinstance(msg, str) or isinstance(msg, unicode):
+        return msg.encode()
+    elif isinstance(msg, bytearray):
+        return bytes(msg)
+    else:
+        raise ValueError('Msg "{}" of type {} cannot be converted to bytes'.format(
+            msg, type(msg)))
sirk390 commented 5 years ago

Hi, yes this look very good, but I don't see the change in "util.py:57" wich would become: h1 = self.hashfunc(msg_bytes(self.msg))

AntonKueltz commented 5 years ago

Will commit above changes (along with the missing line 57 in util.py) and fold them into the next release. Will close this pull request once committed and reference the commit in the closing comment.

AntonKueltz commented 5 years ago

Added in 55c0890a95dd8cd0f9fc4fe7994111153b41a81c and bada3acd2ee9580efa2fd5aff4a9b0f8192e70af.