use fastmcp_core::sha256_bounded;
pub const MAX_BOUND_SOURCE_BYTES: usize = 8 * 1024 * 1024;
pub const SHA256_HEX_LENGTH: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ClosedChildBinding {
path: String,
owner_scope: String,
byte_length: usize,
sha256: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BindingDeclarationError {
EmptyPath,
EmptyOwnerScope,
DigestLength,
DigestNotLowercaseHex,
}
impl std::fmt::Display for BindingDeclarationError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EmptyPath => formatter.write_str("closed-child binding path must be nonempty"),
Self::EmptyOwnerScope => {
formatter.write_str("closed-child binding owner scope must be nonempty")
}
Self::DigestLength => {
formatter.write_str("closed-child binding digest must be 64 hex characters")
}
Self::DigestNotLowercaseHex => {
formatter.write_str("closed-child binding digest must be lowercase hex")
}
}
}
}
impl std::error::Error for BindingDeclarationError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BoundSourceError {
TooLarge {
supplied: usize,
ceiling: usize,
},
}
impl std::fmt::Display for BoundSourceError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::TooLarge { supplied, ceiling } => write!(
formatter,
"bound source of {supplied} bytes exceeds the {ceiling}-byte hashing bound"
),
}
}
}
impl std::error::Error for BoundSourceError {}
impl ClosedChildBinding {
pub fn declare(
path: &str,
owner_scope: &str,
byte_length: usize,
sha256: &str,
) -> Result<Self, BindingDeclarationError> {
if path.is_empty() {
return Err(BindingDeclarationError::EmptyPath);
}
if owner_scope.is_empty() {
return Err(BindingDeclarationError::EmptyOwnerScope);
}
if sha256.len() != SHA256_HEX_LENGTH {
return Err(BindingDeclarationError::DigestLength);
}
if !sha256
.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
{
return Err(BindingDeclarationError::DigestNotLowercaseHex);
}
Ok(Self {
path: path.to_owned(),
owner_scope: owner_scope.to_owned(),
byte_length,
sha256: sha256.to_owned(),
})
}
#[must_use]
pub fn path(&self) -> &str {
&self.path
}
#[must_use]
pub fn owner_scope(&self) -> &str {
&self.owner_scope
}
#[must_use]
pub const fn byte_length(&self) -> usize {
self.byte_length
}
#[must_use]
pub fn sha256(&self) -> &str {
&self.sha256
}
pub fn evaluate(&self, actual: &[u8]) -> Result<BindingOutcome, BoundSourceError> {
let digest = sha256_bounded(actual, MAX_BOUND_SOURCE_BYTES).map_err(|_| {
BoundSourceError::TooLarge {
supplied: actual.len(),
ceiling: MAX_BOUND_SOURCE_BYTES,
}
})?;
let actual_sha256 = lowercase_hex(digest.as_bytes());
Ok(BindingOutcome {
declared_byte_length: self.byte_length,
actual_byte_length: actual.len(),
digest_matches: actual_sha256 == self.sha256,
actual_sha256,
})
}
}
fn lowercase_hex(bytes: &[u8; 32]) -> String {
const DIGITS: &[u8; 16] = b"0123456789abcdef";
let mut rendered = String::with_capacity(SHA256_HEX_LENGTH);
for &byte in bytes {
rendered.push(char::from(DIGITS[usize::from(byte >> 4)]));
rendered.push(char::from(DIGITS[usize::from(byte & 0x0f)]));
}
rendered
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BindingOutcome {
declared_byte_length: usize,
actual_byte_length: usize,
actual_sha256: String,
digest_matches: bool,
}
impl BindingOutcome {
#[must_use]
pub const fn declared_byte_length(&self) -> usize {
self.declared_byte_length
}
#[must_use]
pub const fn actual_byte_length(&self) -> usize {
self.actual_byte_length
}
#[must_use]
pub fn actual_sha256(&self) -> &str {
&self.actual_sha256
}
#[must_use]
pub const fn length_matches(&self) -> bool {
self.declared_byte_length == self.actual_byte_length
}
#[must_use]
pub const fn digest_matches(&self) -> bool {
self.digest_matches
}
#[must_use]
pub const fn is_bound(&self) -> bool {
self.length_matches() && self.digest_matches()
}
#[must_use]
pub const fn drift(&self) -> BindingDrift {
match (self.length_matches(), self.digest_matches()) {
(true, true) => BindingDrift::Bound,
(true, false) => BindingDrift::ContentOnly,
(false, true) => BindingDrift::LengthOnly,
(false, false) => BindingDrift::LengthAndContent,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BindingDrift {
Bound,
ContentOnly,
LengthOnly,
LengthAndContent,
}
impl BindingDrift {
#[must_use]
pub const fn is_bound(self) -> bool {
matches!(self, Self::Bound)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FactDeclarationError {
EmptySubject,
EmptyDeclared,
}
impl std::fmt::Display for FactDeclarationError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::EmptySubject => formatter.write_str("declared fact subject must be nonempty"),
Self::EmptyDeclared => formatter.write_str("declared fact value must be nonempty"),
}
}
}
impl std::error::Error for FactDeclarationError {}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeclaredFact {
subject: String,
declared: String,
}
impl DeclaredFact {
pub fn declare(subject: &str, declared: &str) -> Result<Self, FactDeclarationError> {
if subject.is_empty() {
return Err(FactDeclarationError::EmptySubject);
}
if declared.is_empty() {
return Err(FactDeclarationError::EmptyDeclared);
}
Ok(Self {
subject: subject.to_owned(),
declared: declared.to_owned(),
})
}
#[must_use]
pub fn subject(&self) -> &str {
&self.subject
}
#[must_use]
pub fn declared(&self) -> &str {
&self.declared
}
#[must_use]
pub fn compare(&self, observed: &str) -> FactOutcome {
FactOutcome {
subject: self.subject.clone(),
declared: self.declared.clone(),
observed: observed.to_owned(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FactOutcome {
subject: String,
declared: String,
observed: String,
}
impl FactOutcome {
#[must_use]
pub fn subject(&self) -> &str {
&self.subject
}
#[must_use]
pub fn declared(&self) -> &str {
&self.declared
}
#[must_use]
pub fn observed(&self) -> &str {
&self.observed
}
#[must_use]
pub fn describes_repository(&self) -> bool {
self.declared == self.observed
}
}
impl std::fmt::Display for FactOutcome {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
formatter,
"{}: evidence declares {:?}, repository has {:?}",
self.subject, self.declared, self.observed
)
}
}