cloud_sdk/authentication/
binding.rs1use core::fmt;
2
3use subtle::ConstantTimeEq;
4
5pub const CREDENTIAL_BINDING_BYTES: usize = 32;
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
10pub enum CredentialBindingError {
11 AllZero,
13}
14
15impl_static_error!(CredentialBindingError,
16 Self::AllZero => "credential binding cannot be all zero",
17);
18
19#[derive(Clone, Copy, Eq)]
27pub struct CredentialBinding([u8; CREDENTIAL_BINDING_BYTES]);
28
29impl CredentialBinding {
30 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 pub fn with_bytes<R>(&self, inspect: impl FnOnce(&[u8; CREDENTIAL_BINDING_BYTES]) -> R) -> R {
48 inspect(&self.0)
49 }
50
51 #[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
70pub trait BoundCredentialTransport {
76 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}