Repository: privacyidea/privacyidea

Issue:
Replace ecdsa with pyca
In u2f we are using python-ecdsa which is not safe against side channel attacks.

We should replace it with pyca/cryptography curve secp256r1.



Discussion:
Since we are only handling the verification code, it does not appear that pressing. 
The handling of the secret key is only done in the U2F device (or for our test cases)

Resolving patch:
diff --git a/privacyidea/lib/tokens/u2f.py b/privacyidea/lib/tokens/u2f.py
--- a/privacyidea/lib/tokens/u2f.py
+++ b/privacyidea/lib/tokens/u2f.py
@@ -25,11 +25,11 @@
 import base64
 import logging
 import time
-import ecdsa
 import struct
 import six
-from cryptography.hazmat.primitives.asymmetric.utils import (encode_dss_signature,
-                                                             decode_dss_signature)
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives import hashes
+from cryptography.exceptions import InvalidSignature
 
 from privacyidea.lib.utils import (to_bytes, to_unicode, hexlify_and_unicode,
                                    urlsafe_b64encode_and_unicode)
@@ -199,47 +199,6 @@ def check_registration_data(attestation_cert, app_id,
     return True
 
 
-def sign_challenge(user_priv_key, app_id, client_data, counter,
-                   user_presence_byte=b'\x01'):
-    """
-    This creates a signature for the U2F data.
-    Only used in test scenario
-
-    The calculation of the signature is described here:
-    https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html#authentication-response-message-success
-
-    The input_data is a concatenation of:
-        * AppParameter: sha256(app_id)
-        * The user presence [1byte]
-        * counter [4byte]
-        * ChallengeParameter: sha256(client_data)
-
-    :param user_priv_key: The private key
-    :type user_priv_key: hex string
-    :param app_id: The application id
-    :type app_id: str
-    :param client_data: the stringified JSON
-    :type client_data: str
-    :param counter: the authentication counter
-    :type counter: int
-    :param user_presence_byte: one byte 0x01
-    :type user_presence_byte: char
-    :return: The DER encoded signature
-    :rtype: hex string
-    """
-    app_id_hash = sha256(to_bytes(app_id)).digest()
-    client_data_hash = sha256(to_bytes(client_data)).digest()
-    counter_bin = struct.pack(">L", counter)
-    input_data = app_id_hash + user_presence_byte + counter_bin + \
-                 client_data_hash
-    priv_key_bin = binascii.unhexlify(user_priv_key)
-    sk = ecdsa.SigningKey.from_string(priv_key_bin, curve=ecdsa.NIST256p,
-                                      hashfunc=sha256)
-    signature = sk.sign(input_data)
-    der_sig = der_encode(signature)
-    return hexlify_and_unicode(der_sig)
-
-
 def check_response(user_pub_key, app_id, client_data, signature,
                    counter, user_presence_byte=b'\x01'):
     """
@@ -271,67 +230,22 @@ def check_response(user_pub_key, app_id, client_data, signature,
     counter_bin = struct.pack(">L", counter)
     signature_bin = binascii.unhexlify(signature)
 
-    input_data = app_id_hash + user_presence_byte + counter_bin \
-                 + client_data_hash
+    input_data = app_id_hash + user_presence_byte + counter_bin + client_data_hash
 
-    # The first byte 0x04 only indicates, that the public key is in the
-    # uncompressed format x: 32 byte, y: 32byte
-    user_pub_key_bin = user_pub_key_bin[1:]
-    signature_bin_asn = der_decode(signature_bin)
-    vkey = ecdsa.VerifyingKey.from_string(user_pub_key_bin,
-                                          curve=ecdsa.NIST256p,
-                                          hashfunc=sha256)
     try:
-        vkey.verify(signature_bin_asn, input_data)
-    except ecdsa.BadSignatureError:
+        vkey = ec.EllipticCurvePublicKey.from_encoded_point(ec.SECP256R1(),
+                                                            user_pub_key_bin)
+        vkey.verify(signature_bin, input_data, ec.ECDSA(hashes.SHA256()))
+    except (ValueError, TypeError) as e:
+        log.error("Could not load application specific public key!")
+        log.debug('{0!s}'.format(e))
+        res = False
+    except InvalidSignature:
         log.error("Bad signature for app_id {0!s}".format(app_id))
         res = False
     return res
 
 
-def der_encode(signature_bin_asn):
-    """
-    This encodes a raw signature to DER.
-    It uses the encode_dss_signature() function from cryptography.
-
-    :param signature_bin_asn: RAW signature
-    :type signature_bin_asn: bytes
-    :return: DER encoded signature
-    :rtype: bytes
-    """
-    if len(signature_bin_asn) != 64:
-        raise Exception("The signature needs to be 64 bytes.")
-    vr = int(binascii.hexlify(signature_bin_asn[:32]), 16)
-    vs = int(binascii.hexlify(signature_bin_asn[32:]), 16)
-    signature_bin = encode_dss_signature(vr, vs)
-    return signature_bin
-
-
-def der_decode(signature_bin):
-    """
-    This decodes a DER encoded signature so that it can be used with ecdsa.
-    It uses the decode_dss_signature() function from cryptography.
-
-    :param signature_bin: DER encoded signature
-    :type signature_bin: bytes
-    :return: raw signature
-    :rtype: bytes
-    """
-    try:
-        r, s = decode_dss_signature(signature_bin)
-        sig_bin_asn = binascii.unhexlify('{0:064x}{1:064x}'.format(r, s))
-    except ValueError as _e:
-        raise Exception("The signature is not in supported DER format.")
-
-    # we can only check for too long signatures since we prepend the hex-values
-    # with '0' to reach 64 digits. This will prevent an error in case the one of
-    # the values (r, s) is smaller than 32 bytes (first byte is '0'
-    # in original value).
-    if len(sig_bin_asn) != 64:
-        raise Exception("The signature needs to be 64 bytes.")
-    return sig_bin_asn
-
-
 def x509name_to_string(x509name):
     """
     converts a X509Name to a string as in a DN


Test patch:
diff --git a/tests/test_api_u2f.py b/tests/test_api_u2f.py
--- a/tests/test_api_u2f.py
+++ b/tests/test_api_u2f.py
@@ -1,14 +1,20 @@
+# -*- coding: utf-8 -*-
+
 from .base import MyApiTestCase
-import json
 import binascii
+import struct
+from hashlib import sha256
+from cryptography.hazmat.primitives.asymmetric import ec
+from cryptography.hazmat.primitives import hashes
+
 from privacyidea.lib.token import assign_token, remove_token
 from privacyidea.lib.user import User
 from privacyidea.lib.config import set_privacyidea_config
-from privacyidea.lib.tokens.u2f import (sign_challenge, check_response,
-                                        url_encode)
+from privacyidea.lib.tokens.u2f import (check_response, url_encode)
 from privacyidea.lib.policy import set_policy, delete_policy, SCOPE
 from privacyidea.lib.tokens.u2ftoken import U2FACTION
 from privacyidea.models import Challenge
+from privacyidea.lib.utils import hexlify_and_unicode, to_bytes
 from privacyidea.lib.error import ERROR
 from privacyidea.lib import _
 
@@ -22,6 +28,44 @@
 OTPVALUES2 = ["551536", "703671", "316522", "413789"]
 
 
+def sign_challenge(user_priv_key, app_id, client_data, counter,
+                   user_presence_byte=b'\x01'):
+    """
+    This creates a signature for the U2F data.
+    Only used in test scenario
+
+    The calculation of the signature is described here:
+    https://fidoalliance.org/specs/fido-u2f-v1.0-nfc-bt-amendment-20150514/fido-u2f-raw-message-formats.html#authentication-response-message-success
+
+    The input_data is a concatenation of:
+        * AppParameter: sha256(app_id)
+        * The user presence [1byte]
+        * counter [4byte]
+        * ChallengeParameter: sha256(client_data)
+
+    :param user_priv_key: The private key
+    :type user_priv_key: hex string
+    :param app_id: The application id
+    :type app_id: str
+    :param client_data: the stringified JSON
+    :type client_data: str
+    :param counter: the authentication counter
+    :type counter: int
+    :param user_presence_byte: one byte 0x01
+    :type user_presence_byte: char
+    :return: The DER encoded signature
+    :rtype: hex string
+    """
+    app_id_hash = sha256(to_bytes(app_id)).digest()
+    client_data_hash = sha256(to_bytes(client_data)).digest()
+    counter_bin = struct.pack(">L", counter)
+    input_data = app_id_hash + user_presence_byte + counter_bin + client_data_hash
+
+    sk = ec.derive_private_key(int(user_priv_key, 16), ec.SECP256R1())
+    signature = sk.sign(input_data, ec.ECDSA(hashes.SHA256()))
+    return hexlify_and_unicode(signature)
+
+
 class APIU2fTestCase(MyApiTestCase):
 
     serial = "U2F001"
diff --git a/tests/test_lib_tokens_u2f.py b/tests/test_lib_tokens_u2f.py
--- a/tests/test_lib_tokens_u2f.py
+++ b/tests/test_lib_tokens_u2f.py
@@ -6,8 +6,7 @@
 from privacyidea.lib.tokens.u2ftoken import U2fTokenClass
 from privacyidea.lib.tokens.u2f import (check_registration_data,
                                         parse_registration_data, url_decode,
-                                        check_response, parse_response_data,
-                                        der_encode, der_decode)
+                                        check_response, parse_response_data)
 from privacyidea.lib.token import init_token, remove_token, check_user_pass
 from privacyidea.lib.user import User
 from privacyidea.lib.policy import set_policy, SCOPE, ACTION, delete_policy
