use core::fmt;
use subtle::ConstantTimeEq;
pub const CREDENTIAL_BINDING_BYTES: usize = 32;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum CredentialBindingError {
AllZero,
}
impl_static_error!(CredentialBindingError,
Self::AllZero => "credential binding cannot be all zero",
);
#[derive(Clone, Copy, Eq)]
pub struct CredentialBinding([u8; CREDENTIAL_BINDING_BYTES]);
impl CredentialBinding {
pub const fn new(
value: [u8; CREDENTIAL_BINDING_BYTES],
) -> Result<Self, CredentialBindingError> {
if matches!(
value,
[
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,
0, 0, 0, 0,
]
) {
return Err(CredentialBindingError::AllZero);
}
Ok(Self(value))
}
pub fn with_bytes<R>(&self, inspect: impl FnOnce(&[u8; CREDENTIAL_BINDING_BYTES]) -> R) -> R {
inspect(&self.0)
}
#[must_use]
pub fn matches(self, other: Self) -> bool {
bool::from(self.0.ct_eq(&other.0))
}
}
impl PartialEq for CredentialBinding {
fn eq(&self, other: &Self) -> bool {
self.matches(*other)
}
}
impl fmt::Debug for CredentialBinding {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("CredentialBinding([redacted])")
}
}
pub trait BoundCredentialTransport {
fn credential_binding(&self) -> CredentialBinding;
}
#[cfg(test)]
mod tests {
use core::fmt::Write;
use super::{CREDENTIAL_BINDING_BYTES, CredentialBinding, CredentialBindingError};
struct DebugBuffer {
bytes: [u8; 64],
len: usize,
}
impl DebugBuffer {
const fn new() -> Self {
Self {
bytes: [0; 64],
len: 0,
}
}
fn as_str(&self) -> &str {
core::str::from_utf8(self.bytes.get(..self.len).unwrap_or_default()).unwrap_or_default()
}
}
impl core::fmt::Write for DebugBuffer {
fn write_str(&mut self, value: &str) -> core::fmt::Result {
let end = self.len.checked_add(value.len()).ok_or(core::fmt::Error)?;
let target = self.bytes.get_mut(self.len..end).ok_or(core::fmt::Error)?;
target.copy_from_slice(value.as_bytes());
self.len = end;
Ok(())
}
}
#[test]
fn bindings_reject_zero_compare_exactly_and_redact() {
assert_eq!(
CredentialBinding::new([0; CREDENTIAL_BINDING_BYTES]),
Err(CredentialBindingError::AllZero)
);
let first = CredentialBinding::new([1; CREDENTIAL_BINDING_BYTES])
.unwrap_or_else(|_| unreachable!("binding fixture failed"));
let same = CredentialBinding::new([1; CREDENTIAL_BINDING_BYTES])
.unwrap_or_else(|_| unreachable!("binding fixture failed"));
let other = CredentialBinding::new([2; CREDENTIAL_BINDING_BYTES])
.unwrap_or_else(|_| unreachable!("binding fixture failed"));
assert!(first.matches(same));
assert!(!first.matches(other));
let mut debug = DebugBuffer::new();
assert!(write!(debug, "{first:?}").is_ok());
assert_eq!(debug.as_str(), "CredentialBinding([redacted])");
}
}