-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathmain.rs
More file actions
293 lines (261 loc) · 10.3 KB
/
Copy pathmain.rs
File metadata and controls
293 lines (261 loc) · 10.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
//! Rust reimplementation of D. J. Bernstein's `checkpassword` (checkpassword-0.90).
//!
//! Reads "login\0password\0timestamp\0..." on file descriptor 3, verifies the
//! credentials against the system password/shadow database via the *audited*
//! system `crypt(3)` (libxcrypt: yescrypt/$6$/bcrypt/...), drops privileges, and
//! execs the program given in argv[1..]. Exit codes match the C original:
//! 0 = ok (never returns; execs) 2 = misuse (bad args/input)
//! 1 = bad credentials 111 = temporary failure
//!
//! Behavior and the security hardening are ported 1:1 from the C fork; see
//! ../classic-checkpassword/checkpassword.c and its CHANGELOG.
use std::ffi::{CStr, CString};
use std::fs::File;
use std::io::Read;
use std::os::unix::ffi::OsStrExt;
use std::os::unix::io::FromRawFd;
use std::os::unix::process::CommandExt;
use std::process::{exit, Command};
use nix::unistd::{chdir, setgid, setgroups, setuid, Gid, Uid};
use zeroize::Zeroizing;
/// Burn a crypt() on failure paths so unknown users cost the same as valid ones
/// (login-enumeration timing leak). Fixed yescrypt setting at the Debian default
/// cost, same as the C fork's DUMMYSETTING.
const DUMMY_SETTING: &CStr = c"$y$j9T$checkpassworddummysalt0$";
// crypt(3) from libxcrypt (linked via build.rs). Not thread-safe (static
// return buffer), which is fine: this program is single-threaded.
extern "C" {
fn crypt(phrase: *const libc::c_char, setting: *const libc::c_char) -> *mut libc::c_char;
}
/// Max credential bytes on fd 3 (C original: `char up[513]`).
const MAX_INPUT: usize = 512;
fn main() {
let prog = match std::env::args_os().nth(1) {
Some(p) => p,
None => exit(2), // no subprogram to run
};
let argv: Vec<_> = std::env::args_os().skip(2).collect();
// --- read credentials from fd 3 -----------------------------------------
// File takes ownership of fd 3 and closes it on drop (matches close(3)).
let mut fd3 = unsafe { File::from_raw_fd(3) };
let mut buf: Zeroizing<Vec<u8>> = Zeroizing::new(Vec::new());
if fd3.by_ref().take((MAX_INPUT + 1) as u64).read_to_end(&mut buf).is_err() {
exit(111);
}
if buf.len() > MAX_INPUT {
exit(1); // oversized input
}
// --- parse "login\0password\0..." ---------------------------------------
// Both fields stay inside `buf`; each is already NUL-terminated by its
// delimiter, so we can hand pointers straight to libc without copying the
// plaintext out of the zeroized buffer.
let nul1 = match buf.iter().position(|&b| b == 0) {
Some(i) => i,
None => exit(2),
};
let pw_off = nul1 + 1;
let nul2 = match buf[pw_off..].iter().position(|&b| b == 0) {
Some(i) => pw_off + i,
None => exit(2),
};
let login_ptr = buf.as_ptr() as *const libc::c_char; // login = buf[..nul1], NUL at nul1
let pass_ptr = buf[pw_off..].as_ptr() as *const libc::c_char; // NUL at nul2
let _ = nul2; // silence: nul2 asserts the second NUL exists
// --- look up the account ------------------------------------------------
let acct = match getpwnam(login_ptr) {
Lookup::Found(a) => a,
Lookup::NotFound => fail_bad(pass_ptr, buf), // dummy crypt, wipe, exit 1
Lookup::TempFail => exit(111),
};
// Shadow entry, if any, overrides the passwd hash (usually "x" without it).
let mut stored = acct.passwd.clone();
match getspnam(login_ptr) {
Lookup::Found(hash) => stored = hash,
Lookup::NotFound => {}
Lookup::TempFail => exit(111),
}
// --- verify -------------------------------------------------------------
if stored.is_empty() {
fail_bad(pass_ptr, buf); // no usable hash (locked): burn time, exit 1
}
let stored_c = match CString::new(stored.clone()) {
Ok(c) => c,
Err(_) => fail_bad(pass_ptr, buf), // interior NUL in hash: treat as bad
};
// SAFETY: pass_ptr is a NUL-terminated slice of `buf`; stored_c is valid.
let encrypted = unsafe { crypt(pass_ptr, stored_c.as_ptr()) };
drop(buf); // plaintext no longer needed -> zeroized here
if encrypted.is_null() {
exit(1); // libxcrypt returns NULL on bad/locked salt ("!", "*", "x")
}
let encrypted = unsafe { CStr::from_ptr(encrypted) };
if !ct_eq(encrypted.to_bytes(), &stored) {
exit(1); // wrong password
}
// --- drop privileges (order matters: groups+gid before uid) -------------
let gid = Gid::from_raw(acct.gid);
let uid = Uid::from_raw(acct.uid);
if setgroups(&[gid]).is_err() || setgid(gid).is_err() || setuid(uid).is_err() {
exit(1);
}
if chdir(acct.dir.as_bytes()).is_err() {
exit(111);
}
// --- exec the subprogram with a clean USER/HOME/SHELL -------------------
let err = Command::new(&prog)
.args(&argv)
.env("USER", os(&acct.name))
.env("HOME", os(&acct.dir))
.env("SHELL", os(&acct.shell))
.exec();
let _ = err;
exit(111); // exec failed
}
/// Owned copy of the passwd fields we need, so it outlives the getpwnam_r buffer.
struct Account {
name: CString,
passwd: Vec<u8>,
uid: libc::uid_t,
gid: libc::gid_t,
dir: CString,
shell: CString,
}
enum Lookup<T> {
Found(T),
NotFound,
TempFail,
}
fn os(c: &CString) -> &std::ffi::OsStr {
std::ffi::OsStr::from_bytes(c.as_bytes())
}
fn cstr_bytes(p: *const libc::c_char) -> Vec<u8> {
if p.is_null() {
return Vec::new();
}
unsafe { CStr::from_ptr(p) }.to_bytes().to_vec()
}
fn cstring(p: *const libc::c_char) -> CString {
CString::new(cstr_bytes(p)).unwrap_or_default()
}
/// getpwnam_r wrapper. NotFound covers "no such user"; TempFail is ETXTBSY
/// (mirrors the C fork's stale-errno hardening).
fn getpwnam(name: *const libc::c_char) -> Lookup<Account> {
let mut pwd: libc::passwd = unsafe { std::mem::zeroed() };
let mut result: *mut libc::passwd = std::ptr::null_mut();
let mut cap = 1024usize;
loop {
let mut sbuf = vec![0i8; cap];
let ret = unsafe {
libc::getpwnam_r(name, &mut pwd, sbuf.as_mut_ptr(), cap, &mut result)
};
if ret == libc::ERANGE && cap < 1 << 20 {
cap *= 2;
continue;
}
if !result.is_null() {
return Lookup::Found(Account {
name: cstring(pwd.pw_name),
passwd: cstr_bytes(pwd.pw_passwd),
uid: pwd.pw_uid,
gid: pwd.pw_gid,
dir: cstring(pwd.pw_dir),
shell: cstring(pwd.pw_shell),
});
}
return if ret == libc::ETXTBSY { Lookup::TempFail } else { Lookup::NotFound };
}
}
/// getspnam_r wrapper. Found carries the shadow hash (sp_pwdp) bytes.
fn getspnam(name: *const libc::c_char) -> Lookup<Vec<u8>> {
let mut spwd: libc::spwd = unsafe { std::mem::zeroed() };
let mut result: *mut libc::spwd = std::ptr::null_mut();
let mut cap = 1024usize;
loop {
let mut sbuf = vec![0i8; cap];
let ret = unsafe {
libc::getspnam_r(name, &mut spwd, sbuf.as_mut_ptr(), cap, &mut result)
};
if ret == libc::ERANGE && cap < 1 << 20 {
cap *= 2;
continue;
}
if !result.is_null() {
return Lookup::Found(cstr_bytes(spwd.sp_pwdp));
}
return if ret == libc::ETXTBSY { Lookup::TempFail } else { Lookup::NotFound };
}
}
/// Failure path for unknown user / no usable hash: run a dummy crypt so the
/// timing matches a real verification, wipe the plaintext, then exit 1.
fn fail_bad(pass_ptr: *const libc::c_char, buf: Zeroizing<Vec<u8>>) -> ! {
unsafe { crypt(pass_ptr, DUMMY_SETTING.as_ptr()) };
drop(buf);
exit(1);
}
/// Constant-time byte-slice equality (length included).
fn ct_eq(a: &[u8], b: &[u8]) -> bool {
if a.len() != b.len() {
return false;
}
let mut diff = 0u8;
for (x, y) in a.iter().zip(b) {
diff |= x ^ y;
}
diff == 0
}
#[cfg(test)]
mod tests {
use super::*;
// Mirrors the fd-3 parser: returns Some((login, password)) or None on the
// exit-2 (misuse) inputs.
fn parse(buf: &[u8]) -> Option<(&[u8], &[u8])> {
if buf.len() > MAX_INPUT {
return None; // oversized -> exit 1, not a valid parse
}
let nul1 = buf.iter().position(|&b| b == 0)?;
let pw_off = nul1 + 1;
let rel = buf[pw_off..].iter().position(|&b| b == 0)?;
Some((&buf[..nul1], &buf[pw_off..pw_off + rel]))
}
#[test]
fn parse_wellformed() {
assert_eq!(parse(b"bob\0secret\0Y0\0"), Some((&b"bob"[..], &b"secret"[..])));
}
#[test]
fn parse_rejects_malformed() {
assert_eq!(parse(b""), None); // empty
assert_eq!(parse(b"loginonly"), None); // no NUL
assert_eq!(parse(b"login\0"), None); // missing password field
}
#[test]
fn parse_rejects_oversized() {
let big = vec![b'a'; MAX_INPUT + 1];
assert_eq!(parse(&big), None);
}
#[test]
fn ct_eq_behaves_like_eq() {
assert!(ct_eq(b"$y$abc", b"$y$abc"));
assert!(!ct_eq(b"$y$abc", b"$y$abd"));
assert!(!ct_eq(b"short", b"longer"));
assert!(ct_eq(b"", b""));
}
// End-to-end proof of the auth primitive: system crypt() + ct_eq accept the
// right password and reject the wrong one, using a real yescrypt ($y$) hash
// as stored in Debian 13 shadow. This is the path that must never fail
// silently, so it gets a live check against libxcrypt.
fn sys_crypt(phrase: &CStr, setting: &CStr) -> Vec<u8> {
let p = unsafe { crypt(phrase.as_ptr(), setting.as_ptr()) };
assert!(!p.is_null(), "crypt() returned NULL for a valid setting");
unsafe { CStr::from_ptr(p) }.to_bytes().to_vec()
}
#[test]
fn yescrypt_roundtrip_via_libxcrypt() {
// Hash a known password with a yescrypt setting, then verify exactly as
// main() does: re-crypt with the stored hash as the setting.
let stored = sys_crypt(c"correct horse", DUMMY_SETTING);
assert!(stored.starts_with(b"$y$"), "expected a yescrypt hash");
let stored_c = CString::new(stored.clone()).unwrap();
assert!(ct_eq(&sys_crypt(c"correct horse", &stored_c), &stored)); // right pw
assert!(!ct_eq(&sys_crypt(c"wrong horse", &stored_c), &stored)); // wrong pw
}
}