1
//! Support for cookie authentication within the RPC protocol.
2
use fs_mistrust::Mistrust;
3
use safelog::Sensitive;
4
use std::{
5
    fs, io,
6
    path::{Path, PathBuf},
7
    str::FromStr,
8
    sync::Arc,
9
};
10
use subtle::ConstantTimeEq as _;
11
use tiny_keccak::Hasher as _;
12
use zeroize::Zeroizing;
13

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

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

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

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

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

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

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

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

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

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

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

            
103
4
        Ok(cookie)
104
4
    }
105

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

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

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

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

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

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

            
302
#[cfg(test)]
303
mod 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::rng(), &mistrust).unwrap();
330
        let s_c2 = Cookie::create(path2.as_path(), &mut rand::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::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
}