1
//! Support for cookie authentication within the RPC protocol.
2
use fs_mistrust::Mistrust;
3
use safelog::Sensitive;
4
#[cfg(feature = "rpc-server")]
5
use std::convert::Infallible;
6
use std::{
7
    fs, io,
8
    path::{Path, PathBuf},
9
    str::FromStr,
10
    sync::Arc,
11
};
12
use subtle::ConstantTimeEq as _;
13
use tiny_keccak::Hasher as _;
14
use zeroize::Zeroizing;
15

            
16
/// A secret cookie value, used in RPC authentication.
17
#[derive(Clone, Debug)]
18
pub struct Cookie {
19
    /// The value of the cookie.
20
    value: Sensitive<Zeroizing<[u8; COOKIE_LEN]>>,
21
}
22
impl AsRef<[u8; COOKIE_LEN]> for Cookie {
23
12
    fn as_ref(&self) -> &[u8; COOKIE_LEN] {
24
12
        self.value.as_inner()
25
12
    }
26
}
27

            
28
/// Length of an authentication cookie.
29
pub const COOKIE_LEN: usize = 32;
30

            
31
/// Length of `COOKIE_PREFIX`.
32
pub const COOKIE_PREFIX_LEN: usize = 32;
33

            
34
/// Length of the MAC values we use for cookie authentication.
35
const COOKIE_MAC_LEN: usize = 32;
36

            
37
/// Length of the nonce values we use for cookie authentication.
38
const 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.
44
pub const COOKIE_PREFIX: &[u8; COOKIE_PREFIX_LEN] = b"====== arti-rpc-cookie-v1 ======";
45

            
46
/// Customization string used to initialize TupleHash.
47
const TUPLEHASH_CUSTOMIZATION: &[u8] = b"arti-rpc-cookie-v1";
48

            
49
impl Cookie {
50
    /// Read an RPC cookie from a provided path.
51
4
    pub fn load(path: &Path, mistrust: &Mistrust) -> Result<Cookie, CookieAccessError> {
52
        use std::io::Read;
53

            
54
4
        let mut file = mistrust
55
4
            .verifier()
56
4
            .file_access()
57
4
            .follow_final_links(true)
58
4
            .open(path, fs::OpenOptions::new().read(true))?;
59

            
60
4
        let mut buf = [0_u8; COOKIE_PREFIX_LEN];
61
4
        file.read_exact(&mut buf)?;
62
4
        if &buf != COOKIE_PREFIX {
63
            return Err(CookieAccessError::FileFormat);
64
4
        }
65

            
66
4
        let mut cookie = Cookie {
67
4
            value: Default::default(),
68
4
        };
69
4
        file.read_exact(cookie.value.as_mut().as_mut())?;
70
4
        if file.read(&mut buf)? != 0 {
71
            return Err(CookieAccessError::FileFormat);
72
4
        }
73

            
74
4
        Ok(cookie)
75
4
    }
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
4
    pub fn create<R: rand::CryptoRng + rand::TryRng<Error = Infallible>>(
81
4
        path: &Path,
82
4
        rng: &mut R,
83
4
        mistrust: &Mistrust,
84
4
    ) -> 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
4
        let parent = path.parent().ok_or(CookieAccessError::UnusablePath)?;
90
4
        mistrust
91
4
            .verifier()
92
4
            .require_directory()
93
4
            .make_directory(parent)?;
94
4
        let mut file = mistrust.file_access().follow_final_links(true).open(
95
4
            path,
96
4
            fs::OpenOptions::new()
97
4
                .write(true)
98
4
                .create(true)
99
4
                .truncate(true),
100
        )?;
101
4
        let cookie = Self::new(rng);
102
4
        file.write_all(&COOKIE_PREFIX[..])?;
103
4
        file.write_all(cookie.value.as_inner().as_ref())?;
104

            
105
4
        Ok(cookie)
106
4
    }
107

            
108
    /// Create a new random cookie.
109
6
    fn new<R: rand::CryptoRng + rand::Rng>(rng: &mut R) -> Self {
110
6
        let mut cookie = Cookie {
111
6
            value: Default::default(),
112
6
        };
113
6
        rng.fill_bytes(cookie.value.as_mut().as_mut());
114
6
        cookie
115
6
    }
116

            
117
    /// Return an appropriately personalized TupleHash instance, keyed from this cookie.
118
4
    fn new_mac(&self) -> tiny_keccak::TupleHash {
119
4
        let mut mac = tiny_keccak::TupleHash::v128(TUPLEHASH_CUSTOMIZATION);
120
4
        mac.update(&**self.value);
121
4
        mac
122
4
    }
123

            
124
    /// Compute the "server_mac" value as in the RPC cookie authentication protocol.
