use std::marker::PhantomData;
mod sealed {
pub trait Sealed {}
}
pub trait Integrity: sealed::Sealed {
const TIER: &'static str;
const IS_VERIFIED: bool;
}
#[derive(Debug)]
pub enum Unverified {}
#[derive(Debug)]
pub enum TamperEvident {}
#[derive(Debug)]
pub enum Verified {}
impl sealed::Sealed for Unverified {}
impl sealed::Sealed for TamperEvident {}
impl sealed::Sealed for Verified {}
impl Integrity for Unverified {
const TIER: &'static str = "unverified";
const IS_VERIFIED: bool = false;
}
impl Integrity for TamperEvident {
const TIER: &'static str = "tamper-evident";
const IS_VERIFIED: bool = false;
}
impl Integrity for Verified {
const TIER: &'static str = "verified";
const IS_VERIFIED: bool = true;
}
#[derive(Debug)]
pub struct Blob<S: Integrity, T = axum::body::Bytes> {
payload: T,
_tier: PhantomData<S>,
}
impl<S: Integrity, T> Blob<S, T> {
#[must_use]
pub fn tier(&self) -> &'static str {
S::TIER
}
#[must_use]
pub fn payload(&self) -> &T {
&self.payload
}
#[must_use]
pub fn into_inner(self) -> T {
self.payload
}
}
impl<T> Blob<Unverified, T> {
#[must_use]
pub fn raw(payload: T) -> Self {
Self {
payload,
_tier: PhantomData,
}
}
}
impl<T> Blob<TamperEvident, T> {
#[must_use]
pub fn from_eof_verifier(payload: T) -> Self {
Self {
payload,
_tier: PhantomData,
}
}
}
impl<T: AsRef<[u8]>> Blob<Verified, T> {
pub fn verify(payload: T, expected_sha256_hex: &str) -> Result<Self, IntegrityError> {
use sha2::{Digest, Sha256};
let expected = expected_sha256_hex
.strip_prefix("sha256:")
.unwrap_or(expected_sha256_hex)
.to_ascii_lowercase();
let actual = hex::encode(Sha256::digest(payload.as_ref()));
if actual == expected {
Ok(Self {
payload,
_tier: PhantomData,
})
} else {
Err(IntegrityError::DigestMismatch { expected, actual })
}
}
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum IntegrityError {
#[error("integrity verification failed: expected {expected}, got {actual}")]
DigestMismatch {
expected: String,
actual: String,
},
}
#[derive(Debug)]
pub enum GateOutcome<T = axum::body::Bytes> {
Verified(Blob<Verified, T>),
Unpinned(Blob<Unverified, T>),
}
impl<T> GateOutcome<T> {
#[must_use]
pub fn is_verified(&self) -> bool {
matches!(self, GateOutcome::Verified(_))
}
#[must_use]
pub fn accept_open_world(self) -> T {
match self {
GateOutcome::Verified(b) => b.into_inner(),
GateOutcome::Unpinned(b) => b.into_inner(),
}
}
}
#[must_use]
pub fn verified_body<T>(blob: Blob<Verified, T>) -> T {
blob.into_inner()
}
pub trait Durability: sealed::Sealed {
const TIER: &'static str;
}
#[derive(Debug)]
pub enum Pinned {}
#[derive(Debug)]
pub enum Unpinnable {}
impl sealed::Sealed for Pinned {}
impl sealed::Sealed for Unpinnable {}
impl Durability for Pinned {
const TIER: &'static str = "pinned";
}
impl Durability for Unpinnable {
const TIER: &'static str = "unpinnable";
}
#[derive(Debug)]
pub struct StoreReceipt<P: Durability> {
key: String,
_tier: PhantomData<P>,
}
impl<P: Durability> StoreReceipt<P> {
#[must_use]
pub fn key(&self) -> &str {
&self.key
}
#[must_use]
pub fn tier(&self) -> &'static str {
P::TIER
}
}
impl StoreReceipt<Pinned> {
#[must_use]
pub fn pinned(key: impl Into<String>) -> Self {
Self {
key: key.into(),
_tier: PhantomData,
}
}
}
impl StoreReceipt<Unpinnable> {
#[must_use]
pub fn unpinnable(key: impl Into<String>) -> Self {
Self {
key: key.into(),
_tier: PhantomData,
}
}
}
pub fn require_pinned(receipt: StoreReceipt<Pinned>) -> String {
receipt.key
}
#[cfg(test)]
mod tests {
use super::*;
use sha2::{Digest, Sha256};
fn sha_hex(data: &[u8]) -> String {
hex::encode(Sha256::digest(data))
}
#[test]
fn verify_mints_on_match() {
let data = b"hello-world".to_vec();
let blob = Blob::<Verified, _>::verify(data.clone(), &sha_hex(&data))
.expect("matching digest must mint a Verified blob");
assert_eq!(blob.tier(), "verified");
assert_eq!(verified_body(blob), data);
}
#[test]
fn verify_accepts_sha256_prefix_and_uppercase() {
let data = b"prefixed".to_vec();
let with_prefix = format!("sha256:{}", sha_hex(&data).to_uppercase());
assert!(Blob::<Verified, _>::verify(data, &with_prefix).is_ok());
}
#[test]
fn verify_refuses_on_mismatch() {
let data = b"genuine".to_vec();
let wrong = sha_hex(b"tampered");
let err = Blob::<Verified, _>::verify(data, &wrong).unwrap_err();
match err {
IntegrityError::DigestMismatch { expected, actual } => {
assert_eq!(expected, wrong);
assert_eq!(actual, sha_hex(b"genuine"));
}
}
}
#[test]
fn raw_and_tamper_evident_carry_their_tiers() {
assert_eq!(Blob::raw(b"x".to_vec()).tier(), "unverified");
assert_eq!(
Blob::from_eof_verifier(b"x".to_vec()).tier(),
"tamper-evident"
);
}
#[test]
fn gate_outcome_forces_open_world_to_be_visible() {
let data = b"payload".to_vec();
let verified = GateOutcome::Verified(
Blob::<Verified, _>::verify(data.clone(), &sha_hex(&data)).unwrap(),
);
assert!(verified.is_verified());
assert_eq!(verified.accept_open_world(), data);
let open: GateOutcome<Vec<u8>> = GateOutcome::Unpinned(Blob::raw(b"no-pin".to_vec()));
assert!(!open.is_verified());
assert_eq!(open.accept_open_world(), b"no-pin".to_vec());
}
#[test]
fn store_receipt_tiers() {
assert_eq!(StoreReceipt::pinned("raw/a").tier(), "pinned");
assert_eq!(StoreReceipt::unpinnable("raw/b").tier(), "unpinnable");
assert_eq!(require_pinned(StoreReceipt::pinned("raw/c")), "raw/c");
}
}