Repository: cocagne/pysrp

Issue:
public key of verifier not available before obtaining key of user
In some SRP usages like Homekit protocol it is required to generate the verifier's public key before the user public key. Currently it is necessary to create a Verifier object in order to get the verifier public key and the Verifier object can be created only with the user public Key A as parameter.
It would be more flexible (and HomeKit compatible) to require the User public key when calling the Verifier.verify_session() api instead of in the verifier constructor.

Discussion:

Patch:
diff --git a/srp/_ctsrp.py b/srp/_ctsrp.py
index 9436728..98ae309 100644
--- a/srp/_ctsrp.py
+++ b/srp/_ctsrp.py
@@ -386,12 +386,11 @@ def create_salted_verification_key( username, password, hash_alg=SHA1, ng_type=N
 
 
 class Verifier (object):
-    def __init__(self,  username, bytes_s, bytes_v, bytes_A, hash_alg=SHA1, ng_type=NG_2048, n_hex=None, g_hex=None, bytes_b=None, k_hex=None):
+    def __init__(self,  username, bytes_s, bytes_v, bytes_A=None, hash_alg=SHA1, ng_type=NG_2048, n_hex=None, g_hex=None, bytes_b=None, k_hex=None):
         if ng_type == NG_CUSTOM and (n_hex is None or g_hex is None):
             raise ValueError("Both n_hex and g_hex are required when ng_type = NG_CUSTOM")
         if bytes_b and len(bytes_b) != 32:
             raise ValueError("32 bytes required for bytes_b")
-        self.A     = BN_new()
         self.B     = BN_new()
         self.K     = None
         self.S     = BN_new()
@@ -421,14 +420,10 @@ class Verifier (object):
 
         bytes_to_bn( self.s, bytes_s )
         bytes_to_bn( self.v, bytes_v )
-        bytes_to_bn( self.A, bytes_A )
-
-        # SRP-6a safety check
-        BN_mod(self.tmp1, self.A, N, self.ctx)
+        if bytes_A:
+            self._set_A(bytes_A)
 
-        if BN_is_zero(self.tmp1):
-            self.safety_failed = True
-        else:
+        if not self.safety_failed:
             if bytes_b:
                 bytes_to_bn( self.b, bytes_b )
             else:
@@ -441,18 +436,6 @@ class Verifier (object):
             BN_add(self.B, self.tmp1, self.tmp2)
             BN_mod(self.B, self.B, N, self.ctx)
 
-            H_bn_bn(hash_class, self.u, self.A, self.B, width=BN_num_bytes(N))
-
-            # S = (A *(v^u)) ^ b
-            BN_mod_exp(self.tmp1, self.v, self.u, N, self.ctx)
-            BN_mul(self.tmp2, self.A, self.tmp1, self.ctx)
-            BN_mod_exp(self.S, self.tmp2, self.b, N, self.ctx)
-
-            self.K = hash_class( bn_to_bytes(self.S) ).digest()
-
-            self.M     = calculate_M( hash_class, N, g, self.I, self.s, self.A, self.B, self.K )
-            self.H_AMK = calculate_H_AMK( hash_class, self.A, self.M, self.K )
-
 
     def __del__(self):
         if not hasattr(self, 'A'):
@@ -496,12 +479,40 @@ class Verifier (object):
             return (bn_to_bytes(self.s), bn_to_bytes(self.B))
 
 
-    def verify_session(self, user_M):
-        if user_M == self.M:
-            self._authenticated = True
-            return self.H_AMK
+    def verify_session(self, user_M, bytes_A=None):
+        if bytes_A:
+            self._set_A(bytes_A)
+        if not hasattr(self, 'A'):
+            raise ValueError("bytes_A must be provided through Verifier constructor or verify_session parameter.")
+        if not self.safety_failed:
+            self._derive_H_AMK()
+            if user_M == self.M:
+                self._authenticated = True
+                return self.H_AMK
+
+
+    def _set_A(self, bytes_A):
+        self.A     = BN_new()
+        bytes_to_bn( self.A, bytes_A )
+
+        # SRP-6a safety check
+        BN_mod(self.tmp1, self.A, self.N, self.ctx)
+
+        if BN_is_zero(self.tmp1):
+            self.safety_failed = True
+
+    def _derive_H_AMK(self):
+        H_bn_bn(self.hash_class, self.u, self.A, self.B, width=BN_num_bytes(self.N))
+
+        # S = (A *(v^u)) ^ b
+        BN_mod_exp(self.tmp1, self.v, self.u, self.N, self.ctx)
+        BN_mul(self.tmp2, self.A, self.tmp1, self.ctx)
+        BN_mod_exp(self.S, self.tmp2, self.b, self.N, self.ctx)
 
+        self.K = self.hash_class( bn_to_bytes(self.S) ).digest()
 
+        self.M     = calculate_M( self.hash_class, self.N, self.g, self.I, self.s, self.A, self.B, self.K )
+        self.H_AMK = calculate_H_AMK( self.hash_class, self.A, self.M, self.K )
 
 
 class User (object):
diff --git a/srp/_pysrp.py b/srp/_pysrp.py
index d59f100..854ef2f 100644
--- a/srp/_pysrp.py
+++ b/srp/_pysrp.py
@@ -257,7 +257,7 @@ def calculate_H_AMK( hash_class, A, M, K ):
 
 class Verifier (object):
 
-    def __init__(self, username, bytes_s, bytes_v, bytes_A, hash_alg=SHA1, ng_type=NG_2048, n_hex=None, g_hex=None, bytes_b=None, k_hex=None):
+    def __init__(self, username, bytes_s, bytes_v, bytes_A=None, hash_alg=SHA1, ng_type=NG_2048, n_hex=None, g_hex=None, bytes_b=None, k_hex=None):
         if ng_type == NG_CUSTOM and (n_hex is None or g_hex is None):
             raise ValueError("Both n_hex and g_hex are required when ng_type = NG_CUSTOM")
         if bytes_b and len(bytes_b) != 32:
@@ -268,6 +268,8 @@ class Verifier (object):
         self.K = None
         self._authenticated = False
 
+        self.safety_failed = False
+
         N,g        = get_ng( ng_type, n_hex, g_hex )
         hash_class = _hash_map[ hash_alg ]
         if k_hex is None:
@@ -280,23 +282,15 @@ class Verifier (object):
         self.g          = g
         self.k          = k
 
-        self.A = bytes_to_long(bytes_A)
-
-        # SRP-6a safety check
-        self.safety_failed = self.A % N == 0
+        if bytes_A:
+            self._set_A(bytes_A)
 
         if not self.safety_failed:
-
             if bytes_b:
                 self.b = bytes_to_long(bytes_b)
             else:
                 self.b = get_random_of_length( 32 )
             self.B = (k*self.v + pow(g, self.b, N)) % N
-            self.u = H(hash_class, self.A, self.B, width=len(long_to_bytes(N)))
-            self.S = pow(self.A*pow(self.v, self.u, N ), self.b, N)
-            self.K = hash_class( long_to_bytes(self.S) ).digest()
-            self.M = calculate_M( hash_class, N, g, self.I, self.s, self.A, self.B, self.K )
-            self.H_AMK = calculate_H_AMK( hash_class, self.A, self.M, self.K )
 
 
     def authenticated(self):
@@ -322,10 +316,30 @@ class Verifier (object):
             return (long_to_bytes(self.s), long_to_bytes(self.B))
 
     # returns H_AMK on success, None on failure
-    def verify_session(self, user_M):
-        if not self.safety_failed and user_M == self.M:
-            self._authenticated = True
-            return self.H_AMK
+    def verify_session(self, user_M, bytes_A=None):
+        if bytes_A:
+            self._set_A(bytes_A)
+        if not hasattr(self, 'A'):
+            raise ValueError("bytes_A must be provided through Verifier constructor or verify_session parameter.")
+        if not self.safety_failed:
+            self._derive_H_AMK()
+            if user_M == self.M:
+                self._authenticated = True
+                return self.H_AMK
+
+
+    def _set_A(self, bytes_A):
+        self.A = bytes_to_long(bytes_A)
+        # SRP-6a safety check
+        self.safety_failed = self.A % self.N == 0
+
+
+    def _derive_H_AMK(self):
+        self.u = H(self.hash_class, self.A, self.B, width=len(long_to_bytes(self.N)))
+        self.S = pow(self.A*pow(self.v, self.u, self.N ), self.b, self.N)
+        self.K = self.hash_class( long_to_bytes(self.S) ).digest()
+        self.M = calculate_M( self.hash_class, self.N, self.g, self.I, self.s, self.A, self.B, self.K )
+        self.H_AMK = calculate_H_AMK( self.hash_class, self.A, self.M, self.K )
 
 
 
diff --git a/srp/doc/srp.rst b/srp/doc/srp.rst
index b01ac62..fee30df 100644
--- a/srp/doc/srp.rst
+++ b/srp/doc/srp.rst
@@ -141,7 +141,7 @@ user.
   The standard SRP 6 protocol allows only one password attempt per 
   connection.
 
-.. class:: Verifier( username, bytes_s, bytes_v, bytes_A[, hash_alg=SHA1, ng_type=NG_2048, n_hex=None, g_hex=None] )
+.. class:: Verifier( username, bytes_s, bytes_v[, bytes_A=None, hash_alg=SHA1, ng_type=NG_2048, n_hex=None, g_hex=None] )
 
   *username* Name of the remote user being authenticated.
   
@@ -150,7 +150,10 @@ user.
   *bytes_v* Verification Key generated by :func:`create_salted_verification_key`.
   
   *bytes_A* Challenge from the remote user. Generated by
-  :meth:`User.start_authentication`  
+  :meth:`User.start_authentication`. Useful when user generates A
+  before verifier generates B. If following RFC5054 section 2.2 where
+  B is generated prior to A, then A can instead later be provided to
+  Verifier via :func:`Verifier.verify_session`.
 
   *hash_alg*, *ng_type*, *n_hex*, *g_hex* Refer to the :ref:`constants` section.
   
@@ -173,12 +176,13 @@ user.
     Return (bytes_s, bytes_B) on success or (None, None) if
     authentication has failed.
     
-  .. method:: Verifier.verify_session( user_M )
+  .. method:: Verifier.verify_session( user_M[, user_A=None] )
   
-    Complete the :class:`Verifier` side of the authentication
-    process. If the authentication succeded the return result,
-    bytes_H_AMK should be returned to the remote user. On failure,
-    this method returns None.
+    Complete the :class:`Verifier` side of the authentication process.
+    If A was generated after B as in RFC5054 Section 2.2, then bytes_A
+    can be provided here. If the authentication succeeded, bytes_H_AMK
+    should be returned to the remote user. On failure, this method
+    returns None.
     
     
 :class:`User` Objects
@@ -350,13 +354,17 @@ M = H(H(N) xor H(g), H(I), s, A, B, K)  Session key verifier
 The server stores the password verifier *v*. Authentication begins with a 
 message from the client::
 
-    client -> server: I, A = g^a
+    client -> server: I, [A = g^a]
     
-The server replies with the verifier salt and challenge::
+where public ephemeral key *A* may be provided at this point or later
+as part of verification. The server replies with the verifier salt and
+challenge::
 
-    server -> client: s, B = kv + g^b
+    server -> client: s, B = kv + g^b, [N, g]
 
-At this point, both the client and server calculate the shared session key::
+where *N* and *g* may be provided by the server or hardcoded in the
+user client. At this point, both the client and server calculate the
+shared session key::
 
      client & server: u = H(A,B)
      
@@ -372,13 +380,20 @@ At this point, both the client and server calculate the shared session key::
 Now both parties have a shared, strong session key *K*. To complete 
 authentication they need to prove to each other that their keys match::
 
-    client -> server: M = H(H(N) xor H(g), H(I), s, A, B, K)
+    client -> server: M = H(H(N) xor H(g), H(I), s, A, B, K), [A = g^a]
     server -> client: H(A, M, K)
     
-SRP 6a requires the two parties to use the following safeguards:
+Note that *A* is only provided here if it was not provided during the initial authentication request.
+
+SRP 6a requires the two parties to use the following safeguards (all of which this library automatically checks):
 
 1. The client will abort if it recieves B == 0 (mod N) or u == 0
 2. The server will abort if it detects A == 0 (mod N)
 3. The client must show its proof of K first. If the server detects that this
    proof is incorrect it must abort without showing its own proof of K
 
+Additionally, if the server provides N and g to the user, the user
+should verify the safe prime bit length is as expected, that its
+highest bit is 1 to ensure it is large, and that it is indeed a safe
+prime with the expected generator. These N and g checks are not
+currently provided by this library.


Test patch:
diff --git a/srp/test_srp.py b/srp/test_srp.py
index b49aa78..165ef90 100644
--- a/srp/test_srp.py
+++ b/srp/test_srp.py
@@ -85,12 +85,19 @@ class SRPTests( unittest.TestCase ):
         HAMK2    = svr2.verify_session( M )
         self.assertEqual(HAMK, HAMK2)
 
+        # Make sure Verifier generating B before receiving A will not change authentication.
+        svr3     =  Verifier ( uname, _s, _v, None, hash_alg, ng_type, n_hex, g_hex, bytes_b, k_hex )
+        self.assertEqual(bytes_b, svr3.get_ephemeral_secret())
+        HAMK3    =  svr3.verify_session( M, A )
+        self.assertEqual(HAMK, HAMK3)
+
         # HAMK => client
         usr.verify_session( HAMK )
         usr2.verify_session( HAMK )
 
         self.assertTrue( svr.authenticated() )
         self.assertTrue( svr2.authenticated() )
+        self.assertTrue( svr3.authenticated() )
 
         self.assertTrue( usr.authenticated() )
         self.assertTrue( svr2.authenticated() )
@@ -168,6 +175,16 @@ class SRPTests( unittest.TestCase ):
     def test_custom_k(self):
         self.doit( _ctsrp, _pysrp, _ctsrp, k_hex=six.b('5'))
 
+    def test_verifier_requires_A(self):
+        randbytes = _pysrp.long_to_bytes(_pysrp.get_random_of_length(32))
+
+        for mod in [_ctsrp, _pysrp]:
+            svr = mod.Verifier('uname', randbytes, randbytes)
+            with self.assertRaises(ValueError) as ctx:
+                svr.verify_session(randbytes)
+            self.assertIn('bytes_A', str(ctx.exception))
+
+
 #-----------------------------------------------------------------------------------
 # Performance Testing
 #