Skip to main content

cloud_sdk/authentication/
binding.rs

1use core::fmt;
2
3use subtle::ConstantTimeEq;
4
5/// Exact bytes required for one opaque credential-lineage identity.
6pub const CREDENTIAL_BINDING_BYTES: usize = 32;
7
8/// Invalid transport-owned credential-lineage identity.
9#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum CredentialBindingError {
11    /// An all-zero value cannot identify a credential lifecycle.
12    AllZero,
13}
14
15impl_static_error!(CredentialBindingError,
16    Self::AllZero => "credential binding cannot be all zero",
17);
18
19/// Opaque identity for one transport credential lifecycle.
20///
21/// Transport implementations must generate this value with a CSPRNG when a
22/// credential is created and retain it across clones of that same credential.
23/// Rotation or replacement must create a different binding. The value is not
24/// an authentication secret, but diagnostics remain redacted so it cannot
25/// become an application-visible correlation identifier by accident.
26#[derive(Clone, Copy, Eq)]
27pub struct CredentialBinding([u8; CREDENTIAL_BINDING_BYTES]);
28
29impl CredentialBinding {
30    /// Validates a transport-generated credential-lineage identity.
31    pub const fn new(
32        value: [u8; CREDENTIAL_BINDING_BYTES],
33    ) -> Result<Self, CredentialBindingError> {
34        if matches!(
35            value,
36            [
37                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, 0, 0, 0,
38                0, 0, 0, 0,
39            ]
40        ) {
41            return Err(CredentialBindingError::AllZero);
42        }
43        Ok(Self(value))
44    }
45
46    /// Runs a closure with the exact binding bytes for protected evidence use.
47    pub fn with_bytes<R>(&self, inspect: impl FnOnce(&[u8; CREDENTIAL_BINDING_BYTES]) -> R) -> R {
48        inspect(&self.0)
49    }
50
51    /// Compares two credential lineages without data-dependent early exit.
52    #[must_use]
53    pub fn matches(self, other: Self) -> bool {
54        bool::from(self.0.ct_eq(&other.0))
55    }
56}
57
58impl PartialEq for CredentialBinding {
59    fn eq(&self, other: &Self) -> bool {
60        self.matches(*other)
61    }
62}
63
64impl fmt::Debug for CredentialBinding {
65    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
66        formatter.write_str("CredentialBinding([redacted])")
67    }
68}
69
70/// Authenticated transport exposing its current opaque credential lineage.
71///
72/// This contract is required only by provider operations whose authorization
73/// depends on a prior authenticated observation. Implementations must return
74/// the binding of the credential that `send_authenticated` will use.
75pub trait BoundCredentialTransport {
76    /// Returns the current transport credential lifecycle.
77    fn credential_binding(&self) -> CredentialBinding;
78}
79
80#[cfg(test)]
81mod tests {
82    use core::fmt::Write;
83
84    use super::{CREDENTIAL_BINDING_BYTES, CredentialBinding, CredentialBindingError};
85
86    struct DebugBuffer {
87        bytes: [u8; 64],
88        len: usize,
89    }
90
91    impl DebugBuffer {
92        const fn new() -> Self {
93            Self {
94                bytes: [0; 64],
95                len: 0,
96            }
97        }
98
99        fn as_str(&self) -> &str {
100            core::str::from_utf8(self.bytes.get(..self.len).unwrap_or_default()).unwrap_or_default()
101        }
102    }
103
104    impl core::fmt::Write for DebugBuffer {
105        fn write_str(&mut self, value: &str) -> core::fmt::Result {
106            let end = self.len.checked_add(value.len()).ok_or(core::fmt::Error)?;
107            let target = self.bytes.get_mut(self.len..end).ok_or(core::fmt::Error)?;
108            target.copy_from_slice(value.as_bytes());
109            self.len = end;
110            Ok(())
111        }
112    }
113
114    #[test]
115    fn bindings_reject_zero_compare_exactly_and_redact() {
116        assert_eq!(
117            CredentialBinding::new([0; CREDENTIAL_BINDING_BYTES]),
118            Err(CredentialBindingError::AllZero)
119        );
120        let first = CredentialBinding::new([1; CREDENTIAL_BINDING_BYTES])
121            .unwrap_or_else(|_| unreachable!("binding fixture failed"));
122        let same = CredentialBinding::new([1; CREDENTIAL_BINDING_BYTES])
123            .unwrap_or_else(|_| unreachable!("binding fixture failed"));
124        let other = CredentialBinding::new([2; CREDENTIAL_BINDING_BYTES])
125            .unwrap_or_else(|_| unreachable!("binding fixture failed"));
126        assert!(first.matches(same));
127        assert!(!first.matches(other));
128        let mut debug = DebugBuffer::new();
129        assert!(write!(debug, "{first:?}").is_ok());
130        assert_eq!(debug.as_str(), "CredentialBinding([redacted])");
131    }
132}