1use fs_mistrust::Mistrust;
3use safelog::Sensitive;
4#[cfg(feature = "rpc-server")]
5use std::convert::Infallible;
6use std::{
7 fs, io,
8 path::{Path, PathBuf},
9 str::FromStr,
10 sync::Arc,
11};
12use subtle::ConstantTimeEq as _;
13use tiny_keccak::Hasher as _;
14use zeroize::Zeroizing;
15
16#[derive(Clone, Debug)]
18pub struct Cookie {
19 value: Sensitive<Zeroizing<[u8; COOKIE_LEN]>>,
21}
22impl AsRef<[u8; COOKIE_LEN]> for Cookie {
23 fn as_ref(&self) -> &[u8; COOKIE_LEN] {
24 self.value.as_inner()
25 }
26}
27
28pub const COOKIE_LEN: usize = 32;
30
31pub const COOKIE_PREFIX_LEN: usize = 32;
33
34const COOKIE_MAC_LEN: usize = 32;
36
37const COOKIE_NONCE_LEN: usize = 32;
39
40pub const COOKIE_PREFIX: &[u8; COOKIE_PREFIX_LEN] = b"====== arti-rpc-cookie-v1 ======";
45
46const TUPLEHASH_CUSTOMIZATION: &[u8] = b"arti-rpc-cookie-v1";
48
49impl Cookie {
50 pub fn load(path: &Path, mistrust: &Mistrust) -> Result<Cookie, CookieAccessError> {
52 use std::io::Read;
53
54 let mut file = mistrust
55 .verifier()
56 .file_access()
57 .follow_final_links(true)
58 .open(path, fs::OpenOptions::new().read(true))?;
59
60 let mut buf = [0_u8; COOKIE_PREFIX_LEN];
61 file.read_exact(&mut buf)?;
62 if &buf != COOKIE_PREFIX {
63 return Err(CookieAccessError::FileFormat);
64 }
65
66 let mut cookie = Cookie {
67 value: Default::default(),
68 };
69 file.read_exact(cookie.value.as_mut().as_mut())?;
70 if file.read(&mut buf)? != 0 {
71 return Err(CookieAccessError::FileFormat);
72 }
73
74 Ok(cookie)
75 }
76
77 #[cfg(feature = "rpc-server")]
80 pub fn create<R: rand::CryptoRng + rand::TryRng<Error = Infallible>>(
81 path: &Path,
82 rng: &mut R,
83 mistrust: &Mistrust,
84 ) -> Result<Cookie, CookieAccessError> {
85 use std::io::Write;
86
87 let parent = path.parent().ok_or(CookieAccessError::UnusablePath)?;
90 mistrust
91 .verifier()
92 .require_directory()
93 .make_directory(parent)?;
94 let mut file = mistrust.file_access().follow_final_links(true).open(
95 path,
96 fs::OpenOptions::new()
97 .write(true)
98 .create(true)
99 .truncate(true),
100 )?;
101 let cookie = Self::new(rng);
102 file.write_all(&COOKIE_PREFIX[..])?;
103 file.write_all(cookie.value.as_inner().as_ref())?;
104
105 Ok(cookie)
106 }
107
108 fn new<R: rand::CryptoRng + rand::Rng>(rng: &mut R) -> Self {
110 let mut cookie = Cookie {
111 value: Default::default(),
112 };
113 rng.fill_bytes(cookie.value.as_mut().as_mut());
114 cookie
115 }
116
117 fn new_mac(&self) -> tiny_keccak::TupleHash {
119 let mut mac = tiny_keccak::TupleHash::v128(TUPLEHASH_CUSTOMIZATION);
120 mac.update(&**self.value);
121 mac
122 }
123
124 pub fn server_mac(
126 &self,
127 client_nonce: &CookieAuthNonce,
128 server_nonce: &CookieAuthNonce,
129 socket_canonical: &str,
130 ) -> CookieAuthMac {
131 let mut mac = self.new_mac();
133 mac.update(b"Server");
134 mac.update(socket_canonical.as_bytes());
135 mac.update(&**client_nonce.0);
136 mac.update(&**server_nonce.0);
137 CookieAuthMac::finalize_from(mac)
138 }
139
140 pub fn client_mac(
142 &self,
143 client_nonce: &CookieAuthNonce,
144 server_nonce: &CookieAuthNonce,
145 socket_canonical: &str,
146 ) -> CookieAuthMac {
147 let mut mac = self.new_mac();
149 mac.update(b"Client");
150 mac.update(socket_canonical.as_bytes());
151 mac.update(&**client_nonce.0);
152 mac.update(&**server_nonce.0);
153 CookieAuthMac::finalize_from(mac)
154 }
155}
156
157#[derive(Clone, Debug, thiserror::Error)]
159#[non_exhaustive]
160pub enum CookieAccessError {
161 #[error("Unable to access cookie file")]
163 Access(#[from] fs_mistrust::Error),
164 #[error("IO error while accessing cookie file")]
166 Io(#[source] Arc<io::Error>),
167 #[error("Could not find parent directory or filename for cookie file")]
169 UnusablePath,
170 #[error("Path did not point to a cookie file")]
172 FileFormat,
173}
174impl From<io::Error> for CookieAccessError {
175 fn from(err: io::Error) -> Self {
176 CookieAccessError::Io(Arc::new(err))
177 }
178}
179impl crate::HasClientErrorAction for CookieAccessError {
180 fn client_action(&self) -> crate::ClientErrorAction {
181 use crate::ClientErrorAction as A;
182 use CookieAccessError as E;
183 match self {
184 E::Access(err) => err.client_action(),
185 E::Io(err) => crate::fs_error_action(err.as_ref()),
186 E::UnusablePath => A::Decline,
187 E::FileFormat => A::Abort,
190 }
191 }
192}
193
194#[derive(Debug, Clone)]
196pub struct CookieLocation {
197 pub(crate) path: PathBuf,
199 pub(crate) mistrust: Mistrust,
201}
202
203impl CookieLocation {
204 pub fn load(&self) -> Result<Cookie, CookieAccessError> {
206 Cookie::load(self.path.as_ref(), &self.mistrust)
207 }
208}
209
210#[derive(Clone, Debug, thiserror::Error)]
212#[non_exhaustive]
213pub enum HexError {
214 #[error("Invalid hexadecimal value")]
216 InvalidHex,
217}
218
219#[derive(Clone, Debug, serde_with::SerializeDisplay, serde_with::DeserializeFromStr)]
221pub struct CookieAuthNonce(Sensitive<Zeroizing<[u8; COOKIE_NONCE_LEN]>>);
222impl CookieAuthNonce {
223 pub fn new<R: rand::Rng + rand::CryptoRng>(rng: &mut R) -> Self {
225 let mut nonce = Self(Default::default());
226 rng.fill_bytes(nonce.0.as_mut().as_mut());
227 nonce
228 }
229 pub fn to_hex(&self) -> String {
231 base16ct::upper::encode_string(&**self.0)
232 }
233 pub fn from_hex(s: &str) -> Result<Self, HexError> {
237 let mut nonce = Self(Default::default());
238 let decoded =
239 base16ct::mixed::decode(s, nonce.0.as_mut()).map_err(|_| HexError::InvalidHex)?;
240 if decoded.len() != COOKIE_NONCE_LEN {
241 return Err(HexError::InvalidHex);
242 }
243 Ok(nonce)
244 }
245}
246impl std::fmt::Display for CookieAuthNonce {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 write!(f, "{}", self.to_hex())
249 }
250}
251impl FromStr for CookieAuthNonce {
252 type Err = HexError;
253 fn from_str(s: &str) -> Result<Self, Self::Err> {
254 Self::from_hex(s)
255 }
256}
257
258#[derive(Clone, Debug, serde_with::SerializeDisplay, serde_with::DeserializeFromStr)]
260pub struct CookieAuthMac(Sensitive<Zeroizing<[u8; COOKIE_MAC_LEN]>>);
261impl CookieAuthMac {
262 fn finalize_from(hasher: tiny_keccak::TupleHash) -> Self {
264 let mut mac = Self(Default::default());
265 hasher.finalize(mac.0.as_mut());
266 mac
267 }
268
269 pub fn to_hex(&self) -> String {
271 base16ct::upper::encode_string(&**self.0)
272 }
273 pub fn from_hex(s: &str) -> Result<Self, HexError> {
277 let mut mac = Self(Default::default());
278 let decoded =
279 base16ct::mixed::decode(s, mac.0.as_mut()).map_err(|_| HexError::InvalidHex)?;
280 if decoded.len() != COOKIE_MAC_LEN {
281 return Err(HexError::InvalidHex);
282 }
283 Ok(mac)
284 }
285}
286impl std::fmt::Display for CookieAuthMac {
287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 write!(f, "{}", self.to_hex())
289 }
290}
291impl FromStr for CookieAuthMac {
292 type Err = HexError;
293 fn from_str(s: &str) -> Result<Self, Self::Err> {
294 Self::from_hex(s)
295 }
296}
297impl PartialEq for CookieAuthMac {
298 fn eq(&self, other: &Self) -> bool {
299 self.0.ct_eq(&**other.0).into()
300 }
301}
302impl Eq for CookieAuthMac {}
303
304#[cfg(test)]
305mod test {
306 #![allow(clippy::bool_assert_comparison)]
308 #![allow(clippy::clone_on_copy)]
309 #![allow(clippy::dbg_macro)]
310 #![allow(clippy::mixed_attributes_style)]
311 #![allow(clippy::print_stderr)]
312 #![allow(clippy::print_stdout)]
313 #![allow(clippy::single_char_pattern)]
314 #![allow(clippy::unwrap_used)]
315 #![allow(clippy::unchecked_time_subtraction)]
316 #![allow(clippy::useless_vec)]
317 #![allow(clippy::needless_pass_by_value)]
318 #![allow(clippy::string_slice)] use super::*;
322 use crate::testing::tempdir;
323
324 #[test]
326 #[cfg(all(feature = "rpc-client", feature = "rpc-server"))]
327 fn cookie_file() {
328 let (_tempdir, dir, mistrust) = tempdir();
329 let path1 = dir.join("foo/foo.cookie");
330 let path2 = dir.join("bar.cookie");
331
332 let s_c1 = Cookie::create(path1.as_path(), &mut rand::rng(), &mistrust).unwrap();
333 let s_c2 = Cookie::create(path2.as_path(), &mut rand::rng(), &mistrust).unwrap();
334 assert_ne!(s_c1.as_ref(), s_c2.as_ref());
335
336 let c_c1 = Cookie::load(path1.as_path(), &mistrust).unwrap();
337 let c_c2 = Cookie::load(path2.as_path(), &mistrust).unwrap();
338 assert_eq!(s_c1.as_ref(), c_c1.as_ref());
339 assert_eq!(s_c2.as_ref(), c_c2.as_ref());
340 }
341
342 fn tuplehash(customization: &[u8], input: &[&[u8]]) -> [u8; 32] {
344 let mut th = tiny_keccak::TupleHash::v128(customization);
345 for v in input {
346 th.update(v);
347 }
348 let mut output: [u8; 32] = Default::default();
349 th.finalize(&mut output);
350 output
351 }
352
353 #[test]
355 fn auth_roundtrip() {
356 let addr = "127.0.0.1:9999";
357 let mut rng = rand::rng();
358 let client_nonce = CookieAuthNonce::new(&mut rng);
359 let server_nonce = CookieAuthNonce::new(&mut rng);
360 let cookie = Cookie::new(&mut rng);
361
362 let smac = cookie.server_mac(&client_nonce, &server_nonce, addr);
363 let cmac = cookie.client_mac(&client_nonce, &server_nonce, addr);
364
365 let smac_expected = tuplehash(
367 TUPLEHASH_CUSTOMIZATION,
368 &[
369 &**cookie.value,
370 b"Server",
371 addr.as_bytes(),
372 &**client_nonce.0,
373 &**server_nonce.0,
374 ],
375 );
376 let cmac_expected = tuplehash(
378 TUPLEHASH_CUSTOMIZATION,
379 &[
380 &**cookie.value,
381 b"Client",
382 addr.as_bytes(),
383 &**client_nonce.0,
384 &**server_nonce.0,
385 ],
386 );
387 assert_eq!(**smac.0, smac_expected);
388 assert_eq!(**cmac.0, cmac_expected);
389
390 let smac_hex = smac.to_hex();
391 let smac2 = CookieAuthMac::from_hex(smac_hex.as_str()).unwrap();
392 assert_eq!(smac, smac2);
393
394 assert_ne!(cmac, smac); }
396
397 #[test]
399 fn tuplehash_testvec() {
400 use hex_literal::hex;
402 let val = tuplehash(b"", &[&hex!("00 01 02"), &hex!("10 11 12 13 14 15")]);
403 assert_eq!(
404 val,
405 hex!(
406 "C5 D8 78 6C 1A FB 9B 82 11 1A B3 4B 65 B2 C0 04
407 8F A6 4E 6D 48 E2 63 26 4C E1 70 7D 3F FC 8E D1"
408 )
409 );
410
411 let val = tuplehash(
412 b"My Tuple App",
413 &[&hex!("00 01 02"), &hex!("10 11 12 13 14 15")],
414 );
415 assert_eq!(
416 val,
417 hex!(
418 "75 CD B2 0F F4 DB 11 54 E8 41 D7 58 E2 41 60 C5
419 4B AE 86 EB 8C 13 E7 F5 F4 0E B3 55 88 E9 6D FB"
420 )
421 );
422
423 let val = tuplehash(
424 b"My Tuple App",
425 &[
426 &hex!("00 01 02"),
427 &hex!("10 11 12 13 14 15"),
428 &hex!("20 21 22 23 24 25 26 27 28"),
429 ],
430 );
431 assert_eq!(
432 val,
433 hex!(
434 "E6 0F 20 2C 89 A2 63 1E DA 8D 4C 58 8C A5 FD 07
435 F3 9E 51 51 99 8D EC CF 97 3A DB 38 04 BB 6E 84"
436 )
437 );
438 }
439
440 #[test]
441 fn hex_encoding() {
442 let s = "0000000000000000000000000000000000000000000000000012345678ABCDEF";
443 let expected = [
444 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x12, 0x34,
445 0x56, 0x78, 0xAB, 0xCD, 0xEF,
446 ];
447 assert_eq!(s.len(), COOKIE_NONCE_LEN * 2);
448 assert_eq!(s.len(), COOKIE_MAC_LEN * 2);
449 let cn = CookieAuthNonce::from_hex(s).unwrap();
450 assert_eq!(**cn.0, expected);
451 assert_eq!(cn.to_hex().as_str(), s);
452
453 let cm = CookieAuthMac::from_hex(s).unwrap();
454 assert_eq!(**cm.0, expected);
455 assert_eq!(cm.to_hex().as_str(), s);
456
457 let s2 = s.to_ascii_lowercase();
458 let cn2 = CookieAuthNonce::from_hex(&s2).unwrap();
459 let cm2 = CookieAuthMac::from_hex(&s2).unwrap();
460 assert_eq!(cn2.0, cn.0);
461 assert_eq!(cm2, cm);
462
463 for bad in [
464 "12345678",
466 "0000000000000000000000000000000000000000000000000012345678XXXXXX",
468 "0000000000000000000000000000000000000000000000000012345678ABCDEF12345678",
470 ] {
471 dbg!(bad);
472 assert!(CookieAuthNonce::from_hex(bad).is_err());
473 assert!(CookieAuthMac::from_hex(bad).is_err());
474 }
475 }
476}