SPAKE2/SPAKE2+ password-authenticated key exchange for OCaml
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Squashed 'ocaml-spake2/' content from commit f0a61281 git-subtree-split: f0a612813c7d1de1017592d14db39b3e9324fe83

+1113
+17
.gitignore
··· 1 + # OCaml build artifacts 2 + _build/ 3 + *.install 4 + *.merlin 5 + 6 + # Dune package management 7 + dune.lock/ 8 + 9 + # Editor and OS files 10 + .DS_Store 11 + *.swp 12 + *~ 13 + .vscode/ 14 + .idea/ 15 + 16 + # Opam local switch 17 + _opam/
+1
.ocamlformat
··· 1 + version = 0.28.1
+21
LICENSE.md
··· 1 + MIT License 2 + 3 + Copyright (c) 2025 Thomas Gazagnaire 4 + 5 + Permission is hereby granted, free of charge, to any person obtaining a copy 6 + of this software and associated documentation files (the "Software"), to deal 7 + in the Software without restriction, including without limitation the rights 8 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 + copies of the Software, and to permit persons to whom the Software is 10 + furnished to do so, subject to the following conditions: 11 + 12 + The above copyright notice and this permission notice shall be included in all 13 + copies or substantial portions of the Software. 14 + 15 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 + SOFTWARE.
+101
README.md
··· 1 + # spake2 2 + 3 + SPAKE2 and SPAKE2+ Password-Authenticated Key Exchange for OCaml. 4 + 5 + ## Overview 6 + 7 + This library implements the SPAKE2 (RFC 9382) and SPAKE2+ protocols for 8 + password-authenticated key exchange. These protocols allow two parties who 9 + share a password to derive a strong shared secret key without revealing the 10 + password to eavesdroppers or allowing offline dictionary attacks. 11 + 12 + - **SPAKE2**: Both parties derive the same values from the password 13 + - **SPAKE2+**: Augmented PAKE where the server stores only a verifier, not password-equivalent data 14 + 15 + ## Security Notice 16 + 17 + This implementation uses Zarith for P-256 elliptic curve arithmetic, which is 18 + **not constant-time**. This means the implementation has timing side-channel 19 + vulnerabilities. For high-security deployments, consider using hardware security 20 + modules or ensuring operations occur on trusted networks only. 21 + 22 + ## Installation 23 + 24 + ``` 25 + opam install spake2 26 + ``` 27 + 28 + ## Usage 29 + 30 + ### SPAKE2 31 + 32 + ```ocaml 33 + let password = "secret" in 34 + 35 + (* Party A *) 36 + let state_a, msg_a = Spake2.init ~password `A in 37 + (* send msg_a to B, receive msg_b from B *) 38 + let key_a = Spake2.finish ~context:"myapp" state_a msg_b in 39 + 40 + (* Party B *) 41 + let state_b, msg_b = Spake2.init ~password `B in 42 + (* send msg_b to A, receive msg_a from A *) 43 + let key_b = Spake2.finish ~context:"myapp" state_b msg_a in 44 + 45 + (* key_a = key_b *) 46 + ``` 47 + 48 + ### SPAKE2+ 49 + 50 + ```ocaml 51 + (* Setup: derive verifier data from password *) 52 + let salt = Spake2.Plus.generate_salt () in 53 + let iterations = 1000 in 54 + let w0, w1 = Spake2.Plus.derive_w ~password ~salt ~iterations in 55 + let l = Spake2.Plus.compute_l ~w1 in 56 + (* Server stores: w0, l, salt, iterations (NOT the password or w1) *) 57 + 58 + (* Protocol run *) 59 + let context = "myapp" in 60 + let prover_state, pa = Spake2.Plus.prover_init ~w0 ~w1 ~context in 61 + let verifier_state, pb = Spake2.Plus.verifier_init ~w0 ~l ~context in 62 + 63 + (* Exchange pa and pb *) 64 + let Ok (ke_prover, ca, _) = Spake2.Plus.prover_finish prover_state pb in 65 + let Ok (ke_verifier, cb, _) = Spake2.Plus.verifier_finish verifier_state pa in 66 + 67 + (* ke_prover = ke_verifier *) 68 + (* ca and cb can be exchanged for key confirmation *) 69 + ``` 70 + 71 + ## API 72 + 73 + ### SPAKE2 74 + 75 + - `Spake2.init ~password role` - Initialize protocol for `A or `B 76 + - `Spake2.finish ?context ?id_a ?id_b state peer_msg` - Complete protocol 77 + 78 + ### SPAKE2+ 79 + 80 + - `Spake2.Plus.derive_w ~password ~salt ~iterations` - Derive w0, w1 from password 81 + - `Spake2.Plus.compute_l ~w1` - Compute L for server storage 82 + - `Spake2.Plus.prover_init ~w0 ~w1 ~context` - Initialize as prover (client) 83 + - `Spake2.Plus.verifier_init ~w0 ~l ~context` - Initialize as verifier (server) 84 + - `Spake2.Plus.prover_finish state pb` - Complete as prover 85 + - `Spake2.Plus.verifier_finish state pa` - Complete as verifier 86 + 87 + ### P-256 Curve 88 + 89 + - `Spake2.P256.scalar_mult k p` - Scalar multiplication 90 + - `Spake2.P256.add p q` - Point addition 91 + - `Spake2.P256.to_bytes p` - Encode point (SEC1 uncompressed) 92 + - `Spake2.P256.of_bytes s` - Decode and validate point 93 + 94 + ## References 95 + 96 + - [RFC 9382 - SPAKE2](https://www.rfc-editor.org/rfc/rfc9382.html) 97 + - [SPAKE2+ Draft](https://datatracker.ietf.org/doc/draft-bar-cfrg-spake2plus/) 98 + 99 + ## License 100 + 101 + MIT License. See [LICENSE.md](LICENSE.md) for details.
+32
dune-project
··· 1 + (lang dune 3.0) 2 + 3 + (name spake2) 4 + 5 + (generate_opam_files true) 6 + 7 + (license MIT) 8 + (authors "Thomas Gazagnaire <thomas@gazagnaire.org>") 9 + (maintainers "Thomas Gazagnaire <thomas@gazagnaire.org>") 10 + (homepage "https://github.com/samoht/ocaml-spake2") 11 + (bug_reports "https://github.com/samoht/ocaml-spake2/issues") 12 + 13 + (package 14 + (name spake2) 15 + (synopsis "SPAKE2 and SPAKE2+ Password-Authenticated Key Exchange") 16 + (description 17 + "Implementation of the SPAKE2 and SPAKE2+ protocols for password-authenticated 18 + key exchange. SPAKE2 (RFC 9382) allows two parties who share a password to 19 + derive a strong shared secret key, without revealing the password to 20 + eavesdroppers or allowing offline dictionary attacks. SPAKE2+ adds verifier 21 + storage security so the server doesn't store password-equivalent data.") 22 + (depends 23 + (ocaml (>= 4.08)) 24 + (zarith (>= 1.12)) 25 + (digestif (>= 1.2.0)) 26 + (kdf (>= 0.1)) 27 + (pbkdf2 (>= 0.1)) 28 + (crypto-rng (>= 1.0.0)) 29 + (crypto-ec (>= 1.0.0)) 30 + (logs (>= 0.7.0)) 31 + (alcotest :with-test) 32 + (crowbar :with-test)))
+15
fuzz/dune
··· 1 + ; Crowbar fuzz testing for spake2 2 + ; 3 + ; To run: dune exec fuzz/fuzz_spake2.exe 4 + ; With AFL: afl-fuzz -i fuzz/corpus -o fuzz/findings -- ./_build/default/fuzz/fuzz_spake2.exe @@ 5 + 6 + (executable 7 + (name fuzz_spake2) 8 + (modules fuzz_spake2) 9 + (libraries spake2 crowbar crypto-rng.unix)) 10 + 11 + (rule 12 + (alias fuzz) 13 + (deps fuzz_spake2.exe) 14 + (action 15 + (run %{exe:fuzz_spake2.exe})))
+82
fuzz/fuzz_spake2.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + open Crowbar 7 + 8 + let () = Crypto_rng_unix.use_default () 9 + 10 + let test_spake2_roundtrip password = 11 + if String.length password = 0 then () 12 + else 13 + let state_a, msg_a = Spake2.init ~password `A in 14 + let state_b, msg_b = Spake2.init ~password `B in 15 + match (Spake2.finish state_a msg_b, Spake2.finish state_b msg_a) with 16 + | Ok key_a, Ok key_b -> check_eq ~pp:Format.pp_print_string key_a key_b 17 + | Error e, _ -> failwith ("A failed: " ^ e) 18 + | _, Error e -> failwith ("B failed: " ^ e) 19 + 20 + let test_spake2_plus_roundtrip password salt = 21 + if String.length password = 0 || String.length salt < 16 then () 22 + else 23 + let iterations = 1000 in 24 + let context = "fuzz" in 25 + let w0, w1 = Spake2.Plus.derive_w ~password ~salt ~iterations in 26 + let l = Spake2.Plus.compute_l ~w1 in 27 + let prover_state, pa = Spake2.Plus.prover_init ~w0 ~w1 ~context in 28 + let verifier_state, pb = Spake2.Plus.verifier_init ~w0 ~l ~context in 29 + match 30 + ( Spake2.Plus.prover_finish prover_state pb, 31 + Spake2.Plus.verifier_finish verifier_state pa ) 32 + with 33 + | Ok (ke_p, _, _), Ok (ke_v, _, _) -> 34 + check_eq ~pp:Format.pp_print_string ke_p ke_v 35 + | Error e, _ -> failwith ("prover failed: " ^ e) 36 + | _, Error e -> failwith ("verifier failed: " ^ e) 37 + 38 + let test_p256_point_encoding_roundtrip x_bytes y_bytes = 39 + if String.length x_bytes < 32 || String.length y_bytes < 32 then () 40 + else 41 + let x_bytes = String.sub x_bytes 0 32 in 42 + let y_bytes = String.sub y_bytes 0 32 in 43 + let encoded = "\x04" ^ x_bytes ^ y_bytes in 44 + match Spake2.P256.of_bytes encoded with 45 + | Ok point -> 46 + let re_encoded = Spake2.P256.to_bytes point in 47 + check_eq ~pp:Format.pp_print_string encoded re_encoded 48 + | Error _ -> () 49 + 50 + let test_p256_negate_involutive () = 51 + (* Test that negate(negate(G)) = G *) 52 + let g = Spake2.P256.generator in 53 + let neg_g = Spake2.P256.negate g in 54 + let neg_neg_g = Spake2.P256.negate neg_g in 55 + let g_bytes = Spake2.P256.to_bytes g in 56 + let result_bytes = Spake2.P256.to_bytes neg_neg_g in 57 + check_eq ~pp:Format.pp_print_string g_bytes result_bytes 58 + 59 + let test_p256_add_commutative () = 60 + (* Test that G + M = M + G *) 61 + let g = Spake2.P256.generator in 62 + let m = Spake2.P256.m in 63 + let g_plus_m = Spake2.P256.add g m in 64 + let m_plus_g = Spake2.P256.add m g in 65 + let bytes1 = Spake2.P256.to_bytes g_plus_m in 66 + let bytes2 = Spake2.P256.to_bytes m_plus_g in 67 + check_eq ~pp:Format.pp_print_string bytes1 bytes2 68 + 69 + let () = 70 + add_test ~name:"spake2: roundtrip" [ bytes ] (fun password -> 71 + test_spake2_roundtrip password); 72 + add_test ~name:"spake2+: roundtrip" [ bytes; bytes ] (fun password salt -> 73 + test_spake2_plus_roundtrip password salt); 74 + add_test ~name:"p256: point encoding roundtrip" [ bytes; bytes ] (fun x y -> 75 + test_p256_point_encoding_roundtrip x y); 76 + (* Run constant property tests once with dummy input *) 77 + add_test ~name:"p256: negate involutive" 78 + [ const () ] 79 + (fun () -> test_p256_negate_involutive ()); 80 + add_test ~name:"p256: add commutative" 81 + [ const () ] 82 + (fun () -> test_p256_add_commutative ())
+4
lib/dune
··· 1 + (library 2 + (name spake2) 3 + (public_name spake2) 4 + (libraries zarith digestif kdf.hkdf pbkdf2 crypto-rng crypto-ec logs))
+339
lib/spake2.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** SPAKE2 and SPAKE2+ Password-Authenticated Key Exchange 7 + 8 + This implementation uses mirage-crypto-ec for P-256 elliptic curve 9 + operations, which provides constant-time arithmetic using code generated by 10 + {{:https://github.com/mit-plv/fiat-crypto}fiat-crypto}. This protects 11 + against timing side-channel attacks on scalar multiplication and point 12 + operations. *) 13 + 14 + let log_src = Logs.Src.create "spake2" 15 + 16 + module Log = (val Logs.src_log log_src : Logs.LOG) 17 + 18 + let ( let* ) = Result.bind 19 + 20 + let hkdf_derive ~salt ~ikm ~info ~length = 21 + let prk = Hkdf.extract ~hash:`SHA256 ~salt ikm in 22 + Hkdf.expand ~hash:`SHA256 ~prk ~info length 23 + 24 + type role = [ `A | `B ] 25 + 26 + (** {1 Utility Functions} *) 27 + 28 + let z_to_bytes n z = 29 + let buf = Bytes.make n '\x00' in 30 + let rec fill i v = 31 + if i < 0 || Z.equal v Z.zero then () 32 + else ( 33 + Bytes.set buf i (Char.chr (Z.to_int (Z.logand v (Z.of_int 0xff)))); 34 + fill (i - 1) (Z.shift_right v 8)) 35 + in 36 + fill (n - 1) z; 37 + Bytes.to_string buf 38 + 39 + let z_to_bytes32 z = z_to_bytes 32 z 40 + 41 + let bytes_to_z s = 42 + let len = String.length s in 43 + let result = ref Z.zero in 44 + for i = 0 to len - 1 do 45 + result := Z.add (Z.shift_left !result 8) (Z.of_int (Char.code s.[i])) 46 + done; 47 + !result 48 + 49 + (** {1 Cryptographic Utilities} *) 50 + 51 + let sha256 data = Digestif.SHA256.(digest_string data |> to_raw_string) 52 + 53 + let hmac_sha256 ~key data = 54 + Digestif.SHA256.(hmac_string ~key data |> to_raw_string) 55 + 56 + (** {1 P-256 Elliptic Curve} 57 + 58 + Uses mirage-crypto-ec for constant-time operations. *) 59 + 60 + module P256 = struct 61 + module Ec = Crypto_ec.P256.Point 62 + 63 + (** The curve order for scalar arithmetic *) 64 + let order = 65 + Z.of_string 66 + "0xffffffff00000000ffffffffffffffffbce6faada7179e84f3b9cac2fc632551" 67 + 68 + (** The field prime for point negation *) 69 + let prime = 70 + Z.of_string 71 + "0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff" 72 + 73 + type point = Ec.point 74 + 75 + (** SPAKE2 M point in SEC1 uncompressed format *) 76 + let m_bytes = 77 + "\x04\x88\x6e\x2f\x97\xac\xe4\x6e\x55\xba\x9d\xd7\x24\x25\x79\xf2\x99\x3b\x64\xe1\x6e\xf3\xdc\xab\x95\xaf\xd4\x97\x33\x3d\x8f\xa1\x2f\x5f\xf3\x55\x16\x3e\x43\xce\x22\x4e\x0b\x0e\x65\xff\x02\xac\x8e\x5c\x7b\xe0\x94\x19\xc7\x85\xe0\xca\x54\x7d\x55\xa1\x2e\x2d\x20" 78 + 79 + (** SPAKE2 N point in SEC1 uncompressed format *) 80 + let n_bytes = 81 + "\x04\xd8\xbb\xd6\xc6\x39\xc6\x29\x37\xb0\x4d\x99\x7f\x38\xc3\x77\x07\x19\xc6\x29\xd7\x01\x4d\x49\xa2\x4b\x4f\x98\xba\xa1\x29\x2b\x49\x07\xd6\x0a\xa6\xbf\xad\xe4\x50\x08\xa6\x36\x33\x7f\x51\x68\xc6\x4d\x9b\xd3\x60\x34\x80\x8c\xd5\x64\x49\x0b\x1e\x65\x6e\xdb\xe7" 82 + 83 + let m = 84 + match Ec.of_octets m_bytes with 85 + | Ok p -> p 86 + | Error _ -> failwith "Invalid SPAKE2 M constant" 87 + 88 + let n = 89 + match Ec.of_octets n_bytes with 90 + | Ok p -> p 91 + | Error _ -> failwith "Invalid SPAKE2 N constant" 92 + 93 + let generator = Ec.generator 94 + let add = Ec.add 95 + let scalar_mult scalar pt = Ec.scalar_mult scalar pt 96 + let scalar_mult_base scalar = Ec.scalar_mult scalar Ec.generator 97 + let to_bytes pt = Ec.to_octets pt 98 + 99 + (** Negate a point: -P = (x, p - y). We parse the SEC1 encoding, negate the 100 + y-coordinate, and re-encode. This is a single arithmetic operation, not 101 + timing-sensitive. *) 102 + let negate pt = 103 + let octets = Ec.to_octets pt in 104 + if String.length octets = 1 && octets.[0] = '\x00' then pt 105 + else 106 + let x_bytes = String.sub octets 1 32 in 107 + let y_bytes = String.sub octets 33 32 in 108 + let y = bytes_to_z y_bytes in 109 + let neg_y = Z.sub prime y in 110 + let neg_y_bytes = z_to_bytes32 neg_y in 111 + let new_octets = "\x04" ^ x_bytes ^ neg_y_bytes in 112 + match Ec.of_octets new_octets with 113 + | Ok p -> p 114 + | Error _ -> failwith "negate: invalid result" 115 + 116 + let of_bytes s = 117 + match Ec.of_octets s with 118 + | Ok p -> Ok p 119 + | Error e -> Error (Format.asprintf "%a" Crypto_ec.pp_error e) 120 + 121 + (** Convert a scalar represented as Z.t to the constant-time scalar type *) 122 + let scalar_of_z z = 123 + let z = Z.erem z order in 124 + let z = if Z.lt z Z.zero then Z.add z order else z in 125 + let bytes = z_to_bytes32 z in 126 + match Ec.scalar_of_octets bytes with 127 + | Ok s -> s 128 + | Error _ -> ( 129 + (* If scalar is 0, use 1 instead (edge case) *) 130 + match Ec.scalar_of_octets (z_to_bytes32 Z.one) with 131 + | Ok s -> s 132 + | Error _ -> failwith "scalar_of_z: cannot create scalar") 133 + 134 + (** Generate a random scalar in [1, order-1] *) 135 + let random_scalar () = 136 + let rec try_generate () = 137 + let bytes = Crypto_rng.generate 32 in 138 + match Ec.scalar_of_octets bytes with 139 + | Ok s -> s 140 + | Error _ -> try_generate () 141 + in 142 + try_generate () 143 + end 144 + 145 + (** {1 SPAKE2 Protocol} *) 146 + 147 + type state = { 148 + role : role; 149 + w : P256.Ec.scalar; 150 + scalar : P256.Ec.scalar; 151 + my_share : string; 152 + } 153 + 154 + let derive_w_from_password password = 155 + let hash = sha256 password in 156 + let w = Z.erem (bytes_to_z hash) P256.order in 157 + let w = if Z.equal w Z.zero then Z.one else w in 158 + P256.scalar_of_z w 159 + 160 + let init ~password role = 161 + let w = derive_w_from_password password in 162 + let scalar = P256.random_scalar () in 163 + let blind_point = match role with `A -> P256.m | `B -> P256.n in 164 + let w_times_blind = P256.scalar_mult w blind_point in 165 + let scalar_times_g = P256.scalar_mult_base scalar in 166 + let share_point = P256.add w_times_blind scalar_times_g in 167 + let my_share = P256.to_bytes share_point in 168 + Log.debug (fun f -> 169 + f "SPAKE2 init: role=%s share=%d bytes" 170 + (match role with `A -> "A" | `B -> "B") 171 + (String.length my_share)); 172 + ({ role; w; scalar; my_share }, my_share) 173 + 174 + let compute_transcript ~context ~id_a ~id_b ~pa ~pb ~k = 175 + let add_length_prefixed buf s = 176 + let len = String.length s in 177 + Buffer.add_char buf (Char.chr ((len lsr 56) land 0xff)); 178 + Buffer.add_char buf (Char.chr ((len lsr 48) land 0xff)); 179 + Buffer.add_char buf (Char.chr ((len lsr 40) land 0xff)); 180 + Buffer.add_char buf (Char.chr ((len lsr 32) land 0xff)); 181 + Buffer.add_char buf (Char.chr ((len lsr 24) land 0xff)); 182 + Buffer.add_char buf (Char.chr ((len lsr 16) land 0xff)); 183 + Buffer.add_char buf (Char.chr ((len lsr 8) land 0xff)); 184 + Buffer.add_char buf (Char.chr (len land 0xff)); 185 + Buffer.add_string buf s 186 + in 187 + let buf = Buffer.create 256 in 188 + add_length_prefixed buf context; 189 + add_length_prefixed buf id_a; 190 + add_length_prefixed buf id_b; 191 + add_length_prefixed buf pa; 192 + add_length_prefixed buf pb; 193 + add_length_prefixed buf k; 194 + sha256 (Buffer.contents buf) 195 + 196 + let finish ?(context = "") ?(id_a = "") ?(id_b = "") state peer_share = 197 + let* peer_point = P256.of_bytes peer_share in 198 + let peer_blind = match state.role with `A -> P256.n | `B -> P256.m in 199 + let w_times_peer_blind = P256.scalar_mult state.w peer_blind in 200 + let peer_unblinded = P256.add peer_point (P256.negate w_times_peer_blind) in 201 + let k_point = P256.scalar_mult state.scalar peer_unblinded in 202 + let k = P256.to_bytes k_point in 203 + let pa, pb = 204 + match state.role with 205 + | `A -> (state.my_share, peer_share) 206 + | `B -> (peer_share, state.my_share) 207 + in 208 + let transcript = compute_transcript ~context ~id_a ~id_b ~pa ~pb ~k in 209 + let shared_secret = 210 + hkdf_derive ~salt:"" ~ikm:transcript ~info:"SPAKE2" ~length:32 211 + in 212 + Log.debug (fun f -> f "SPAKE2 finish: derived 32-byte shared secret"); 213 + Ok shared_secret 214 + 215 + (** {1 SPAKE2+ Protocol} *) 216 + 217 + module Plus = struct 218 + let default_iterations = 1000 219 + let generate_salt () = Crypto_rng.generate 32 220 + 221 + let derive_w ~password ~salt ~iterations = 222 + let ws = Pbkdf2.derive ~password ~salt ~iterations ~length:80 in 223 + let w0s = String.sub ws 0 40 in 224 + let w1s = String.sub ws 40 40 in 225 + let w0 = Z.erem (bytes_to_z w0s) P256.order in 226 + let w1 = Z.erem (bytes_to_z w1s) P256.order in 227 + (z_to_bytes32 w0, z_to_bytes32 w1) 228 + 229 + let compute_l ~w1 = 230 + let w1_scalar = P256.scalar_of_z (bytes_to_z w1) in 231 + let l_point = P256.scalar_mult_base w1_scalar in 232 + P256.to_bytes l_point 233 + 234 + type prover_state = { 235 + w0 : string; 236 + w1 : string; 237 + x : P256.Ec.scalar; 238 + pa : string; 239 + context : string; 240 + } 241 + 242 + type verifier_state = { 243 + w0 : string; 244 + l : string; 245 + y : P256.Ec.scalar; 246 + pb : string; 247 + context : string; 248 + } 249 + 250 + let compute_tt ~context ~pa ~pb ~z ~v ~w0 = 251 + let add_length_prefixed buf s = 252 + let len = String.length s in 253 + Buffer.add_char buf (Char.chr ((len lsr 24) land 0xff)); 254 + Buffer.add_char buf (Char.chr ((len lsr 16) land 0xff)); 255 + Buffer.add_char buf (Char.chr ((len lsr 8) land 0xff)); 256 + Buffer.add_char buf (Char.chr (len land 0xff)); 257 + Buffer.add_string buf s 258 + in 259 + let buf = Buffer.create 512 in 260 + add_length_prefixed buf context; 261 + add_length_prefixed buf ""; 262 + add_length_prefixed buf ""; 263 + add_length_prefixed buf pa; 264 + add_length_prefixed buf pb; 265 + add_length_prefixed buf z; 266 + add_length_prefixed buf v; 267 + add_length_prefixed buf w0; 268 + sha256 (Buffer.contents buf) 269 + 270 + let derive_keys ~tt = 271 + let ka = hkdf_derive ~salt:"" ~ikm:tt ~info:"ConfirmationKeys" ~length:64 in 272 + let ke = hkdf_derive ~salt:"" ~ikm:tt ~info:"SharedSecret" ~length:32 in 273 + let kca = String.sub ka 0 32 in 274 + let kcb = String.sub ka 32 32 in 275 + (kca, kcb, ke) 276 + 277 + let prover_init ~w0 ~w1 ~context = 278 + let x = P256.random_scalar () in 279 + let w0_scalar = P256.scalar_of_z (bytes_to_z w0) in 280 + let x_g = P256.scalar_mult_base x in 281 + let w0_m = P256.scalar_mult w0_scalar P256.m in 282 + let pa_point = P256.add x_g w0_m in 283 + let pa = P256.to_bytes pa_point in 284 + Log.debug (fun f -> f "SPAKE2+ prover init: pA=%d bytes" (String.length pa)); 285 + ({ w0; w1; x; pa; context }, pa) 286 + 287 + let prover_finish (state : prover_state) pb_bytes = 288 + let* pb = P256.of_bytes pb_bytes in 289 + let w0_scalar = P256.scalar_of_z (bytes_to_z state.w0) in 290 + let w1_scalar = P256.scalar_of_z (bytes_to_z state.w1) in 291 + let w0_n = P256.scalar_mult w0_scalar P256.n in 292 + let pb_minus_w0n = P256.add pb (P256.negate w0_n) in 293 + let z_point = P256.scalar_mult state.x pb_minus_w0n in 294 + let v_point = P256.scalar_mult w1_scalar pb_minus_w0n in 295 + let z = P256.to_bytes z_point in 296 + let v = P256.to_bytes v_point in 297 + let tt = 298 + compute_tt ~context:state.context ~pa:state.pa ~pb:pb_bytes ~z ~v 299 + ~w0:state.w0 300 + in 301 + let kca, kcb, ke = derive_keys ~tt in 302 + let ca = hmac_sha256 ~key:kca pb_bytes in 303 + Log.debug (fun f -> 304 + f "SPAKE2+ prover finish: ke=%d cA=%d bytes" (String.length ke) 305 + (String.length ca)); 306 + Ok (ke, ca, kcb) 307 + 308 + let verifier_init ~w0 ~l ~context = 309 + let y = P256.random_scalar () in 310 + let w0_scalar = P256.scalar_of_z (bytes_to_z w0) in 311 + let y_g = P256.scalar_mult_base y in 312 + let w0_n = P256.scalar_mult w0_scalar P256.n in 313 + let pb_point = P256.add y_g w0_n in 314 + let pb = P256.to_bytes pb_point in 315 + Log.debug (fun f -> 316 + f "SPAKE2+ verifier init: pB=%d bytes" (String.length pb)); 317 + ({ w0; l; y; pb; context }, pb) 318 + 319 + let verifier_finish state pa_bytes = 320 + let* pa = P256.of_bytes pa_bytes in 321 + let* l_point = P256.of_bytes state.l in 322 + let w0_scalar = P256.scalar_of_z (bytes_to_z state.w0) in 323 + let w0_m = P256.scalar_mult w0_scalar P256.m in 324 + let pa_minus_w0m = P256.add pa (P256.negate w0_m) in 325 + let z_point = P256.scalar_mult state.y pa_minus_w0m in 326 + let v_point = P256.scalar_mult state.y l_point in 327 + let z = P256.to_bytes z_point in 328 + let v = P256.to_bytes v_point in 329 + let tt = 330 + compute_tt ~context:state.context ~pa:pa_bytes ~pb:state.pb ~z ~v 331 + ~w0:state.w0 332 + in 333 + let kca, kcb, ke = derive_keys ~tt in 334 + let cb = hmac_sha256 ~key:kcb pa_bytes in 335 + Log.debug (fun f -> 336 + f "SPAKE2+ verifier finish: ke=%d cB=%d bytes" (String.length ke) 337 + (String.length cb)); 338 + Ok (ke, cb, kca) 339 + end
+235
lib/spake2.mli
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + (** SPAKE2 and SPAKE2+ Password-Authenticated Key Exchange 7 + 8 + SPAKE2 and SPAKE2+ are Password-Authenticated Key Exchange (PAKE) protocols 9 + that allow two parties who share a low-entropy password to derive a strong 10 + shared secret key, without revealing the password to eavesdroppers or 11 + allowing offline dictionary attacks. 12 + 13 + {b SPAKE2} (RFC 9382) is the simpler variant where both parties derive the 14 + same values from the password. 15 + 16 + {b SPAKE2+} adds "augmented PAKE" properties: the verifier (server) stores 17 + only a password verifier, not the password itself. This protects against 18 + server compromise. 19 + 20 + {1 Security} 21 + 22 + This implementation uses mirage-crypto-ec for P-256 elliptic curve 23 + operations, which provides constant-time arithmetic using code generated by 24 + {{:https://github.com/mit-plv/fiat-crypto}fiat-crypto}. This protects 25 + against timing side-channel attacks on scalar multiplication and point 26 + operations. 27 + 28 + {1 Protocol Overview} 29 + 30 + {2 SPAKE2} 31 + 32 + Both parties share password [w]: 33 + + A generates random scalar [x] and sends [pA = w*M + x*G] to B 34 + + B generates random scalar [y] and sends [pB = w*N + y*G] to A 35 + + A computes [K = x*(pB - w*N) = x*y*G] 36 + + B computes [K = y*(pA - w*M) = x*y*G] 37 + + Both derive the shared secret from [K] using a KDF 38 + 39 + {2 SPAKE2+} 40 + 41 + Prover (client) has password, Verifier (server) has [w0] and [L = w1*G]: 42 + + Prover: [pA = w0*M + x*G] 43 + + Verifier: [pB = w0*N + y*G] 44 + + Prover computes [Z = x*(pB - w0*N)], [V = w1*(pB - w0*N)] 45 + + Verifier computes [Z = y*(pA - w0*M)], [V = y*L] 46 + + Both derive keys from transcript including [Z] and [V] 47 + 48 + {1 Usage} 49 + 50 + {2 SPAKE2 Example} 51 + 52 + {[ 53 + let password = "secret" in 54 + 55 + (* Party A *) 56 + let state_a, msg_a = Spake2.init ~password `A in 57 + (* send msg_a to B, receive msg_b from B *) 58 + let key_a = Spake2.finish ~context:"myapp" state_a msg_b in 59 + 60 + (* Party B *) 61 + let state_b, msg_b = Spake2.init ~password `B in 62 + (* send msg_b to A, receive msg_a from A *) 63 + let key_b = Spake2.finish ~context:"myapp" state_b msg_a in 64 + 65 + (* key_a = key_b *) 66 + ]} 67 + 68 + {2 SPAKE2+ Example} 69 + 70 + {[ 71 + (* Setup: derive verifier data from password *) 72 + let w0, w1 = Spake2.Plus.derive_w ~password ~salt ~iterations in 73 + let l = Spake2.Plus.compute_l ~w1 in 74 + (* Server stores: w0, l (NOT the password or w1) *) 75 + 76 + (* Protocol run *) 77 + let prover_state, pa = Spake2.Plus.prover_init ~w0 ~w1 ~context in 78 + let verifier_state, pb = Spake2.Plus.verifier_init ~w0 ~l ~context in 79 + 80 + let prover_result = Spake2.Plus.prover_finish prover_state pb in 81 + let verifier_result = Spake2.Plus.verifier_finish verifier_state pa in 82 + ]} *) 83 + 84 + (** {1 Types} *) 85 + 86 + type role = [ `A | `B ] 87 + (** The role of a party in the SPAKE2 protocol. *) 88 + 89 + (** {1 P-256 Curve} *) 90 + 91 + module P256 : sig 92 + (** P-256 (secp256r1) elliptic curve operations. 93 + 94 + Uses mirage-crypto-ec for constant-time arithmetic. *) 95 + 96 + (** {2 Types} *) 97 + 98 + type point = Crypto_ec.P256.Point.point 99 + (** Opaque point type from mirage-crypto-ec. *) 100 + 101 + (** {2 Constants} *) 102 + 103 + val order : Z.t 104 + (** The curve order n. *) 105 + 106 + val generator : point 107 + (** The generator point G. *) 108 + 109 + val m : point 110 + (** The SPAKE2 M point (RFC 9382). *) 111 + 112 + val n : point 113 + (** The SPAKE2 N point (RFC 9382). *) 114 + 115 + (** {2 Operations} *) 116 + 117 + val add : point -> point -> point 118 + (** [add p q] computes [P + Q] (constant-time). *) 119 + 120 + val negate : point -> point 121 + (** [negate p] computes [-P]. *) 122 + 123 + (** {2 Encoding} *) 124 + 125 + val to_bytes : point -> string 126 + (** [to_bytes p] encodes [p] in SEC1 uncompressed format (65 bytes). *) 127 + 128 + val of_bytes : string -> (point, string) result 129 + (** [of_bytes s] decodes a SEC1 uncompressed point, validating it. *) 130 + end 131 + 132 + (** {1 SPAKE2 (RFC 9382)} *) 133 + 134 + type state 135 + (** Opaque state for the SPAKE2 protocol. *) 136 + 137 + val init : password:string -> role -> state * string 138 + (** [init ~password role] initializes SPAKE2 for the given [role]. 139 + 140 + @param password The shared password 141 + @param role Either [`A] or [`B] 142 + @return [(state, message)] where [message] is the public share to send *) 143 + 144 + val finish : 145 + ?context:string -> 146 + ?id_a:string -> 147 + ?id_b:string -> 148 + state -> 149 + string -> 150 + (string, string) result 151 + (** [finish ?context ?id_a ?id_b state peer_message] completes the protocol. 152 + 153 + @param context Optional context string for key derivation 154 + @param id_a Optional identity of party A 155 + @param id_b Optional identity of party B 156 + @param state The state from {!init} 157 + @param peer_message The message received from the peer 158 + @return [Ok shared_secret] (32 bytes) or [Error msg] *) 159 + 160 + (** {1 SPAKE2+ (Augmented PAKE)} *) 161 + 162 + module Plus : sig 163 + (** SPAKE2+ adds verifier storage security: the server stores only [w0] and 164 + [L = w1*G], not password-equivalent data. *) 165 + 166 + (** {2 Key Derivation} *) 167 + 168 + val derive_w : 169 + password:string -> salt:string -> iterations:int -> string * string 170 + (** [derive_w ~password ~salt ~iterations] derives [w0] and [w1] using 171 + PBKDF2-SHA256. 172 + 173 + @param password The user's password 174 + @param salt Random salt (should be at least 16 bytes) 175 + @param iterations PBKDF2 iteration count (minimum 1000) 176 + @return [(w0, w1)] where each is 32 bytes *) 177 + 178 + val compute_l : w1:string -> string 179 + (** [compute_l ~w1] computes [L = w1*G] for server storage. 180 + 181 + @return L encoded as SEC1 uncompressed point (65 bytes) *) 182 + 183 + (** {2 Prover (Client)} *) 184 + 185 + type prover_state 186 + (** Prover protocol state. *) 187 + 188 + val prover_init : 189 + w0:string -> w1:string -> context:string -> prover_state * string 190 + (** [prover_init ~w0 ~w1 ~context] initiates SPAKE2+ as prover. 191 + 192 + @return [(state, pA)] where pA is sent to the verifier *) 193 + 194 + val prover_finish : 195 + prover_state -> string -> (string * string * string, string) result 196 + (** [prover_finish state pB] processes the verifier's message. 197 + 198 + @return 199 + [Ok (shared_key, confirmation_a, expected_confirmation_b)] or error *) 200 + 201 + (** {2 Verifier (Server)} *) 202 + 203 + type verifier_state 204 + (** Verifier protocol state. *) 205 + 206 + val verifier_init : 207 + w0:string -> l:string -> context:string -> verifier_state * string 208 + (** [verifier_init ~w0 ~l ~context] initiates SPAKE2+ as verifier. 209 + 210 + @param l The stored [L = w1*G] value 211 + @return [(state, pB)] where pB is sent to the prover *) 212 + 213 + val verifier_finish : 214 + verifier_state -> string -> (string * string * string, string) result 215 + (** [verifier_finish state pA] processes the prover's message. 216 + 217 + @return 218 + [Ok (shared_key, confirmation_b, expected_confirmation_a)] or error *) 219 + 220 + (** {2 Utilities} *) 221 + 222 + val generate_salt : unit -> string 223 + (** [generate_salt ()] generates a random 32-byte salt. *) 224 + 225 + val default_iterations : int 226 + (** Default PBKDF2 iteration count (1000). *) 227 + end 228 + 229 + (** {1 Cryptographic Utilities} *) 230 + 231 + val sha256 : string -> string 232 + (** [sha256 data] computes SHA-256 hash. *) 233 + 234 + val hmac_sha256 : key:string -> string -> string 235 + (** [hmac_sha256 ~key data] computes HMAC-SHA256. *)
+42
spake2.opam
··· 1 + # This file is generated by dune, edit dune-project instead 2 + opam-version: "2.0" 3 + synopsis: "SPAKE2 and SPAKE2+ Password-Authenticated Key Exchange" 4 + description: """ 5 + Implementation of the SPAKE2 and SPAKE2+ protocols for password-authenticated 6 + key exchange. SPAKE2 (RFC 9382) allows two parties who share a password to 7 + derive a strong shared secret key, without revealing the password to 8 + eavesdroppers or allowing offline dictionary attacks. SPAKE2+ adds verifier 9 + storage security so the server doesn't store password-equivalent data.""" 10 + maintainer: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 11 + authors: ["Thomas Gazagnaire <thomas@gazagnaire.org>"] 12 + license: "MIT" 13 + homepage: "https://github.com/samoht/ocaml-spake2" 14 + bug-reports: "https://github.com/samoht/ocaml-spake2/issues" 15 + depends: [ 16 + "dune" {>= "3.0"} 17 + "ocaml" {>= "4.08"} 18 + "zarith" {>= "1.12"} 19 + "digestif" {>= "1.2.0"} 20 + "kdf" {>= "0.1"} 21 + "pbkdf2" {>= "0.1"} 22 + "crypto-rng" {>= "1.0.0"} 23 + "crypto-ec" {>= "1.0.0"} 24 + "logs" {>= "0.7.0"} 25 + "alcotest" {with-test} 26 + "crowbar" {with-test} 27 + "odoc" {with-doc} 28 + ] 29 + build: [ 30 + ["dune" "subst"] {dev} 31 + [ 32 + "dune" 33 + "build" 34 + "-p" 35 + name 36 + "-j" 37 + jobs 38 + "@install" 39 + "@runtest" {with-test} 40 + "@doc" {with-doc} 41 + ] 42 + ]
+3
test/dune
··· 1 + (test 2 + (name test) 3 + (libraries spake2 kdf.hkdf pbkdf2 alcotest crypto-rng.unix ohex))
+3
test/test.ml
··· 1 + let () = 2 + Crypto_rng_unix.use_default (); 3 + Alcotest.run "spake2" Test_spake2.suite
+218
test/test_spake2.ml
··· 1 + (*--------------------------------------------------------------------------- 2 + Copyright (c) 2025 Thomas Gazagnaire. All rights reserved. 3 + SPDX-License-Identifier: MIT 4 + ---------------------------------------------------------------------------*) 5 + 6 + (* P-256 curve tests *) 7 + 8 + let test_p256_encoding_roundtrip () = 9 + let g = Spake2.P256.generator in 10 + let encoded = Spake2.P256.to_bytes g in 11 + Alcotest.(check int) "encoded length" 65 (String.length encoded); 12 + Alcotest.(check char) "uncompressed prefix" '\x04' encoded.[0]; 13 + match Spake2.P256.of_bytes encoded with 14 + | Ok decoded -> 15 + (* Verify by re-encoding *) 16 + let re_encoded = Spake2.P256.to_bytes decoded in 17 + Alcotest.(check string) "roundtrip preserves encoding" encoded re_encoded 18 + | Error e -> Alcotest.fail ("decoding failed: " ^ e) 19 + 20 + let test_p256_negate () = 21 + let g = Spake2.P256.generator in 22 + let neg_g = Spake2.P256.negate g in 23 + let result = Spake2.P256.add g neg_g in 24 + (* Point at infinity encodes as single 0x00 byte *) 25 + let encoded = Spake2.P256.to_bytes result in 26 + Alcotest.(check int) "G + (-G) encodes to 1 byte" 1 (String.length encoded); 27 + Alcotest.(check char) "G + (-G) = O (infinity)" '\x00' encoded.[0] 28 + 29 + let test_p256_m_encoding () = 30 + let encoded = Spake2.P256.to_bytes Spake2.P256.m in 31 + Alcotest.(check int) "M encoded length" 65 (String.length encoded); 32 + Alcotest.(check char) "M uncompressed prefix" '\x04' encoded.[0] 33 + 34 + let test_p256_n_encoding () = 35 + let encoded = Spake2.P256.to_bytes Spake2.P256.n in 36 + Alcotest.(check int) "N encoded length" 65 (String.length encoded); 37 + Alcotest.(check char) "N uncompressed prefix" '\x04' encoded.[0] 38 + 39 + (* SPAKE2 protocol tests *) 40 + 41 + let test_spake2_basic () = 42 + let password = "correcthorsebatterystaple" in 43 + let state_a, msg_a = Spake2.init ~password `A in 44 + let state_b, msg_b = Spake2.init ~password `B in 45 + Alcotest.(check int) "message A length" 65 (String.length msg_a); 46 + Alcotest.(check int) "message B length" 65 (String.length msg_b); 47 + match (Spake2.finish state_a msg_b, Spake2.finish state_b msg_a) with 48 + | Ok key_a, Ok key_b -> 49 + Alcotest.(check int) "key A length" 32 (String.length key_a); 50 + Alcotest.(check int) "key B length" 32 (String.length key_b); 51 + Alcotest.(check string) "keys match" key_a key_b 52 + | Error e, _ -> Alcotest.fail ("A failed: " ^ e) 53 + | _, Error e -> Alcotest.fail ("B failed: " ^ e) 54 + 55 + let test_spake2_with_context () = 56 + let password = "secret" in 57 + let context = "myapp v1.0" in 58 + let state_a, msg_a = Spake2.init ~password `A in 59 + let state_b, msg_b = Spake2.init ~password `B in 60 + match 61 + (Spake2.finish ~context state_a msg_b, Spake2.finish ~context state_b msg_a) 62 + with 63 + | Ok key_a, Ok key_b -> 64 + Alcotest.(check string) "keys match with context" key_a key_b 65 + | Error e, _ -> Alcotest.fail ("A failed: " ^ e) 66 + | _, Error e -> Alcotest.fail ("B failed: " ^ e) 67 + 68 + let test_spake2_wrong_password () = 69 + let state_a, msg_a = Spake2.init ~password:"password1" `A in 70 + let state_b, msg_b = Spake2.init ~password:"password2" `B in 71 + match (Spake2.finish state_a msg_b, Spake2.finish state_b msg_a) with 72 + | Ok key_a, Ok key_b -> 73 + Alcotest.(check bool) 74 + "keys should NOT match with different passwords" false 75 + (String.equal key_a key_b) 76 + | Error e, _ -> Alcotest.fail ("A failed: " ^ e) 77 + | _, Error e -> Alcotest.fail ("B failed: " ^ e) 78 + 79 + let test_spake2_different_context () = 80 + let password = "secret" in 81 + let state_a, msg_a = Spake2.init ~password `A in 82 + let state_b, msg_b = Spake2.init ~password `B in 83 + match 84 + ( Spake2.finish ~context:"context1" state_a msg_b, 85 + Spake2.finish ~context:"context2" state_b msg_a ) 86 + with 87 + | Ok key_a, Ok key_b -> 88 + Alcotest.(check bool) 89 + "keys should NOT match with different contexts" false 90 + (String.equal key_a key_b) 91 + | Error e, _ -> Alcotest.fail ("A failed: " ^ e) 92 + | _, Error e -> Alcotest.fail ("B failed: " ^ e) 93 + 94 + (* SPAKE2+ protocol tests *) 95 + 96 + let test_spake2_plus_basic () = 97 + let password = "hunter2" in 98 + let salt = Spake2.Plus.generate_salt () in 99 + let iterations = 1000 in 100 + let context = "test" in 101 + let w0, w1 = Spake2.Plus.derive_w ~password ~salt ~iterations in 102 + let l = Spake2.Plus.compute_l ~w1 in 103 + Alcotest.(check int) "w0 length" 32 (String.length w0); 104 + Alcotest.(check int) "w1 length" 32 (String.length w1); 105 + Alcotest.(check int) "L length" 65 (String.length l); 106 + let prover_state, pa = Spake2.Plus.prover_init ~w0 ~w1 ~context in 107 + let verifier_state, pb = Spake2.Plus.verifier_init ~w0 ~l ~context in 108 + Alcotest.(check int) "pA length" 65 (String.length pa); 109 + Alcotest.(check int) "pB length" 65 (String.length pb); 110 + match 111 + ( Spake2.Plus.prover_finish prover_state pb, 112 + Spake2.Plus.verifier_finish verifier_state pa ) 113 + with 114 + | Ok (ke_p, ca, kcb_p), Ok (ke_v, cb, kca_v) -> 115 + Alcotest.(check string) "shared keys match" ke_p ke_v; 116 + Alcotest.(check int) "confirmation A length" 32 (String.length ca); 117 + Alcotest.(check int) "confirmation B length" 32 (String.length cb); 118 + let ca_expected = Spake2.hmac_sha256 ~key:kca_v pb in 119 + let cb_expected = Spake2.hmac_sha256 ~key:kcb_p pa in 120 + Alcotest.(check string) "prover can verify cB" cb cb_expected; 121 + Alcotest.(check string) "verifier can verify cA" ca ca_expected 122 + | Error e, _ -> Alcotest.fail ("prover failed: " ^ e) 123 + | _, Error e -> Alcotest.fail ("verifier failed: " ^ e) 124 + 125 + let test_spake2_plus_wrong_password () = 126 + let salt = Spake2.Plus.generate_salt () in 127 + let iterations = 1000 in 128 + let context = "test" in 129 + let w0_p, w1_p = 130 + Spake2.Plus.derive_w ~password:"password1" ~salt ~iterations 131 + in 132 + let w0_v, w1_v = 133 + Spake2.Plus.derive_w ~password:"password2" ~salt ~iterations 134 + in 135 + let l = Spake2.Plus.compute_l ~w1:w1_v in 136 + let prover_state, pa = Spake2.Plus.prover_init ~w0:w0_p ~w1:w1_p ~context in 137 + let verifier_state, pb = Spake2.Plus.verifier_init ~w0:w0_v ~l ~context in 138 + match 139 + ( Spake2.Plus.prover_finish prover_state pb, 140 + Spake2.Plus.verifier_finish verifier_state pa ) 141 + with 142 + | Ok (ke_p, _, _), Ok (ke_v, _, _) -> 143 + Alcotest.(check bool) 144 + "shared keys should NOT match" false (String.equal ke_p ke_v) 145 + | Error _, _ | _, Error _ -> () 146 + 147 + (* Crypto utility tests *) 148 + 149 + let test_sha256 () = 150 + let input = "hello" in 151 + let expected = 152 + "2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824" 153 + in 154 + let result = Spake2.sha256 input in 155 + Alcotest.(check string) "SHA256(hello)" expected (Ohex.encode result) 156 + 157 + let test_hmac_sha256 () = 158 + let key = "key" in 159 + let data = "The quick brown fox jumps over the lazy dog" in 160 + let expected = 161 + "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8" 162 + in 163 + let result = Spake2.hmac_sha256 ~key data in 164 + Alcotest.(check string) "HMAC-SHA256" expected (Ohex.encode result) 165 + 166 + let test_hkdf () = 167 + let ikm = Ohex.decode "0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b" in 168 + let salt = Ohex.decode "000102030405060708090a0b0c" in 169 + let info = Ohex.decode "f0f1f2f3f4f5f6f7f8f9" in 170 + let expected = 171 + "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865" 172 + in 173 + let prk = Hkdf.extract ~hash:`SHA256 ~salt ikm in 174 + let result = Hkdf.expand ~hash:`SHA256 ~prk ~info 42 in 175 + Alcotest.(check string) "HKDF-SHA256" expected (Ohex.encode result) 176 + 177 + let test_pbkdf2 () = 178 + let password = "password" in 179 + let salt = "salt" in 180 + let iterations = 1 in 181 + let expected = 182 + "120fb6cffcf8b32c43e7225256c4f837a86548c92ccc35480805987cb70be17b" 183 + in 184 + let result = Pbkdf2.derive ~password ~salt ~iterations ~length:32 in 185 + Alcotest.(check string) "PBKDF2-SHA256 c=1" expected (Ohex.encode result) 186 + 187 + let suite = 188 + [ 189 + ( "P256", 190 + [ 191 + Alcotest.test_case "encoding roundtrip" `Quick 192 + test_p256_encoding_roundtrip; 193 + Alcotest.test_case "negate" `Quick test_p256_negate; 194 + Alcotest.test_case "M encoding" `Quick test_p256_m_encoding; 195 + Alcotest.test_case "N encoding" `Quick test_p256_n_encoding; 196 + ] ); 197 + ( "SPAKE2", 198 + [ 199 + Alcotest.test_case "basic exchange" `Quick test_spake2_basic; 200 + Alcotest.test_case "with context" `Quick test_spake2_with_context; 201 + Alcotest.test_case "wrong password" `Quick test_spake2_wrong_password; 202 + Alcotest.test_case "different context" `Quick 203 + test_spake2_different_context; 204 + ] ); 205 + ( "SPAKE2+", 206 + [ 207 + Alcotest.test_case "basic exchange" `Quick test_spake2_plus_basic; 208 + Alcotest.test_case "wrong password" `Quick 209 + test_spake2_plus_wrong_password; 210 + ] ); 211 + ( "Crypto", 212 + [ 213 + Alcotest.test_case "SHA256" `Quick test_sha256; 214 + Alcotest.test_case "HMAC-SHA256" `Quick test_hmac_sha256; 215 + Alcotest.test_case "HKDF" `Quick test_hkdf; 216 + Alcotest.test_case "PBKDF2" `Quick test_pbkdf2; 217 + ] ); 218 + ]