tor_rpc_connect/auth/
cookie.rs

1//! Support for cookie authentication within the RPC protocol.
2use fs_mistrust::Mistrust;
3use safelog::Sensitive;
4use std::{
5    fs, io,
6    path::{Path, PathBuf},
7    str::FromStr,
8    sync::Arc,
9};
10use subtle::ConstantTimeEq as _;
11use tiny_keccak::Hasher as _;
12use zeroize::Zeroizing;
13
14/// A secret cookie value, used in RPC authentication.
15#[derive(Clone, Debug)]
16pub struct Cookie {
17    /// The value of the cookie.
18    value: Sensitive<Zeroizing<[u8; COOKIE_LEN]>>,
19}
20impl AsRef<[u8; COOKIE_LEN]> for Cookie {
21    fn as_ref(&self) -> &[u8; COOKIE_LEN] {
22        self.value.as_inner()
23    }
24}
25
26/// Length of an authentication cookie.
27pub const COOKIE_LEN: usize = 32;
28
29/// Length of `COOKIE_PREFIX`.
30pub const COOKIE_PREFIX_LEN: usize = 32;
31
32/// Length of the MAC values we use for cookie authentication.
33const COOKIE_MAC_LEN: usize = 32;
34
35/// Length of the nonce values we use for cookie authentication.
36const COOKIE_NONCE_LEN: usize = 32;
37
38/// A value used to differentiate cookie files,
39/// and as a personalization parameter within the RPC cookie authentication protocol.
40///
41/// This is equivalent to `P` in the RPC cookie spec.
42pub const COOKIE_PREFIX: &[u8; COOKIE_PREFIX_LEN] = b"====== arti-rpc-cookie-v1 ======";
43
44/// Customization string used to initialize TupleHash.
45const TUPLEHASH_CUSTOMIZATION: &[u8] = b"arti-rpc-cookie-v1";
46
47impl Cookie {
48    /// Read an RPC cookie from a provided path.
49    pub fn load(path: &Path, mistrust: &Mistrust) -> Result<Cookie, CookieAccessError> {
50        use std::io::Read;
51
52        let mut file = mistrust
53            .verifier()
54            .file_access()
55            .follow_final_links(true)
56            .open(path, fs::OpenOptions::new().read(true))?;
57
58        let mut buf = [0_u8; COOKIE_PREFIX_LEN];
59        file.read_exact(&mut buf)?;
60        if &buf != COOKIE_PREFIX {
61            return Err(CookieAccessError::FileFormat);
62        }
63
64        let mut cookie = Cookie {
65            value: Default::default(),
66        };
67        file.read_exact(cookie.value.as_mut().as_mut())?;
68        if file.read(&mut buf)? != 0 {
69            return Err(CookieAccessError::FileFormat);
70        }
71
72        Ok(cookie)
73    }
74
75    /// Create a new RPC cookie and store it at a provided path,
76    /// overwriting any previous file at that location.
77    #[cfg(feature = "rpc-server")]
78    pub fn create<R: rand::CryptoRng + rand::RngCore>(
79        path: &Path,
80        rng: &mut R,
81        mistrust: &Mistrust,
82    ) -> Result<Cookie, CookieAccessError> {
83        use std::io::Write;
84
85        // NOTE: We do not use the "write and rename" pattern here,
86        // since it doesn't preserve file permissions.
87        let parent = path.parent().ok_or(CookieAccessError::UnusablePath)?;
88        mistrust
89            .verifier()
90            .require_directory()
91            .make_directory(parent)?;
92        let mut file = mistrust.file_access().follow_final_links(true).open(
93            path,
94            fs::OpenOptions::new()
95                .write(true)
96                .create(true)
97                .truncate(true),
98        )?;
99        let cookie = Self::new(rng);
100        file.write_all(&COOKIE_PREFIX[..])?;
101        file.write_all(cookie.value.as_inner().as_ref())?;
102
103        Ok(cookie)
104    }
105
106    /// Create a new random cookie.
107    fn new<R: rand::CryptoRng + rand::RngCore>(rng: &mut R) -> Self {
108        let mut cookie = Cookie {
109            value: Default::default(),
110        };
111        rng.fill_bytes(cookie.value.as_mut().as_mut());
112        cookie
113    }
114
115    /// Return an appropriately personalized TupleHash instance, keyed from this cookie.
116    fn new_mac(&self) -> tiny_keccak::TupleHash {
117        let mut mac = tiny_keccak::TupleHash::v128(TUPLEHASH_CUSTOMIZATION);
118        mac.update(&**self.value);
119        mac
120    }
121
122    /// Compute the "server_mac" value as in the RPC cookie authentication protocol.
123    pub fn server_mac(
124        &self,
125        client_nonce: &CookieAuthNonce,
126        server_nonce: &CookieAuthNonce,
127        socket_canonical: &str,
128    ) -> CookieAuthMac {
129        // `server_mac = MAC(cookie, "Server", socket_canonical, client_nonce)`
130        let mut mac = self.new_mac();
131        mac.update(b"Server");
132        mac.update(socket_canonical.as_bytes());
133        mac.update(&**client_nonce.0);
134        mac.update(&**server_nonce.0);
135        CookieAuthMac::finalize_from(mac)
136    }
137
138    /// Compute the "client_mac" value as in the RPC cookie authentication protocol.
139    pub fn client_mac(
140        &self,
141        client_nonce: &CookieAuthNonce,
142        server_nonce: &CookieAuthNonce,
143        socket_canonical: &str,
144    ) -> CookieAuthMac {
145        // `client_mac = MAC(cookie, "Client", socket_canonical, server_nonce)`
146        let mut mac = self.new_mac();
147        mac.update(b"Client");
148        mac.update(socket_canonical.as_bytes());
149        mac.update(&**client_nonce.0);
150        mac.update(&**server_nonce.0);
151        CookieAuthMac::finalize_from(mac)
152    }
153}
154
155/// An error that has occurred while trying to load or create a cookie.
156#[derive(Clone, Debug, thiserror::Error)]
157#[non_exhaustive]
158pub enum CookieAccessError {
159    /// Unable to access cookie file due to an error from fs_mistrust
160    #[error("Unable to access cookie file")]
161    Access(#[from] fs_mistrust::Error),
162    /// Unable to access cookie file due to an IO error.
163    #[error("IO error while accessing cookie file")]
164    Io(#[source] Arc<io::Error>),
165    /// Calling `parent()` or `file_name() on the cookie path failed.
166    #[error("Could not find parent directory or filename for cookie file")]
167    UnusablePath,
168    /// Cookie file wasn't in the right format.
169    #[error("Path did not point to a cookie file")]
170    FileFormat,
171}
172impl From<io::Error> for CookieAccessError {
173    fn from(err: io::Error) -> Self {
174        CookieAccessError::Io(Arc::new(err))
175    }
176}
177impl crate::HasClientErrorAction for CookieAccessError {
178    fn client_action(&self) -> crate::ClientErrorAction {
179        use crate::ClientErrorAction as A;
180        use CookieAccessError as E;
181        match self {
182            E::Access(err) => err.client_action(),
183            E::Io(err) => crate::fs_error_action(err.as_ref()),
184            E::UnusablePath => A::Decline,
185            // We use the banner to make sure that we never read the cookie file before it is ready,
186            // so we don't need to worry about a partially written file.
187            E::FileFormat => A::Abort,
188        }
189    }
190}
191
192/// The location of a cookie on disk, and the rules to access it.
193#[derive(Debug, Clone)]
194pub struct CookieLocation {
195    /// Where the cookie is on disk.
196    pub(crate) path: PathBuf,
197    /// The mistrust we should use when loading it.
198    pub(crate) mistrust: Mistrust,
199}
200
201impl CookieLocation {
202    /// Try to read the cookie at this location.
203    pub fn load(&self) -> Result<Cookie, CookieAccessError> {
204        Cookie::load(self.path.as_ref(), &self.mistrust)
205    }
206}
207
208/// An error when decoding a hexadecimal value.
209#[derive(Clone, Debug, thiserror::Error)]
210#[non_exhaustive]
211pub enum HexError {
212    /// Hexadecimal value was wrong, or had the wrong length.
213    #[error("Invalid hexadecimal value")]
214    InvalidHex,
215}
216
217/// A random nonce used during cookie authentication protocol.
218#[derive(Clone, Debug, serde_with::SerializeDisplay, serde_with::DeserializeFromStr)]
219pub struct CookieAuthNonce(Sensitive<Zeroizing<[u8; COOKIE_NONCE_LEN]>>);
220impl CookieAuthNonce {
221    /// Create a new random nonce.
222    pub fn new<R: rand::RngCore + rand::CryptoRng>(rng: &mut R) -> Self {
223        let mut nonce = Self(Default::default());
224        rng.fill_bytes(nonce.0.as_mut().as_mut());
225        nonce
226    }
227    /// Convert this nonce to a hexadecimal string.
228    pub fn to_hex(&self) -> String {
229        base16ct::upper::encode_string(&**self.0)
230    }
231    /// Decode a nonce from a hexadecimal string.
232    ///
233    /// (Case-insensitive, no leading "0x" marker.  Output must be COOKIE_NONCE_LEN bytes long.)
234    pub fn from_hex(s: &str) -> Result<Self, HexError> {
235        let mut nonce = Self(Default::default());
236        let decoded =
237            base16ct::mixed::decode(s, nonce.0.as_mut()).map_err(|_| HexError::InvalidHex)?;
238        if decoded.len() != COOKIE_NONCE_LEN {
239            return Err(HexError::InvalidHex);
240        }
241        Ok(nonce)
242    }
243}
244impl std::fmt::Display for CookieAuthNonce {
245    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
246        write!(f, "{}", self.to_hex())
247    }
248}
249impl FromStr for CookieAuthNonce {
250    type Err = HexError;
251    fn from_str(s: &str) -> Result<Self, Self::Err> {
252        Self::from_hex(s)
253    }
254}
255
256/// A MAC derived during the cookie authentication protocol.
257#[derive(Clone, Debug, serde_with::SerializeDisplay, serde_with::DeserializeFromStr)]
258pub struct CookieAuthMac(Sensitive<Zeroizing<[u8; COOKIE_MAC_LEN]>>);
259impl CookieAuthMac {
260    /// Construct a MAC by finalizing the provided hasher.
261    fn finalize_from(hasher: tiny_keccak::TupleHash) -> Self {
262        let mut mac = Self(Default::default());
263        hasher.finalize(mac.0.as_mut());
264        mac
265    }
266
267    /// Convert this MAC to a hexadecimal string.
268    pub fn to_hex(&self) -> String {
269        base16ct::upper::encode_string(&**self.0)
270    }
271    /// Decode a MAC from a hexadecimal string.
272    ///
273    /// (Case-insensitive, no leading "0x" marker.  Output must be COOKIE_MAC_LEN bytes long.)
274    pub fn from_hex(s: &str) -> Result<Self, HexError> {
275        let mut mac = Self(Default::default());
276        let decoded =
277            base16ct::mixed::decode(s, mac.0.as_mut()).map_err(|_| HexError::InvalidHex)?;
278        if decoded.len() != COOKIE_MAC_LEN {
279            return Err(HexError::InvalidHex);
280        }
281        Ok(mac)
282    }
283}
284impl std::fmt::Display for CookieAuthMac {
285    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
286        write!(f, "{}", self.to_hex())
287    }
288}
289impl FromStr for CookieAuthMac {
290    type Err = HexError;
291    fn from_str(s: &str) -> Result<Self, Self::Err> {
292        Self::from_hex(s)
293    }
294}
295impl PartialEq for CookieAuthMac {
296    fn eq(&self, other: &Self) -> bool {
297        self.0.ct_eq(&**other.0).into()
298    }
299}
300impl Eq for CookieAuthMac {}
301
302#[cfg(test)]
303mod test {
304    // @@ begin test lint list maintained by maint/add_warning @@
305    #![allow(clippy::bool_assert_comparison)]
306    #![allow(clippy::clone_on_copy)]
307    #![allow(clippy::dbg_macro)]
308    #![allow(clippy::mixed_attributes_style)]
309    #![allow(clippy::print_stderr)]
310    #![allow(clippy::print_stdout)]
311    #![allow(clippy::single_char_pattern)]
312    #![allow(clippy::unwrap_used)]
313    #![allow(clippy::unchecked_duration_subtraction)]
314    #![allow(clippy::useless_vec)]
315    #![allow(clippy::needless_pass_by_value)]
316    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
317
318    use super::*;
319    use crate::testing::tempdir;
320
321    // Simple case: test creating and loading cookies.
322    #[test]
323    #[cfg(all(feature = "rpc-client", feature = "rpc-server"))]
324    fn cookie_file() {
325        let (_tempdir, dir, mistrust) = tempdir();
326        let path1 = dir.join("foo/foo.cookie");
327        let path2 = dir.join("bar.cookie");
328
329        let s_c1 = Cookie::create(path1.as_path(), &mut rand::thread_rng(), &mistrust).unwrap();
330        let s_c2 = Cookie::create(path2.as_path(), &mut rand::thread_rng(), &mistrust).unwrap();
331        assert_ne!(s_c1.as_ref(), s_c2.as_ref());
332
333        let c_c1 = Cookie::load(path1.as_path(), &mistrust).unwrap();
334        let c_c2 = Cookie::load(path2.as_path(), &mistrust).unwrap();
335        assert_eq!(s_c1.as_ref(), c_c1.as_ref());
336        assert_eq!(s_c2.as_ref(), c_c2.as_ref());
337    }
338
339    /// Helper: Compute a TupleHash over the elements in input.
340    fn tuplehash(customization: &[u8], input: &[&[u8]]) -> [u8; 32] {
341        let mut th = tiny_keccak::TupleHash::v128(customization);
342        for v in input {
343            th.update(v);
344        }
345        let mut output: [u8; 32] = Default::default();
346        th.finalize(&mut output);
347        output
348    }
349
350    // Conformance test test for cryptography for cookie auth.
351    #[test]
352    fn auth_roundtrip() {
353        let addr = "127.0.0.1:9999";
354        let mut rng = rand::thread_rng();
355        let client_nonce = CookieAuthNonce::new(&mut rng);
356        let server_nonce = CookieAuthNonce::new(&mut rng);
357        let cookie = Cookie::new(&mut rng);
358
359        let smac = cookie.server_mac(&client_nonce, &server_nonce, addr);
360        let cmac = cookie.client_mac(&client_nonce, &server_nonce, addr);
361
362        // `server_mac = MAC(cookie, "Server", socket_canonical, client_nonce)`
363        let smac_expected = tuplehash(
364            TUPLEHASH_CUSTOMIZATION,
365            &[
366                &**cookie.value,
367                b"Server",
368                addr.as_bytes(),
369                &**client_nonce.0,
370                &**server_nonce.0,
371            ],
372        );
373        // `client_mac = MAC(cookie, "Client", socket_canonical, server_nonce)`
374        let cmac_expected = tuplehash(
375            TUPLEHASH_CUSTOMIZATION,
376            &[
377                &**cookie.value,
378                b"Client",
379                addr.as_bytes(),
380                &**client_nonce.0,
381                &**server_nonce.0,
382            ],
383        );
384        assert_eq!(**smac.0, smac_expected);
385        assert_eq!(**cmac.0, cmac_expected);
386
387        let smac_hex = smac.to_hex();
388        let smac2 = CookieAuthMac::from_hex(smac_hex.as_str()).unwrap();
389        assert_eq!(smac, smac2);
390
391        assert_ne!(cmac, smac); // Fails with P = 2^256 ;)
392    }
393
394    /// Basic tests for tuplehash crate, to make sure it does what we expect.
395    #[test]
396    fn tuplehash_testvec() {
397        // From http://csrc.nist.gov/groups/ST/toolkit/documents/Examples/TupleHash_samples.pdf
398        use hex_literal::hex;
399        let val = tuplehash(b"", &[&hex!("00 01 02"), &hex!("10 11 12 13 14 15")]);
400        assert_eq!(
401            val,
402            hex!(
403                "C5 D8 78 6C 1A FB 9B 82 11 1A B3 4B 65 B2 C0 04
404                 8F A6 4E 6D 48 E2 63 26 4C E1 70 7D 3F FC 8E D1"
405            )
406        );
407
408        let val = tuplehash(
409            b"My Tuple App",
410            &[&hex!("00 01 02"), &hex!("10 11 12 13 14 15")],
411        );
412        assert_eq!(
413            val,
414            hex!(
415                "75 CD B2 0F F4 DB 11 54 E8 41 D7 58 E2 41 60 C5
416                 4B AE 86 EB 8C 13 E7 F5 F4 0E B3 55 88 E9 6D FB"
417            )
418        );
419
420        let val = tuplehash(
421            b"My Tuple App",
422            &[
423                &hex!("00 01 02"),
424                &hex!("10 11 12 13 14 15"),
425                &hex!("20 21 22 23 24 25 26 27 28"),
426            ],
427        );
428        assert_eq!(
429            val,
430            hex!(
431                "E6 0F 20 2C 89 A2 63 1E DA 8D 4C 58 8C A5 FD 07
432                 F3 9E 51 51 99 8D EC CF 97 3A DB 38 04 BB 6E 84"
433            )
434        );
435    }
436
437    #[test]
438    fn hex_encoding() {
439        let s = "0000000000000000000000000000000000000000000000000012345678ABCDEF";
440        let expected = [
441            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,
442            0x56, 0x78, 0xAB, 0xCD, 0xEF,
443        ];
444        assert_eq!(s.len(), COOKIE_NONCE_LEN * 2);
445        assert_eq!(s.len(), COOKIE_MAC_LEN * 2);
446        let cn = CookieAuthNonce::from_hex(s).unwrap();
447        assert_eq!(**cn.0, expected);
448        assert_eq!(cn.to_hex().as_str(), s);
449
450        let cm = CookieAuthMac::from_hex(s).unwrap();
451        assert_eq!(**cm.0, expected);
452        assert_eq!(cm.to_hex().as_str(), s);
453
454        let s2 = s.to_ascii_lowercase();
455        let cn2 = CookieAuthNonce::from_hex(&s2).unwrap();
456        let cm2 = CookieAuthMac::from_hex(&s2).unwrap();
457        assert_eq!(cn2.0, cn.0);
458        assert_eq!(cm2, cm);
459
460        for bad in [
461            // too short
462            "12345678",
463            // bad characters
464            "0000000000000000000000000000000000000000000000000012345678XXXXXX",
465            // too long
466            "0000000000000000000000000000000000000000000000000012345678ABCDEF12345678",
467        ] {
468            dbg!(bad);
469            assert!(CookieAuthNonce::from_hex(bad).is_err());
470            assert!(CookieAuthMac::from_hex(bad).is_err());
471        }
472    }
473}