125
2
    pub fn server_mac(
126
2
        &self,
127
2
        client_nonce: &CookieAuthNonce,
128
2
        server_nonce: &CookieAuthNonce,
129
2
        socket_canonical: &str,
130
2
    ) -> CookieAuthMac {
131
        // `server_mac = MAC(cookie, "Server", socket_canonical, client_nonce)`
132
2
        let mut mac = self.new_mac();
133
2
        mac.update(b"Server");
134
2
        mac.update(socket_canonical.as_bytes());
135
2
        mac.update(&**client_nonce.0);
136
2
        mac.update(&**server_nonce.0);
137
2
        CookieAuthMac::finalize_from(mac)
138
2
    }
139

            
140
    /// Compute the "client_mac" value as in the RPC cookie authentication protocol.
141
2
    pub fn client_mac(
142
2
        &self,
143
2
        client_nonce: &CookieAuthNonce,
144
2
        server_nonce: &CookieAuthNonce,
145
2
        socket_canonical: &str,
146
2
    ) -> CookieAuthMac {
147
        // `client_mac = MAC(cookie, "Client", socket_canonical, server_nonce)`
148
2
        let mut mac = self.new_mac();
149
2
        mac.update(b"Client");
150
2
        mac.update(socket_canonical.as_bytes());
151
2
        mac.update(&**client_nonce.0);
152
2
        mac.update(&**server_nonce.0);
153
2
        CookieAuthMac::finalize_from(mac)
154
2
    }
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]
160
pub 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
}
174
impl From<io::Error> for CookieAccessError {
175
    fn from(err: io::Error) -> Self {
176
        CookieAccessError::Io(Arc::new(err))
177
    }
178
}
179
impl 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)]
196
pub 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

            
203
impl 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]
213
pub 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)]
221
pub struct CookieAuthNonce(Sensitive<Zeroizing<[u8; COOKIE_NONCE_LEN]>>);
222
impl CookieAuthNonce {
223
    /// Create a new random nonce.
224
4
    pub fn new<R: rand::Rng + rand::CryptoRng>(rng: &mut R) -> Self {
225
4
        let mut nonce = Self(Default::default());
226
4
        rng.fill_bytes(nonce.0.as_mut().as_mut());
227
4
        nonce
228
4
    }
229
    /// Convert this nonce to a hexadecimal string.
230
2
    pub fn to_hex(&self) -> String {
231
2
        base16ct::upper::encode_string(&**self.0)
232
2
    }
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
10
    pub fn from_hex(s: &str) -> Result<Self, HexError> {
237
10
        let mut nonce = Self(Default::default());
238
6
        let decoded =
239
10
            base16ct::mixed::decode(s, nonce.0.as_mut()).map_err(|_| HexError::InvalidHex)?;
240
6
        if decoded.len() != COOKIE_NONCE_LEN {
241
2
            return Err(HexError::InvalidHex);
242
4
        }
243
4
        Ok(nonce)
244
10
    }
245
}
246
impl 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
}
251
impl 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)]
260
pub struct CookieAuthMac(Sensitive<Zeroizing<[u8; COOKIE_MAC_LEN]>>);
261
impl CookieAuthMac {
262
    /// Construct a MAC by finalizing the provided hasher.
263
4
    fn finalize_from(hasher: tiny_keccak::TupleHash) -> Self {
264
4
        let mut mac = Self(Default::default());
265
4
        hasher.finalize(mac.0.as_mut());
266
4
        mac
267
4
    }
268

            
269
    /// Convert this MAC to a hexadecimal string.
270
4
    pub fn to_hex(&self) -> String {
271
4
        base16ct::upper::encode_string(&**self.0)
272
4
    }
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
12
    pub fn from_hex(s: &str) -> Result<Self, HexError> {
277
12
        let mut mac = Self(Default::default());
278
8
        let decoded =
279
12
            base16ct::mixed::decode(s, mac.0.as_mut()).map_err(|_| HexError::InvalidHex)?;
280
8
        if decoded.len() != COOKIE_MAC_LEN {
281
2
            return Err(HexError::InvalidHex);
282
6
        }
283
6
        Ok(mac)
284
12
    }
285
}
286
impl 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
}
291
impl FromStr for CookieAuthMac {
292
    type Err = HexError;
293
    fn from_str(s: &str) -> Result<Self, Self::Err> {
294
        Self::from_hex(s)
295
    }
296
}
297
impl PartialEq for CookieAuthMac {
298
6
    fn eq(&self, other: &Self) -> bool {
299
6
        self.0.ct_eq(&**other.0).into()
300
6
    }
301
}
302
impl Eq for CookieAuthMac {}
303

            
304
#[cfg(test)]
305
mod 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
}