···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+open Crowbar
77+88+let () = Crypto_rng_unix.use_default ()
99+1010+(* Test that matching passwords produce matching session keys *)
1111+let test_protocol_roundtrip username password salt =
1212+ if String.length username = 0 || String.length password = 0 then ()
1313+ else if String.length salt < 8 then ()
1414+ else
1515+ let salt = String.sub salt 0 (min 16 (String.length salt)) in
1616+ let verifier = Srp.compute_verifier ~salt ~username ~password in
1717+ let client = Srp.Client.create ~username ~password in
1818+ let big_a = Srp.Client.public_key client in
1919+ let server = Srp.Server.create ~username ~salt ~verifier in
2020+ let big_b = Srp.Server.public_key server in
2121+ match
2222+ ( Srp.Client.compute_session_key client ~salt ~big_b,
2323+ Srp.Server.compute_session_key server ~big_a )
2424+ with
2525+ | Ok key_c, Ok key_s -> check_eq ~pp:Format.pp_print_string key_c key_s
2626+ | Error (`Msg e), _ -> failwith ("Client error: " ^ e)
2727+ | _, Error (`Msg e) -> failwith ("Server error: " ^ e)
2828+2929+(* Test that verifier computation is deterministic *)
3030+let test_verifier_deterministic username password salt =
3131+ if String.length username = 0 || String.length password = 0 then ()
3232+ else if String.length salt < 8 then ()
3333+ else
3434+ let salt = String.sub salt 0 (min 16 (String.length salt)) in
3535+ let v1 = Srp.compute_verifier ~salt ~username ~password in
3636+ let v2 = Srp.compute_verifier ~salt ~username ~password in
3737+ check (Z.equal v1 v2)
3838+3939+let () =
4040+ add_test ~name:"srp: protocol roundtrip" [ bytes; bytes; bytes ] (fun u p s ->
4141+ test_protocol_roundtrip u p s);
4242+ add_test ~name:"srp: verifier deterministic" [ bytes; bytes; bytes ]
4343+ (fun u p s -> test_verifier_deterministic u p s)
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(** SRP-6a Secure Remote Password Protocol (RFC 5054)
77+88+ This implementation uses the 3072-bit group from RFC 5054 with SHA-512 as
99+ the hash function, as required by HomeKit.
1010+1111+ {1 Security Notice}
1212+1313+ {b WARNING: This implementation has known timing side-channel
1414+ vulnerabilities.}
1515+1616+ The modular exponentiation uses Zarith for big integer arithmetic, which is
1717+ {e not constant-time}. For high-security deployments, consider using
1818+ hardware security modules or ensuring operations occur on trusted networks
1919+ only. *)
2020+2121+(** {1 Constants} *)
2222+2323+(** RFC 5054 3072-bit group prime N *)
2424+let n_hex =
2525+ "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF"
2626+2727+(** Generator g = 5 *)
2828+let g = Z.of_int 5
2929+3030+(** Group prime N *)
3131+let n = Z.of_string_base 16 n_hex
3232+3333+(** {1 Utility Functions} *)
3434+3535+(** Hash data using SHA-512 *)
3636+let hash data =
3737+ Digestif.SHA512.digest_string data |> Digestif.SHA512.to_raw_string
3838+3939+(** Reverse a string (for endianness conversion) *)
4040+let string_rev s =
4141+ let len = String.length s in
4242+ String.init len (fun i -> s.[len - 1 - i])
4343+4444+(** Convert bytes (big-endian) to Z *)
4545+let z_of_bytes s =
4646+ (* Z.of_bits is little-endian, SRP uses big-endian *)
4747+ Z.of_bits (string_rev s)
4848+4949+(** Convert Z to bytes (big-endian), optionally padding to specified length *)
5050+let bytes_of_z ?(pad = 0) z =
5151+ (* Z.to_bits is little-endian, SRP uses big-endian *)
5252+ let s = string_rev (Z.to_bits z) in
5353+ (* Remove leading zeros that Z.to_bits may have added *)
5454+ let s =
5555+ let rec skip i =
5656+ if i >= String.length s - 1 then String.length s - 1
5757+ else if s.[i] = '\x00' then skip (i + 1)
5858+ else i
5959+ in
6060+ String.sub s (skip 0) (String.length s - skip 0)
6161+ in
6262+ if pad > 0 && String.length s < pad then
6363+ String.make (pad - String.length s) '\x00' ^ s
6464+ else s
6565+6666+(** Compute k = H(N | PAD(g)) *)
6767+let compute_k () =
6868+ let n_bytes = bytes_of_z n in
6969+ let g_bytes = bytes_of_z ~pad:(String.length n_bytes) g in
7070+ z_of_bytes (hash (n_bytes ^ g_bytes))
7171+7272+(** Compute x = H(salt | H(username | ":" | password)) *)
7373+let compute_x ~salt ~username ~password =
7474+ let inner = hash (username ^ ":" ^ password) in
7575+ z_of_bytes (hash (salt ^ inner))
7676+7777+(** Compute verifier v = g^x mod N *)
7878+let compute_verifier ~salt ~username ~password =
7979+ let x = compute_x ~salt ~username ~password in
8080+ Z.powm g x n
8181+8282+(** {1 Client} *)
8383+8484+module Client = struct
8585+ type t = {
8686+ username : string;
8787+ password : string;
8888+ a : Z.t; (* private random *)
8989+ big_a : Z.t; (* A = g^a mod N *)
9090+ }
9191+9292+ let create ~username ~password =
9393+ let a_bytes = Crypto_rng.generate 32 in
9494+ let a = Z.(z_of_bytes a_bytes mod n) in
9595+ let big_a = Z.powm g a n in
9696+ { username; password; a; big_a }
9797+9898+ let public_key t = t.big_a
9999+100100+ let compute_session_key t ~salt ~big_b =
101101+ if Z.(equal (big_b mod n) zero) then Error (`Msg "Invalid B value")
102102+ else
103103+ let k = compute_k () in
104104+ let n_len = (Z.numbits n + 7) / 8 in
105105+ let a_pad = bytes_of_z ~pad:n_len t.big_a in
106106+ let b_pad = bytes_of_z ~pad:n_len big_b in
107107+ let u = z_of_bytes (hash (a_pad ^ b_pad)) in
108108+ if Z.(equal u zero) then Error (`Msg "Invalid u value")
109109+ else
110110+ let x = compute_x ~salt ~username:t.username ~password:t.password in
111111+ (* S = (B - k * g^x)^(a + u * x) mod N *)
112112+ let base = Z.((big_b - (k * powm g x n)) mod n) in
113113+ let base = if Z.(lt base zero) then Z.(base + n) else base in
114114+ let exp = Z.((t.a + (u * x)) mod (n - one)) in
115115+ let s = Z.powm base exp n in
116116+ let session_key = hash (bytes_of_z s) in
117117+ Ok session_key
118118+119119+ let compute_proof t ~salt ~big_b ~session_key =
120120+ let h_n = hash (bytes_of_z n) in
121121+ let h_g = hash (bytes_of_z g) in
122122+ let h_xor =
123123+ String.init (String.length h_n) (fun i ->
124124+ Char.chr (Char.code h_n.[i] lxor Char.code h_g.[i]))
125125+ in
126126+ let h_user = hash t.username in
127127+ let n_len = (Z.numbits n + 7) / 8 in
128128+ hash
129129+ (h_xor ^ h_user ^ salt
130130+ ^ bytes_of_z ~pad:n_len t.big_a
131131+ ^ bytes_of_z ~pad:n_len big_b
132132+ ^ session_key)
133133+134134+ let verify_proof t ~m1 ~m2 ~session_key =
135135+ let n_len = (Z.numbits n + 7) / 8 in
136136+ let expected = hash (bytes_of_z ~pad:n_len t.big_a ^ m1 ^ session_key) in
137137+ String.equal expected m2
138138+end
139139+140140+(** {1 Server} *)
141141+142142+module Server = struct
143143+ type t = {
144144+ username : string;
145145+ salt : string;
146146+ verifier : Z.t;
147147+ b : Z.t; (* private random *)
148148+ big_b : Z.t; (* B = k*v + g^b mod N *)
149149+ }
150150+151151+ let create ~username ~salt ~verifier =
152152+ let k = compute_k () in
153153+ let b_bytes = Crypto_rng.generate 32 in
154154+ let b = Z.(z_of_bytes b_bytes mod n) in
155155+ let big_b = Z.(((k * verifier) + powm g b n) mod n) in
156156+ { username; salt; verifier; b; big_b }
157157+158158+ let public_key t = t.big_b
159159+ let salt t = t.salt
160160+161161+ let compute_session_key t ~big_a =
162162+ if Z.(equal (big_a mod n) zero) then Error (`Msg "Invalid A value")
163163+ else
164164+ let n_len = (Z.numbits n + 7) / 8 in
165165+ let a_pad = bytes_of_z ~pad:n_len big_a in
166166+ let b_pad = bytes_of_z ~pad:n_len t.big_b in
167167+ let u = z_of_bytes (hash (a_pad ^ b_pad)) in
168168+ if Z.(equal u zero) then Error (`Msg "Invalid u value")
169169+ else
170170+ (* S = (A * v^u)^b mod N *)
171171+ let s = Z.(powm (big_a * powm t.verifier u n) t.b n) in
172172+ let session_key = hash (bytes_of_z s) in
173173+ Ok session_key
174174+175175+ let verify_proof t ~big_a ~m1 ~session_key =
176176+ let h_n = hash (bytes_of_z n) in
177177+ let h_g = hash (bytes_of_z g) in
178178+ let h_xor =
179179+ String.init (String.length h_n) (fun i ->
180180+ Char.chr (Char.code h_n.[i] lxor Char.code h_g.[i]))
181181+ in
182182+ let h_user = hash t.username in
183183+ let n_len = (Z.numbits n + 7) / 8 in
184184+ let expected =
185185+ hash
186186+ (h_xor ^ h_user ^ t.salt
187187+ ^ bytes_of_z ~pad:n_len big_a
188188+ ^ bytes_of_z ~pad:n_len t.big_b
189189+ ^ session_key)
190190+ in
191191+ String.equal expected m1
192192+193193+ let compute_proof _t ~big_a ~m1 ~session_key =
194194+ let n_len = (Z.numbits n + 7) / 8 in
195195+ hash (bytes_of_z ~pad:n_len big_a ^ m1 ^ session_key)
196196+end
+166
lib/srp.mli
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(** SRP-6a Secure Remote Password Protocol (RFC 5054)
77+88+ SRP is a password-authenticated key exchange protocol that allows a client
99+ to authenticate to a server using a password without transmitting the
1010+ password over the network. The server stores only a verifier derived from
1111+ the password, not the password itself.
1212+1313+ This implementation uses the 3072-bit group from RFC 5054 with SHA-512 as
1414+ the hash function, as required by Apple HomeKit.
1515+1616+ {1 Security Notice}
1717+1818+ {b WARNING: This implementation has known timing side-channel
1919+ vulnerabilities.}
2020+2121+ The modular exponentiation uses Zarith for big integer arithmetic, which is
2222+ {e not constant-time}. For high-security deployments, consider using
2323+ hardware security modules or ensuring operations occur on trusted networks
2424+ only.
2525+2626+ {1 Protocol Overview}
2727+2828+ {2 Registration}
2929+3030+ When a user registers:
3131+ + Server generates a random salt
3232+ + Verifier is computed: [v = g^(H(salt | H(username:password))) mod N]
3333+ + Server stores [username], [salt], and [verifier]
3434+3535+ {2 Authentication}
3636+3737+ + Client sends username and [A = g^a mod N]
3838+ + Server looks up [salt] and [verifier], computes [B = k*v + g^b mod N]
3939+ + Server sends [salt] and [B]
4040+ + Both compute [u = H(A | B)] and session key [K]
4141+ + Client computes proof [M1] and sends it
4242+ + Server verifies [M1], computes proof [M2] and sends it
4343+ + Client verifies [M2]
4444+ + Both now share session key [K]
4545+4646+ {1 Usage}
4747+4848+ {[
4949+ (* Registration: compute verifier to store on server *)
5050+ let salt = Crypto_rng.generate 16 in
5151+ let verifier = Srp.compute_verifier ~salt ~username ~password in
5252+ (* Store username, salt, verifier on server *)
5353+5454+ (* Authentication *)
5555+ let client = Srp.Client.create ~username ~password in
5656+ let big_a = Srp.Client.public_key client in
5757+ (* Send username and big_a to server *)
5858+5959+ let server = Srp.Server.create ~username ~salt ~verifier in
6060+ let big_b = Srp.Server.public_key server in
6161+ let salt = Srp.Server.salt server in
6262+ (* Send salt and big_b to client *)
6363+6464+ (* Both compute session key *)
6565+ let key_c = Srp.Client.compute_session_key client ~salt ~big_b in
6666+ let key_s = Srp.Server.compute_session_key server ~big_a in
6767+ (* key_c = key_s *)
6868+ ]} *)
6969+7070+(** {1 Constants} *)
7171+7272+val n : Z.t
7373+(** The 3072-bit group prime N from RFC 5054. *)
7474+7575+val g : Z.t
7676+(** The generator g = 5. *)
7777+7878+(** {1 Byte Conversion}
7979+8080+ SRP values are transmitted as big-endian byte strings. *)
8181+8282+val z_of_bytes : string -> Z.t
8383+(** [z_of_bytes s] converts a big-endian byte string to a big integer. *)
8484+8585+val bytes_of_z : ?pad:int -> Z.t -> string
8686+(** [bytes_of_z ?pad z] converts a big integer to a big-endian byte string. If
8787+ [pad] is specified, the result is left-padded with zeros to reach the
8888+ specified length. *)
8989+9090+(** {1 Verifier Computation} *)
9191+9292+val compute_verifier : salt:string -> username:string -> password:string -> Z.t
9393+(** [compute_verifier ~salt ~username ~password] computes the verifier
9494+ [v = g^x mod N] where [x = H(salt | H(username:password))].
9595+9696+ The verifier should be stored on the server along with the username and
9797+ salt. The password should never be stored. *)
9898+9999+(** {1 Client} *)
100100+101101+module Client : sig
102102+ type t
103103+ (** Client state for SRP authentication. *)
104104+105105+ val create : username:string -> password:string -> t
106106+ (** [create ~username ~password] initializes a client for SRP authentication.
107107+ Generates a random private value [a] and computes [A = g^a mod N]. *)
108108+109109+ val public_key : t -> Z.t
110110+ (** [public_key t] returns the client's public value [A] to send to the
111111+ server. *)
112112+113113+ val compute_session_key :
114114+ t -> salt:string -> big_b:Z.t -> (string, [ `Msg of string ]) result
115115+ (** [compute_session_key t ~salt ~big_b] computes the shared session key after
116116+ receiving the server's [B] value and salt.
117117+118118+ Returns [Error] if [B mod N = 0] (invalid server value) or if the computed
119119+ [u = 0]. *)
120120+121121+ val compute_proof :
122122+ t -> salt:string -> big_b:Z.t -> session_key:string -> string
123123+ (** [compute_proof t ~salt ~big_b ~session_key] computes the client proof
124124+ [M1 = H(H(N) xor H(g) | H(username) | salt | A | B | K)] to send to the
125125+ server. *)
126126+127127+ val verify_proof : t -> m1:string -> m2:string -> session_key:string -> bool
128128+ (** [verify_proof t ~m1 ~m2 ~session_key] verifies the server's proof [M2].
129129+ Returns [true] if the server has the correct verifier. *)
130130+end
131131+132132+(** {1 Server} *)
133133+134134+module Server : sig
135135+ type t
136136+ (** Server state for SRP authentication. *)
137137+138138+ val create : username:string -> salt:string -> verifier:Z.t -> t
139139+ (** [create ~username ~salt ~verifier] initializes a server for SRP
140140+ authentication. Generates a random private value [b] and computes
141141+ [B = k*v + g^b mod N]. *)
142142+143143+ val public_key : t -> Z.t
144144+ (** [public_key t] returns the server's public value [B] to send to the
145145+ client. *)
146146+147147+ val salt : t -> string
148148+ (** [salt t] returns the salt to send to the client. *)
149149+150150+ val compute_session_key :
151151+ t -> big_a:Z.t -> (string, [ `Msg of string ]) result
152152+ (** [compute_session_key t ~big_a] computes the shared session key after
153153+ receiving the client's [A] value.
154154+155155+ Returns [Error] if [A mod N = 0] (invalid client value) or if the computed
156156+ [u = 0]. *)
157157+158158+ val verify_proof : t -> big_a:Z.t -> m1:string -> session_key:string -> bool
159159+ (** [verify_proof t ~big_a ~m1 ~session_key] verifies the client's proof [M1].
160160+ Returns [true] if the client knows the password. *)
161161+162162+ val compute_proof :
163163+ t -> big_a:Z.t -> m1:string -> session_key:string -> string
164164+ (** [compute_proof t ~big_a ~m1 ~session_key] computes the server proof
165165+ [M2 = H(A | M1 | K)] to send to the client. *)
166166+end
+35
srp.opam
···11+# This file is generated by dune, edit dune-project instead
22+opam-version: "2.0"
33+synopsis: "SRP-6a Secure Remote Password protocol"
44+description: """
55+Implementation of the SRP-6a protocol (RFC 5054) for password-authenticated
66+ key exchange. Includes support for the 3072-bit group used by HomeKit."""
77+maintainer: ["Thomas Gazagnaire <thomas@gazagnaire.org>"]
88+authors: ["Thomas Gazagnaire <thomas@gazagnaire.org>"]
99+license: "MIT"
1010+homepage: "https://github.com/samoht/ocaml-srp"
1111+bug-reports: "https://github.com/samoht/ocaml-srp/issues"
1212+depends: [
1313+ "dune" {>= "3.0"}
1414+ "ocaml" {>= "4.08"}
1515+ "zarith" {>= "1.12"}
1616+ "digestif" {>= "1.2.0"}
1717+ "crypto-rng" {>= "1.0.0"}
1818+ "alcotest" {with-test}
1919+ "crowbar" {with-test}
2020+ "odoc" {with-doc}
2121+]
2222+build: [
2323+ ["dune" "subst"] {dev}
2424+ [
2525+ "dune"
2626+ "build"
2727+ "-p"
2828+ name
2929+ "-j"
3030+ jobs
3131+ "@install"
3232+ "@runtest" {with-test}
3333+ "@doc" {with-doc}
3434+ ]
3535+]
···11+(*---------------------------------------------------------------------------
22+ Copyright (c) 2025 Thomas Gazagnaire. All rights reserved.
33+ SPDX-License-Identifier: MIT
44+ ---------------------------------------------------------------------------*)
55+66+(* Test basic SRP protocol flow *)
77+let test_basic_protocol () =
88+ let username = "alice" in
99+ let password = "password123" in
1010+ let salt = Crypto_rng.generate 16 in
1111+1212+ (* Registration: compute verifier *)
1313+ let verifier = Srp.compute_verifier ~salt ~username ~password in
1414+1515+ (* Client initiates *)
1616+ let client = Srp.Client.create ~username ~password in
1717+ let big_a = Srp.Client.public_key client in
1818+1919+ (* Server responds *)
2020+ let server = Srp.Server.create ~username ~salt ~verifier in
2121+ let big_b = Srp.Server.public_key server in
2222+ let server_salt = Srp.Server.salt server in
2323+2424+ Alcotest.(check string) "salt matches" salt server_salt;
2525+2626+ (* Both compute session key *)
2727+ let key_c =
2828+ match Srp.Client.compute_session_key client ~salt ~big_b with
2929+ | Ok k -> k
3030+ | Error (`Msg e) -> Alcotest.fail ("Client session key: " ^ e)
3131+ in
3232+ let key_s =
3333+ match Srp.Server.compute_session_key server ~big_a with
3434+ | Ok k -> k
3535+ | Error (`Msg e) -> Alcotest.fail ("Server session key: " ^ e)
3636+ in
3737+3838+ Alcotest.(check string) "session keys match" key_c key_s;
3939+ Alcotest.(check int)
4040+ "session key is 64 bytes (SHA-512)" 64 (String.length key_c)
4141+4242+(* Test client proof verification *)
4343+let test_proof_exchange () =
4444+ let username = "bob" in
4545+ let password = "secret" in
4646+ let salt = Crypto_rng.generate 16 in
4747+4848+ let verifier = Srp.compute_verifier ~salt ~username ~password in
4949+ let client = Srp.Client.create ~username ~password in
5050+ let big_a = Srp.Client.public_key client in
5151+ let server = Srp.Server.create ~username ~salt ~verifier in
5252+ let big_b = Srp.Server.public_key server in
5353+5454+ let key_c =
5555+ match Srp.Client.compute_session_key client ~salt ~big_b with
5656+ | Ok k -> k
5757+ | Error (`Msg e) -> Alcotest.fail e
5858+ in
5959+ let key_s =
6060+ match Srp.Server.compute_session_key server ~big_a with
6161+ | Ok k -> k
6262+ | Error (`Msg e) -> Alcotest.fail e
6363+ in
6464+6565+ (* Client computes and sends M1 *)
6666+ let m1 = Srp.Client.compute_proof client ~salt ~big_b ~session_key:key_c in
6767+6868+ (* Server verifies M1 *)
6969+ Alcotest.(check bool)
7070+ "server verifies client proof" true
7171+ (Srp.Server.verify_proof server ~big_a ~m1 ~session_key:key_s);
7272+7373+ (* Server computes and sends M2 *)
7474+ let m2 = Srp.Server.compute_proof server ~big_a ~m1 ~session_key:key_s in
7575+7676+ (* Client verifies M2 *)
7777+ Alcotest.(check bool)
7878+ "client verifies server proof" true
7979+ (Srp.Client.verify_proof client ~m1 ~m2 ~session_key:key_c)
8080+8181+(* Test wrong password fails *)
8282+let test_wrong_password () =
8383+ let username = "charlie" in
8484+ let password = "correct" in
8585+ let wrong_password = "incorrect" in
8686+ let salt = Crypto_rng.generate 16 in
8787+8888+ let verifier = Srp.compute_verifier ~salt ~username ~password in
8989+ let client = Srp.Client.create ~username ~password:wrong_password in
9090+ let big_a = Srp.Client.public_key client in
9191+ let server = Srp.Server.create ~username ~salt ~verifier in
9292+ let big_b = Srp.Server.public_key server in
9393+9494+ let key_c =
9595+ match Srp.Client.compute_session_key client ~salt ~big_b with
9696+ | Ok k -> k
9797+ | Error (`Msg e) -> Alcotest.fail e
9898+ in
9999+ let key_s =
100100+ match Srp.Server.compute_session_key server ~big_a with
101101+ | Ok k -> k
102102+ | Error (`Msg e) -> Alcotest.fail e
103103+ in
104104+105105+ (* Keys should NOT match with wrong password *)
106106+ Alcotest.(check bool)
107107+ "session keys differ with wrong password" false (String.equal key_c key_s)
108108+109109+(* Test verifier is deterministic *)
110110+let test_verifier_deterministic () =
111111+ let username = "dave" in
112112+ let password = "mypassword" in
113113+ let salt = "fixed_salt_value" in
114114+115115+ let v1 = Srp.compute_verifier ~salt ~username ~password in
116116+ let v2 = Srp.compute_verifier ~salt ~username ~password in
117117+118118+ Alcotest.(check bool) "verifiers are equal" true (Z.equal v1 v2)
119119+120120+(* Test different salts produce different verifiers *)
121121+let test_different_salts () =
122122+ let username = "eve" in
123123+ let password = "samepassword" in
124124+ let salt1 = "salt_one" in
125125+ let salt2 = "salt_two" in
126126+127127+ let v1 = Srp.compute_verifier ~salt:salt1 ~username ~password in
128128+ let v2 = Srp.compute_verifier ~salt:salt2 ~username ~password in
129129+130130+ Alcotest.(check bool)
131131+ "different salts produce different verifiers" false (Z.equal v1 v2)
132132+133133+(* Test constants are valid *)
134134+let test_constants () =
135135+ (* N should be a 3072-bit prime *)
136136+ Alcotest.(check int) "N is 3072 bits" 3072 (Z.numbits Srp.n);
137137+ (* g should be 5 *)
138138+ Alcotest.(check bool) "g is 5" true (Z.equal Srp.g (Z.of_int 5))
139139+140140+let suite =
141141+ [
142142+ ( "Protocol",
143143+ [
144144+ Alcotest.test_case "basic protocol" `Quick test_basic_protocol;
145145+ Alcotest.test_case "proof exchange" `Quick test_proof_exchange;
146146+ Alcotest.test_case "wrong password" `Quick test_wrong_password;
147147+ ] );
148148+ ( "Verifier",
149149+ [
150150+ Alcotest.test_case "deterministic" `Quick test_verifier_deterministic;
151151+ Alcotest.test_case "salt affects verifier" `Quick test_different_salts;
152152+ ] );
153153+ ("Constants", [ Alcotest.test_case "valid constants" `Quick test_constants ]);
154154+ ]