Skip to content

Commit a41c3fe

Browse files
author
Colin
authored
Merge pull request #5916 from contificate/crypt_r
CP-50518: Add binding for crypt_r to ocaml/auth
2 parents ccf3882 + 727592b commit a41c3fe

File tree

5 files changed

+319
-0
lines changed

5 files changed

+319
-0
lines changed

ocaml/auth/pam.ml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,27 @@
1515
external authenticate : string -> string -> unit = "stub_XA_mh_authorize"
1616

1717
external change_password : string -> string -> unit = "stub_XA_mh_chpasswd"
18+
19+
include (
20+
struct
21+
external unsafe_crypt_r : key:string -> setting:string -> string option
22+
= "stub_XA_crypt_r"
23+
end :
24+
sig
25+
val unsafe_crypt_r : key:string -> setting:string -> string option
26+
[@@alert unsafe "Direct usage of this function is not recommended."]
27+
end
28+
)
29+
30+
type crypt_algorithm = SHA256 | SHA512
31+
32+
type crypt_err = SaltTooLong | HashFailure
33+
34+
let crypt ~algo ~key ~salt =
35+
if String.length salt > 16 then
36+
Error SaltTooLong
37+
else
38+
let crypt_r = unsafe_crypt_r [@@alert "-unsafe"] in
39+
let algo_id = match algo with SHA256 -> 5 | SHA512 -> 6 in
40+
let setting = Printf.sprintf "$%d$%s$" algo_id salt in
41+
match crypt_r ~key ~setting with Some h -> Ok h | _ -> Error HashFailure

ocaml/auth/xa_auth_stubs.c

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,25 @@ void __attribute__((constructor)) stub_XA_workaround(void)
9595
crypt_r("", "$6$", &data);
9696
}
9797

