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
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
319

            
320
    use super::*;
321
    use crate::testing::tempdir;
322

            
323
    // Simple case: test creating and loading cookies.
324
    #[test]
325
    #[cfg(all(feature = "rpc-client", feature = "rpc-server"))]
326
    fn cookie_file() {
327
        let (_tempdir, dir, mistrust) = tempdir();
328
        let path1 = dir.join("foo/foo.cookie");
329
        let path2 = dir.join("bar.cookie");
330

            
331
        let s_c1 = Cookie::create(path1.as_path(), &mut rand::rng(), &mistrust).unwrap();
332
        let s_c2 = Cookie::create(path2.as_path(), &mut rand::rng(), &mistrust).unwrap();
333
        assert_ne!(s_c1.as_ref(), s_c2.as_ref());
334

            
335
        let c_c1 = Cookie::load(path1.as_path(), &mistrust).unwrap();
336
        let c_c2 = Cookie::load(path2.as_path(), &mistrust).unwrap();
337
        assert_eq!(s_c1.as_ref(), c_c1.as_ref());
338
        assert_eq!(s_c2.as_ref(), c_c2.as_ref());
339
    }
340

            
341
    /// Helper: Compute a TupleHash over the elements in input.
342
    fn tuplehash(customization: &[u8], input: &[&[u8]]) -> [u8; 32] {
343
        let mut th = tiny_keccak::TupleHash::v128(customization);
344
        for v in input {
345
            th.update(v);
346
        }
347
        let mut output: [u8; 32] = Default::default();
348
        th.finalize(&mut output);
349
        output
350
    }
351

            
352
    // Conformance test test for cryptography for cookie auth.
353
    #[test]
354
    fn auth_roundtrip() {
355
        let addr = "127.0.0.1:9999";
356
        let mut rng = rand::rng();
357
        let client_nonce = CookieAuthNonce::new(&mut rng);
358
        let server_nonce = CookieAuthNonce::new(&mut rng);
359
        let cookie = Cookie::new(&mut rng);
360

            
361
        let smac = cookie.server_mac(&client_nonce, &server_nonce, addr);
362
        let cmac = cookie.client_mac(&client_nonce, &server_nonce, addr);
363

            
364
        // `server_mac = MAC(cookie, "Server", socket_canonical, client_nonce)`
365
        let smac_expected = tuplehash(
366
            TUPLEHASH_CUSTOMIZATION,
367
            &[
368
                &**cookie.value,
369
                b"Server",
370
                addr.as_bytes(),
371
                &**client_nonce.0,
372
                &**server_nonce.0,
373
            ],
374
        );
375
        // `client_mac = MAC(cookie, "Client", socket_canonical, server_nonce)`
376
        let cmac_expected = tuplehash(
377
            TUPLEHASH_CUSTOMIZATION,
378
            &[
379
                &**cookie.value,
380
                b"Client",
381
                addr.as_bytes(),
382
                &**client_nonce.0,
383
                &**server_nonce.0,
384
            ],
385
        );
386
        assert_eq!(**smac.0, smac_expected);
387
        assert_eq!(**cmac.0, cmac_expected);
388

            
389
        let smac_hex = smac.to_hex();
390
        let smac2 = CookieAuthMac::from_hex(smac_hex.as_str()).unwrap();
391
        assert_eq!(smac, smac2);
392

            
393
        assert_ne!(cmac, smac); // Fails with P = 2^256 ;)
394
    }
395

            
396
    /// Basic tests for tuplehash crate, to make sure it does what we expect.
397
    #[test]
398
    fn tuplehash_testvec() {
399
        // From http://csrc.nist.gov/groups/ST/toolkit/documents/Examples/TupleHash_samples.pdf
400
        use hex_literal::hex;
401
        let val = tuplehash(b"", &[&hex!("00 01 02"), &hex!("10 11 12 13 14 15")]);
402
        assert_eq!(
403
            val,
404
            hex!(
405
                "C5 D8 78 6C 1A FB 9B 82 11 1A B3 4B 65 B2 C0 04
406
                 8F A6 4E 6D 48 E2 63 26 4C E1 70 7D 3F FC 8E D1"
407
            )
408
        );
409

            
410
        let val = tuplehash(
411
            b"My Tuple App",
412
            &[&hex!("00 01 02"), &hex!("10 11 12 13 14 15")],
413
        );
414
        assert_eq!(
415
            val,
416
            hex!(
417
                "75 CD B2 0F F4 DB 11 54 E8 41 D7 58 E2 41 60 C5
418
                 4B AE 86 EB 8C 13 E7 F5 F4 0E B3 55 88 E9 6D FB"
419
            )
420
        );
421

            
422
        let val = tuplehash(
423
            b"My Tuple App",
424
            &[
425
                &hex!("00 01 02"),
426
                &hex!("10 11 12 13 14 15"),
427
                &hex!("20 21 22 23 24 25 26 27 28"),
428
            ],
429
        );
430
        assert_eq!(
431
            val,
432
            hex!(
433
                "E6 0F 20 2C 89 A2 63 1E DA 8D 4C 58 8C A5 FD 07
434
                 F3 9E 51 51 99 8D EC CF 97 3A DB 38 04 BB 6E 84"
435
            )
436
        );
437
    }
438

            
439
    #[test]
440
    fn hex_encoding() {
441
        let s = "0000000000000000000000000000000000000000000000000012345678ABCDEF";
442
        let expected = [
443
            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,
444
            0x56, 0x78, 0xAB, 0xCD, 0xEF,
445
        ];
446
        assert_eq!(s.len(), COOKIE_NONCE_LEN * 2);
447
        assert_eq!(s.len(), COOKIE_MAC_LEN * 2);
448
        let cn = CookieAuthNonce::from_hex(s).unwrap();
449
        assert_eq!(**cn.0, expected);
450
        assert_eq!(cn.to_hex().as_str(), s);
451

            
452
        let cm = CookieAuthMac::from_hex(s).unwrap();
453
        assert_eq!(**cm.0, expected);
454
        assert_eq!(cm.to_hex().as_str(), s);
455

            
456
        let s2 = s.to_ascii_lowercase();
457
        let cn2 = CookieAuthNonce::from_hex(&s2).unwrap();
458
        let cm2 = CookieAuthMac::from_hex(&s2).unwrap();
459
        assert_eq!(cn2.0, cn.0);
460
        assert_eq!(cm2, cm);
461

            
462
        for bad in [
463
            // too short
464
            "12345678",
465
            // bad characters
466
            "0000000000000000000000000000000000000000000000000012345678XXXXXX",
467
            // too long
468
            "0000000000000000000000000000000000000000000000000012345678ABCDEF12345678",
469
        ] {
470
            dbg!(bad);
471
            assert!(CookieAuthNonce::from_hex(bad).is_err());
472
            assert!(CookieAuthMac::from_hex(bad).is_err());
473
        }
474
    }
475
}