@@ -252,41 +251,6 @@ def test_04_broken_reg_data(self):
                                      'The registration data is in a wrong format.'):
             parse_registration_data(reg_data)
 
-    def test_05_der_signature(self):
-        import os
-        r = os.urandom(32)
-        s = os.urandom(32)
-
-        # get a valid signature
-        sig_der_bin = der_encode(r + s)
-
-        # check for wrong signature length
-        broken_sig_bin_raw = r + s[1:]
-        with self.assertRaisesRegexp(Exception,
-                                     "The signature needs to be 64 bytes."):
-            der_encode(broken_sig_bin_raw)
-
-        # decode valid signature
-        sig_bin = der_decode(sig_der_bin)
-        self.assertEqual(sig_bin[:32], r)
-        self.assertEqual(sig_bin[32:], s)
-
-        # decode broken signature
-        broken_sig_hex = '304502207f3e632dc9fd607aa9e04c9130f1ddff0d44cf3f2dc3' \
-                         '639b194a720815454d39022053883079bc3fcacd8f178a0bd40d' \
-                         '88ef1286195665c8bd78300a15c23aee5f8c'
-        with self.assertRaisesRegexp(Exception,
-                                     "The signature is not in supported DER format."):
-            der_decode(binascii.unhexlify(broken_sig_hex))
-
-        # invalid signature length
-        long_sig_hex = '3048022201027f3e632dc9fd607aa9e04c9130f1ddff0d44cf3f2' \
-                       'dc3639b194a720815454d390222010253883079bc3fcacd8f178a' \
-                       '0bd40d88ef1286195665c8bd78300a15c23aee5f8c'
-        with self.assertRaisesRegexp(Exception,
-                                     "The signature needs to be 64 bytes."):
-            der_decode(binascii.unhexlify(long_sig_hex))
-
 
 class MultipleU2FTokenTestCase(MyTestCase):
     app_id = 'https://localhost:5000'