98+
/* key:string -> setting:string -> string option */
99+
CAMLprim value stub_XA_crypt_r(value key, value setting) {
100+
CAMLparam2(key, setting);
101+
CAMLlocal1(result);
102+
103+
struct crypt_data cd = {0};
104+
105+
caml_enter_blocking_section();
106+
const char* const hashed =
107+
crypt_r(String_val(key), String_val(setting), &cd);
108+
caml_leave_blocking_section();
109+
110+
if (!hashed || *hashed == '*')
111+
CAMLreturn(Val_none);
112+
113+
result = caml_copy_string(hashed);
114+
CAMLreturn(caml_alloc_some(result));
115+
}
116+
98117
/*
99118
* Local variables:
100119
* mode: C

ocaml/quicktest/dune

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
http_lib
1414
mtime
1515
mtime.clock.os
16+
pam
1617
qcheck-alcotest
1718
result
1819
rresult

ocaml/quicktest/quicktest.ml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ let () =
4242
; ("Quicktest_max_vdi_size", Quicktest_max_vdi_size.tests ())
4343
; ("Quicktest_static_vdis", Quicktest_static_vdis.tests ())
4444
; ("Quicktest_date", Quicktest_date.tests ())
45+
; ("Quicktest_crypt_r", Quicktest_crypt_r.tests ())
4546
]
4647
@
4748
if not !Quicktest_args.using_unix_domain_socket then

ocaml/quicktest/quicktest_crypt_r.ml

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
(*
2+
* Copyright (c) Cloud Software Group, Inc
3+
*
4+
* This program is free software; you can redistribute it and/or modify
5+
* it under the terms of the GNU Lesser General Public License as published
6+
* by the Free Software Foundation; version 2.1 only. with the special
7+
* exception on linking described in file LICENSE.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU Lesser General Public License for more details.
13+
*)
14+
15+
module Pam = struct
16+
include Pam
17+
18+
let unsafe_crypt_r = Pam.unsafe_crypt_r [@@alert "-unsafe"]
19+
(* Suppress the alert the purpose of testing. *)
20+
end
21+
22+
let valid_salts =
23+
[
24+
"salt" (* Don't need to specify algorithm, will default to something. *)
25+
; "$5$salt$" (* 5 = SHA-256 should work. *)
26+
; "$6$salt$" (* 6 = SHA-512 should work. *)
27+
]
28+
29+
let invalid_salts =
30+
[
31+
"" (* Salt cannot be empty. *)
32+
; "$" (* Salt cannot be $. *)
33+
; "$9$salt$" (* Salt must specify valid algorithm constant. *)
34+
; "$6,rounds=1000$salt$" (* Salt cannot specify iteration count. *)
35+
; "£6£salt£" (* Only American currency is legal tender. *)
36+
]
37+
38+
let test_salts ~msg ~succeeds salts =
39+
let test salt =
40+
let actual =
41+
Option.is_some (Pam.unsafe_crypt_r ~key:"password" ~setting:salt)
42+
in
43+
Alcotest.((check' bool) ~msg ~expected:succeeds ~actual)
44+
in
45+
List.iter test salts
46+
47+
let test_valid_salts () =
48+
test_salts ~msg:"Hash can be computed from valid salt" ~succeeds:true
49+
valid_salts
50+
51+
let test_invalid_salts () =
52+
test_salts ~msg:"Hash cannot be computed from invalid salt" ~succeeds:false
53+
invalid_salts
54+
55+
let test_salt_truncation () =
56+
let salt_max_length = 16 in
57+
let salt = "a_salt_that_is_longer_than_is_actually_accepted" in
58+
assert (String.length salt > salt_max_length) ;
59+
let test prefix_length =
60+
(* The C API accepts at most 16 chars for the salt, optionally
61+
enclosed within $k$salt$ - anything else is ignored (implicitly
62+
truncated). *)
63+
let truncated_salt = String.sub salt 0 prefix_length in
64+
let sha512 = Printf.sprintf "$6$%s$" in
65+
let key = "password" in
66+
let h = Pam.unsafe_crypt_r ~key ~setting:(sha512 salt) in
67+
let h' = Pam.unsafe_crypt_r ~key ~setting:(sha512 truncated_salt) in
68+
if Option.(is_none h || is_none h') then
69+
failwith (Printf.sprintf "Failed to compute hash in %s" __FUNCTION__)
70+
else
71+
Option.equal ( = ) h h'
72+
in
73+
let msg =
74+
Printf.sprintf
75+
"Hash computed with implicitly truncated salt is the same as explicitly \
76+
truncated (len = %d)\n\
77+
."
78+
in
79+
let expectation len =
80+
(* We expect all lengths greater than max salt length to succeed,
81+
as they are implicitly truncated. Any length < salt_max_length
82+
should fail. *)
83+
len >= salt_max_length
84+
in
85+
for len = 0 to String.length salt do
86+
let msg = msg len in
87+
let actual = test len in
88+
let expected = expectation len in
89+
Alcotest.(check' bool) ~msg ~expected ~actual
90+
done
91+
92+
(* Invalidate the following tests if any hash fails to be computed. *)
93+
let unsafe_crypt_r ~key ~setting =
94+
match Pam.unsafe_crypt_r ~key ~setting with
95+
| Some hash ->
96+
hash
97+
| _ ->
98+
failwith "Invalid input provided to crypt_r"
99+
100+
let test_crypt_r_many_threads () =
101+
Printexc.record_backtrace true ;
102+
let settings = ["$6$salt$"; "$5$salt123$"; "$6$foobar$"; "salt"] in
103+
(* Each test case is a 3-tuple (key, setting, hash). A thread is
104+
spawned for each test case. The hash component stores the expected
105+
result of hashing key with setting. These hashes are computed prior
106+
to spawning the threads so they are guaranteed to have been computed
107+
sequentially. *)
108+
let test_cases =
109+
let create_case setting =
110+
let key = "password" in
111+
let hash = unsafe_crypt_r ~key ~setting in
112+
(key, setting, hash)
113+
in
114+
List.map create_case settings
115+
in
116+
let num_cases = List.length test_cases in
117+
let thread_count = Atomic.make 0 in
118+
let ready () = Atomic.get thread_count >= num_cases in
119+
let m = Mutex.create () in
120+
let c = Condition.create () in
121+
(* Each thread will populate an entry in the results array. *)
122+
let results : (unit, _) result array = Array.make num_cases (Ok ()) in
123+
let spawn i (key, setting, expectation) =
124+
let loop () =
125+
let now = Unix.gettimeofday in
126+
let start = now () in
127+
while now () -. start < 0.2 do
128+
let actual = unsafe_crypt_r ~key ~setting in
129+
if actual <> expectation then
130+
failwith (Printf.sprintf "%s <> %s" actual expectation)
131+
done
132+
in
133+
(* Record that this thread has been started, then wait for the
134+
main thread to broadcast that the others have also started. *)
135+
Atomic.incr thread_count ;
136+
Mutex.lock m ;
137+
while not (ready ()) do
138+
Condition.wait c m
139+
done ;
140+
Mutex.unlock m ;
141+
(* Run the test, capturing any exception as a result to the
142+
negative. *)
143+
results.(i) <- Rresult.R.trap_exn loop ()
144+
in
145+
(* Spawn a thread per valid test case. *)
146+
let tids = List.mapi (fun i -> Thread.create (spawn i)) test_cases in
147+
(* Wait for all threads to identify themselves as having started
148+
before broadcasting that they should start hashing. *)
149+
while not (ready ()) do
150+
Unix.sleepf 0.1
151+
done ;
152+
Mutex.lock m ;
153+
Condition.broadcast c ;
154+
Mutex.unlock m ;
155+
List.iter Thread.join tids ;
156+
(* Re-raise the first encountered trapped exception with its
157+
backtrace to ensure the test fails if any thread reported
158+
failure. *)
159+
let reraise = function
160+
| Error (`Exn_trap (exn, bt)) ->
161+
Printexc.raise_with_backtrace exn bt
162+
| _ ->
163+
()
164+
in
165+
Array.iter reraise results
166+
167+
(* This test hashes strings of language 'a'+ over a small range of lengths to
168+
ensure no duplicates occur. A suitable cryptographic hash function should have
169+
no collisions doing this. So, if a collision occurs, it is more likely because
170+
the underlying algorithm has a maximum length key size (and is truncating our
171+
input). *)
172+
let test_increasing_length () =
173+
let min, max = (50, 140) in
174+
(* Records hash -> length, so colliding lengths can be reported. *)
175+
let tbl = Hashtbl.create 127 in
176+
let setting = "$6$salt$" in
177+
let go len =
178+
let key = String.make len 'a' in
179+
let hash =
180+
try unsafe_crypt_r ~key ~setting
181+
with _ ->
182+
failwith (Printf.sprintf "Failed to compute hash aa..a of length %d" len)
183+
in
184+
match Hashtbl.find_opt tbl hash with
185+
| Some len' ->
186+
failwith
187+
(Printf.sprintf "Hash value a.. (len = %d) matches a.. (len %d)" len
188+
len'
189+
)
190+
| _ ->
191+
Hashtbl.add tbl hash len
192+
in
193+
for i = min to max do
194+
go i
195+
done
196+
197+
(* This test demonstrates the behaviour that the C API will
198+
(expectedly) only read up to the null terminator character. OCaml
199+
strings are stored as an array of words, with the final byte
200+
specifying how many padding bytes precede it. Since the number of
201+
words and number of padding bytes is used to determine string length,
202+
there is no reliance on a C-style null terminator - so '\0' can appear
203+
anywhere in an OCaml string. *)
204+
let test_c_truncation () =
205+
let key = "password" in
206+
let key' = key ^ "\x00_arbitrary_data_here" in
207+
let setting = "$6$salt$" in
208+
let hash = unsafe_crypt_r ~key ~setting in
209+
let hash' = unsafe_crypt_r ~key:key' ~setting in
210+
if hash <> hash' then
211+
failwith "Expected truncation using C-style null termination failed"
212+
213+
(* Make following tests fail if the safe API fails to return a valid result. *)
214+
let crypt ~algo ~key ~salt =
215+
let open struct exception CryptException of Pam.crypt_err end in
216+
match Pam.crypt ~algo ~key ~salt with
217+
| Ok hash ->
218+
hash
219+
| Error e ->
220+
raise (CryptException e)
221+
222+
(* Test trivial correspondence between safe API invocation and unsafe calls. *)
223+
let test_api_correspondence () =
224+
let cases =
225+
[
226+
("$5$salt123$", Pam.SHA256, "salt123")
227+
; ("$6$salt456$", Pam.SHA512, "salt456")
228+
]
229+
in
230+
let go (setting, algo, salt) =
231+
let key = "password" in
232+
let h = unsafe_crypt_r ~key ~setting in
233+
let h' = crypt ~algo ~key ~salt in
234+
if h <> h' then
235+
failwith
236+
"Hashes differ between invocations of safe and unsafe crypt_r APIs"
237+
in
238+
List.iter go cases
239+
240+
(** Ensure the safe API fails in the way you expect. *)
241+
let test_safe_failures () =
242+
let key = "password" in
243+
let cases =
244+
[
245+
(* Salt exceeding maximum length. *)
246+
( (fun () ->
247+
Pam.crypt ~algo:SHA256 ~key ~salt:"asaltthatexceedsthemaximumlength"
248+
)
249+
, Pam.SaltTooLong
250+
)
251+
]
252+
in
253+
let test (case, expected_error) =
254+
match case () with
255+
| Ok _ ->
256+
failwith "Expected crypt error"
257+
| Error e when e <> expected_error ->
258+
failwith "Actual crypt error does not match expectation"
259+
| Error _ ->
260+
()
261+
in
262+
List.iter test cases
263+
264+
let tests () =
265+
[
266+
("Valid salts", `Quick, test_valid_salts)
267+
; ("Invalid salts", `Quick, test_invalid_salts)
268+
; ("Implicit salt truncation", `Quick, test_salt_truncation)
269+
; ("Increasing string length", `Quick, test_increasing_length)
270+
; ("C-style termination", `Quick, test_c_truncation)
271+
; ("Safe and unsafe API", `Quick, test_api_correspondence)
272+
; ("Safe API error reporting", `Quick, test_safe_failures)
273+
; ("Multiple threads", `Quick, test_crypt_r_many_threads)
274+
]

0 commit comments

Comments
 (0)