Skip to main content

tor_rpc_connect/auth/
cookie.rs

1//! Support for cookie authentication within the RPC protocol.
2use 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/// A secret cookie value, used in RPC authentication.
17#[derive(Clone, Debug)]
18pub struct Cookie {
19    /// The value of the cookie.
20    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
28/// Length of an authentication cookie.
29pub const COOKIE_LEN: usize = 32;
30
31/// Length of `COOKIE_PREFIX`.
32pub const COOKIE_PREFIX_LEN: usize = 32;
33
34/// Length of the MAC values we use for cookie authentication.
35const COOKIE_MAC_LEN: usize = 32;
36
37/// Length of the nonce values we use for cookie authentication.
38const COOKIE_NONCE_LEN: usize = 32;
39
40/// A value used to differentiate cookie files,
41/// and as a personalization parameter within the RPC cookie authentication protocol.
42///
43/// This is equivalent to `P` in the RPC cookie spec.
44pub const COOKIE_PREFIX: &[u8; COOKIE_PREFIX_LEN] = b"====== arti-rpc-cookie-v1 ======";
45
46/// Customization string used to initialize TupleHash.
47const TUPLEHASH_CUSTOMIZATION: &[u8] = b"arti-rpc-cookie-v1";
48
49impl Cookie {
50    /// Read an RPC cookie from a provided path.
51    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    /// Create a new RPC cookie and store it at a provided path,
78    /// overwriting any previous file at that location.
79    #[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        // NOTE: We do not use the "write and rename" pattern here,
88        // since it doesn't preserve file permissions.
89        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    /// Create a new random cookie.
109    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    /// Return an appropriately personalized TupleHash instance, keyed from this cookie.
118    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    /// Compute the "server_mac" value as in the RPC cookie authentication protocol.
125    pub fn server_mac(
126        &self,
127        client_nonce: &CookieAuthNonce,
128        server_nonce: &CookieAuthNonce,
129        socket_canonical: &str,
130    ) -> CookieAuthMac {
131        // `server_mac = MAC(cookie, "Server", socket_canonical, client_nonce)`
132        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    /// Compute the "client_mac" value as in the RPC cookie authentication protocol.
141    pub fn client_mac(
142        &self,
143        client_nonce: &CookieAuthNonce,
144        server_nonce: &CookieAuthNonce,
145        socket_canonical: &str,
146    ) -> CookieAuthMac {
147        // `client_mac = MAC(cookie, "Client", socket_canonical, server_nonce)`
148        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/// An error that has occurred while trying to load or create a cookie.
158#[derive(Clone, Debug, thiserror::Error)]
159#[non_exhaustive]
160pub enum CookieAccessError {
161    /// Unable to access cookie file due to an error from fs_mistrust
162    #[error("Unable to access cookie file")]
163    Access(#[from] fs_mistrust::Error),
164    /// Unable to access cookie file due to an IO error.
165    #[error("IO error while accessing cookie file")]
166    Io(#[source] Arc<io::Error>),
167    /// Calling `parent()` or `file_name() on the cookie path failed.
168    #[error("Could not find parent directory or filename for cookie file")]
169    UnusablePath,
170    /// Cookie file wasn't in the right format.
171    #[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            // We use the banner to make sure that we never read the cookie file before it is ready,
188            // so we don't need to worry about a partially written file.
189            E::FileFormat => A::Abort,
190        }
191    }
192}
193
194/// The location of a cookie on disk, and the rules to access it.
195#[derive(Debug, Clone)]
196pub struct CookieLocation {
197    /// Where the cookie is on disk.
198    pub(crate) path: PathBuf,
199    /// The mistrust we should use when loading it.
200    pub(crate) mistrust: Mistrust,
201}
202
203impl CookieLocation {
204    /// Try to read the cookie at this location.
205    pub fn load(&self) -> Result<Cookie, CookieAccessError> {
206        Cookie::load(self.path.as_ref(), &self.mistrust)
207    }
208}
209
210/// An error when decoding a hexadecimal value.
211#[derive(Clone, Debug, thiserror::Error)]
212#[non_exhaustive]
213pub enum HexError {
214    /// Hexadecimal value was wrong, or had the wrong length.
215    #[error("Invalid hexadecimal value")]
216    InvalidHex,
217}
218
219/// A random nonce used during cookie authentication protocol.
220#[derive(Clone, Debug, serde_with::SerializeDisplay, serde_with::DeserializeFromStr)]
221pub struct CookieAuthNonce(Sensitive<Zeroizing<[u8; COOKIE_NONCE_LEN]>>);
222impl CookieAuthNonce {
223    /// Create a new random nonce.
224    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    /// Convert this nonce to a hexadecimal string.
230    pub fn to_hex(&self) -> String {
231        base16ct::upper::encode_string(&**self.0)
232    }
233    /// Decode a nonce from a hexadecimal string.
234    ///
235    /// (Case-insensitive, no leading "0x" marker.  Output must be COOKIE_NONCE_LEN bytes long.)
236    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/// A MAC derived during the cookie authentication protocol.
259#[derive(Clone, Debug, serde_with::SerializeDisplay, serde_with::DeserializeFromStr)]
260pub struct CookieAuthMac(Sensitive<Zeroizing<[u8; COOKIE_MAC_LEN]>>);
261impl CookieAuthMac {
262    /// Construct a MAC by finalizing the provided hasher.
263    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    /// Convert this MAC to a hexadecimal string.
270    pub fn to_hex(&self) -> String {
271        base16ct::upper::encode_string(&**self.0)
272    }
273    /// Decode a MAC from a hexadecimal string.
274    ///
275    /// (Case-insensitive, no leading "0x" marker.  Output must be COOKIE_MAC_LEN bytes long.)
276    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    // @@ begin test lint list maintained by maint/add_warning @@
307    #![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)] // See arti#2571
319    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
320
321    use super::*;
322    use crate::testing::tempdir;
323
324    // Simple case: test creating and loading cookies.
325    #[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    /// Helper: Compute a TupleHash over the elements in input.
343    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    // Conformance test test for cryptography for cookie auth.
354    #[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        // `server_mac = MAC(cookie, "Server", socket_canonical, client_nonce)`
366        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        // `client_mac = MAC(cookie, "Client", socket_canonical, server_nonce)`
377        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); // Fails with P = 2^256 ;)
395    }
396
397    /// Basic tests for tuplehash crate, to make sure it does what we expect.
398    #[test]
399    fn tuplehash_testvec() {
400        // From http://csrc.nist.gov/groups/ST/toolkit/documents/Examples/TupleHash_samples.pdf
401        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            // too short
465            "12345678",
466            // bad characters
467            "0000000000000000000000000000000000000000000000000012345678XXXXXX",
468            // too long
469            "0000000000000000000000000000000000000000000000000012345678ABCDEF12345678",
470        ] {
471            dbg!(bad);
472            assert!(CookieAuthNonce::from_hex(bad).is_err());
473            assert!(CookieAuthMac::from_hex(bad).is_err());
474        }
475    }
476}