Skip to main content

context_authenticator/
lib.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use fidl_fuchsia_pkg as fpkg;
6use hmac::Mac as _;
7
8/// Creates and authenticates `fidl_fuchsia_pkg::ResolutionContext`s using an HMAC.
9/// The contexts contain the hash of the superpackage.
10#[derive(Clone, Debug)]
11pub struct ContextAuthenticator {
12    hmac: hmac::Hmac<sha2::Sha256>,
13}
14
15/// The size, in bytes, of the HMAC secret key.
16const SECRET_LEN: usize = 64;
17/// The size, in bytes, of the HMAC tag.
18const TAG_LEN: usize = 32;
19
20impl ContextAuthenticator {
21    /// Create a ContextAuthenticator initialized with a random secret key.
22    pub fn new() -> Self {
23        let mut secret = [0; SECRET_LEN];
24        let () = rand::fill(&mut secret[..]);
25        Self::from_secret(secret)
26    }
27
28    fn from_secret(secret: [u8; SECRET_LEN]) -> Self {
29        Self { hmac: hmac::Hmac::<sha2::Sha256>::new(secret.as_slice().into()) }
30    }
31
32    /// Create a `fidl_fuchsia_pkg::ResolutionContext`, tagged by this `ContextAuthenticator`'s
33    /// secret key, capable of being authenticated by `self.authenticate(context)`.
34    pub fn create(mut self, hash: &fuchsia_hash::Hash) -> fpkg::ResolutionContext {
35        let () = self.hmac.update(hash.as_bytes());
36        let mut bytes = self.hmac.finalize().into_bytes().to_vec();
37        bytes.extend_from_slice(hash.as_bytes());
38        fpkg::ResolutionContext { bytes }
39    }
40
41    /// Authenticate a `fidl_fuchsia_pkg::ResolutionContext` and return the wrapped
42    /// `fuchsia_hash::Hash`.
43    pub fn authenticate(
44        mut self,
45        context: fpkg::ResolutionContext,
46    ) -> Result<fuchsia_hash::Hash, ContextAuthenticatorError> {
47        let context: &[u8; TAG_LEN + fuchsia_hash::HASH_SIZE] = context
48            .bytes
49            .as_slice()
50            .try_into()
51            .map_err(|_| ContextAuthenticatorError::InvalidLength(context.bytes.len()))?;
52        let (tag, hash) = context.split_at(TAG_LEN);
53        let () = self.hmac.update(hash);
54        let () =
55            self.hmac.verify_slice(tag).map_err(ContextAuthenticatorError::AuthenticationFailed)?;
56        // This will never fail, but need a way to infallibly split an array reference into two
57        // array references to communicate that to the type system.
58        fuchsia_hash::Hash::try_from(hash)
59            .map_err(|_| ContextAuthenticatorError::InvalidLength(context.len()))
60    }
61}
62
63impl Default for ContextAuthenticator {
64    fn default() -> Self {
65        Self::new()
66    }
67}
68
69#[derive(thiserror::Error, Debug)]
70pub enum ContextAuthenticatorError {
71    #[error("expected context length {} found {}", TAG_LEN + fuchsia_hash::HASH_SIZE, .0)]
72    InvalidLength(usize),
73
74    #[error("authentication failed")]
75    AuthenticationFailed(#[source] hmac::digest::MacError),
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81    use assert_matches::assert_matches;
82
83    fn hash() -> fuchsia_hash::Hash {
84        [0; 32].into()
85    }
86
87    #[fuchsia::test]
88    fn tag_len() {
89        let authenticator = ContextAuthenticator::new();
90        assert_eq!(authenticator.hmac.finalize().into_bytes().len(), TAG_LEN);
91    }
92
93    #[fuchsia::test]
94    fn success() {
95        let authenticator = ContextAuthenticator::from_secret([0u8; SECRET_LEN]);
96        let context = authenticator.clone().create(&hash());
97        assert_eq!(authenticator.authenticate(context).unwrap(), hash());
98    }
99
100    #[fuchsia::test]
101    fn invalid_context_length() {
102        let authenticator = ContextAuthenticator::from_secret([0u8; SECRET_LEN]);
103        assert_matches!(
104            authenticator.authenticate(fpkg::ResolutionContext { bytes: vec![] }),
105            Err(ContextAuthenticatorError::InvalidLength(0))
106        );
107    }
108
109    #[fuchsia::test]
110    fn authentication_fails() {
111        let authenticator = ContextAuthenticator::from_secret([0u8; SECRET_LEN]);
112        let mut context = authenticator.clone().create(&hash());
113        context.bytes[0] = !context.bytes[0];
114        assert_matches!(
115            authenticator.authenticate(context),
116            Err(ContextAuthenticatorError::AuthenticationFailed(_))
117        );
118    }
119}