use crate::crypto::{PublicKey, Signature, SigningKey};
use crate::receipt::{Receipt, RECEIPT_VERSION};
use crate::SIGNATURE_CONTEXT;
use std::fmt;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
const DEFAULT_RECEIPT_CAPACITY: usize = 10_000;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[non_exhaustive]
pub enum EvidencePolicy {
#[default]
Disabled,
BestEffort,
RequiredBeforeExecution,
}
pub trait ReceiptSigner: Send + Sync {
fn public_key(&self) -> PublicKey;
fn sign_receipt(
&self,
request: &ReceiptSigningRequest<'_>,
) -> Result<Signature, ReceiptSignerError>;
}
pub trait ReceiptSink: Send + Sync {
fn persist(&self, receipt: &Receipt) -> Result<ReceiptRef, ReceiptSinkError>;
}
pub struct ReceiptSigningRequest<'a> {
preimage: &'a [u8],
}
impl<'a> ReceiptSigningRequest<'a> {
pub(crate) fn new(preimage: &'a [u8]) -> Self {
Self { preimage }
}
pub fn purpose(&self) -> &'static str {
"receipt"
}
pub fn final_signing_bytes(&self) -> Vec<u8> {
let mut out = Vec::with_capacity(SIGNATURE_CONTEXT.len() + self.preimage.len());
out.extend_from_slice(SIGNATURE_CONTEXT);
out.extend_from_slice(self.preimage);
out
}
}
pub struct LocalReceiptSigner {
key: SigningKey,
}
impl LocalReceiptSigner {
pub fn new(key: SigningKey) -> Self {
Self { key }
}
pub fn for_development() -> Self {
Self {
key: SigningKey::generate(),
}
}
}
impl ReceiptSigner for LocalReceiptSigner {
fn public_key(&self) -> PublicKey {
self.key.public_key()
}
fn sign_receipt(
&self,
request: &ReceiptSigningRequest<'_>,
) -> Result<Signature, ReceiptSignerError> {
Ok(self.key.sign_raw(&request.final_signing_bytes()))
}
}
pub struct MemoryReceiptSink {
stored: Mutex<SinkBuf>,
max: usize,
deny_max: usize,
overflowed: AtomicUsize,
}
struct SinkBuf {
items: Vec<StoredReceipt>,
allows: usize,
denies: usize,
}
struct StoredReceipt {
receipt: Receipt,
deny: bool,
}
impl Default for MemoryReceiptSink {
fn default() -> Self {
Self::with_capacity(DEFAULT_RECEIPT_CAPACITY)
}
}
impl MemoryReceiptSink {
pub fn new() -> Self {
Self::default()
}
pub fn with_capacity(max: usize) -> Self {
Self {
stored: Mutex::new(SinkBuf {
items: Vec::new(),
allows: 0,
denies: 0,
}),
max: max.max(1),
deny_max: max.max(1),
overflowed: AtomicUsize::new(0),
}
}
pub fn overflowed(&self) -> usize {
self.overflowed.load(Ordering::Relaxed)
}
pub fn stored(&self) -> Vec<Receipt> {
self.stored
.lock()
.map(|g| g.items.iter().map(|item| item.receipt.clone()).collect())
.unwrap_or_default()
}
pub fn pending(&self) -> Vec<Receipt> {
self.stored()
}
pub fn peek_from(&self, cursor: usize) -> Vec<Receipt> {
let stored = self.stored();
if cursor >= stored.len() {
return Vec::new();
}
stored[cursor..].to_vec()
}
pub fn drop_prefix(&self, count: usize) -> usize {
let Ok(mut stored) = self.stored.lock() else {
return 0;
};
let n = count.min(stored.items.len());
let removed: Vec<bool> = stored.items.drain(..n).map(|item| item.deny).collect();
for deny in removed {
if deny {
stored.denies = stored.denies.saturating_sub(1);
} else {
stored.allows = stored.allows.saturating_sub(1);
}
}
n
}
}
fn receipt_is_deny(receipt: &Receipt) -> Option<bool> {
ciborium::from_reader::<crate::receipt::ReceiptPayload, _>(receipt.payload.as_slice())
.ok()
.map(|payload| payload.outcome == crate::receipt::Outcome::Deny)
}
impl ReceiptSink for MemoryReceiptSink {
fn persist(&self, receipt: &Receipt) -> Result<ReceiptRef, ReceiptSinkError> {
let deny = receipt_is_deny(receipt).ok_or(ReceiptSinkError::Unavailable)?;
let mut stored = self
.stored
.lock()
.map_err(|_| ReceiptSinkError::Unavailable)?;
let (used, cap) = if deny {
(stored.denies, self.deny_max)
} else {
(stored.allows, self.max)
};
if used >= cap {
self.overflowed.fetch_add(1, Ordering::Relaxed);
return Err(ReceiptSinkError::Unavailable);
}
stored.items.push(StoredReceipt {
receipt: receipt.clone(),
deny,
});
if deny {
stored.denies += 1;
} else {
stored.allows += 1;
}
Ok(ReceiptRef {
id: format!("mem:{}", stored.items.len()),
})
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ReceiptRef {
pub id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ReceiptSignerError {
Unavailable,
}
impl fmt::Display for ReceiptSignerError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unavailable => write!(f, "receipt signer unavailable"),
}
}
}
impl std::error::Error for ReceiptSignerError {}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ReceiptSinkError {
Unavailable,
}
impl fmt::Display for ReceiptSinkError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Unavailable => write!(f, "receipt sink unavailable"),
}
}
}
impl std::error::Error for ReceiptSinkError {}
pub(crate) fn sign_payload(
payload_bytes: &[u8],
signer: &dyn ReceiptSigner,
) -> Result<Receipt, ReceiptSignerError> {
let preimage = Receipt::signing_preimage(RECEIPT_VERSION, payload_bytes);
let request = ReceiptSigningRequest::new(&preimage);
let signature = signer.sign_receipt(&request)?;
Ok(Receipt {
receipt_version: RECEIPT_VERSION,
payload: payload_bytes.to_vec(),
signer_key: signer.public_key(),
signature,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::crypto::SigningKey;
use crate::receipt::{Receipt, ReceiptPayload};
#[test]
fn local_signer_verifies_against_core() {
let signer = LocalReceiptSigner::for_development();
let payload = ReceiptPayload::allow(
vec![0xA1, 0x01, 0x02],
"tool:read",
1_700_000_000,
"inv-1",
[3u8; 64],
);
let bytes = payload.to_cbor().unwrap();
let receipt = sign_payload(&bytes, &signer).unwrap();
let verified = receipt.verify_signature().unwrap();
assert_eq!(verified.action, "tool:read");
assert_eq!(verified.request_id, "inv-1");
}
#[test]
fn memory_sink_round_trips() {
let sink = MemoryReceiptSink::new();
let signer = LocalReceiptSigner::for_development();
let payload = ReceiptPayload::allow(vec![0xA0], "tool:read", 1, "inv-2", [1u8; 64]);
let receipt = sign_payload(&payload.to_cbor().unwrap(), &signer).unwrap();
let reference = sink.persist(&receipt).unwrap();
assert!(reference.id.starts_with("mem:"));
assert_eq!(sink.stored().len(), 1);
assert_eq!(sink.peek_from(0).len(), 1);
assert_eq!(sink.peek_from(0).len(), 1);
assert_eq!(sink.drop_prefix(1), 1);
assert!(sink.pending().is_empty());
}
#[test]
fn memory_sink_overflow_is_counted_and_unavailable() {
let sink = MemoryReceiptSink::with_capacity(1);
let signer = LocalReceiptSigner::for_development();
let payload = ReceiptPayload::allow(vec![0xA0], "tool:read", 1, "inv-2", [1u8; 64]);
let receipt = sign_payload(&payload.to_cbor().unwrap(), &signer).unwrap();
assert!(sink.persist(&receipt).is_ok());
assert_eq!(sink.persist(&receipt), Err(ReceiptSinkError::Unavailable));
assert_eq!(sink.stored().len(), 1);
assert_eq!(sink.overflowed(), 1);
}
#[test]
fn deny_receipts_do_not_consume_allow_budget() {
let sink = MemoryReceiptSink::with_capacity(1);
let signer = LocalReceiptSigner::for_development();
let deny = sign_payload(
&ReceiptPayload::deny_before_pop(vec![0xA0], "tool:read", 1, "deny-1", "denied")
.to_cbor()
.unwrap(),
&signer,
)
.unwrap();
let allow = sign_payload(
&ReceiptPayload::allow(vec![0xA0], "tool:read", 1, "inv-2", [1u8; 64])
.to_cbor()
.unwrap(),
&signer,
)
.unwrap();
assert!(sink.persist(&deny).is_ok());
assert_eq!(sink.persist(&deny), Err(ReceiptSinkError::Unavailable));
assert!(sink.persist(&allow).is_ok());
assert_eq!(sink.stored().len(), 2);
assert_eq!(sink.overflowed(), 1);
}
#[test]
fn undecodable_payload_does_not_consume_allow_budget() {
let sink = MemoryReceiptSink::with_capacity(1);
let signer = LocalReceiptSigner::for_development();
let mut garbage = sign_payload(
&ReceiptPayload::allow(vec![0xA0], "tool:read", 1, "inv-2", [1u8; 64])
.to_cbor()
.unwrap(),
&signer,
)
.unwrap();
garbage.payload = vec![0xff, 0x00];
assert_eq!(sink.persist(&garbage), Err(ReceiptSinkError::Unavailable));
let allow = sign_payload(
&ReceiptPayload::allow(vec![0xA0], "tool:read", 1, "inv-2", [1u8; 64])
.to_cbor()
.unwrap(),
&signer,
)
.unwrap();
assert!(sink.persist(&allow).is_ok());
assert_eq!(sink.stored().len(), 1);
}
#[test]
fn required_persist_failure_denies_without_running() {
use crate::constraints::ConstraintSet;
use crate::planes::Authorizer;
use crate::sdk::signer::LocalSigner;
use crate::sdk::{Call, Guard, PresentedAuthority, RevocationMode};
use crate::warrant::Warrant;
use std::collections::HashMap;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
struct FailSink;
impl ReceiptSink for FailSink {
fn persist(&self, _: &Receipt) -> Result<ReceiptRef, ReceiptSinkError> {
Err(ReceiptSinkError::Unavailable)
}
}
let issuer = SigningKey::generate();
let holder = SigningKey::generate();
let warrant = Warrant::builder()
.capability("read", ConstraintSet::new())
.holder(holder.public_key())
.ttl(Duration::from_secs(300))
.build(&issuer)
.unwrap();
let mut authorizer = Authorizer::new();
authorizer.add_trusted_root(issuer.public_key());
let guard = Guard::builder()
.authorizer(authorizer)
.revocation(RevocationMode::TtlOnly {
max_lifetime: Duration::from_secs(3600),
})
.evidence_policy(EvidencePolicy::RequiredBeforeExecution)
.receipt_signer(Arc::new(LocalReceiptSigner::for_development()))
.receipt_sink(Arc::new(FailSink))
.build()
.unwrap();
let authority =
PresentedAuthority::new(vec![warrant], Arc::new(LocalSigner::new(holder))).unwrap();
let args = HashMap::new();
let call = Call::borrowed("read", &args);
let runs = AtomicUsize::new(0);
let err = guard
.guard(&authority, &call, |_| {
runs.fetch_add(1, Ordering::SeqCst);
Ok::<_, &str>(())
})
.err()
.unwrap();
assert_eq!(runs.load(Ordering::SeqCst), 0);
match err {
crate::sdk::GuardError::Denied(denial) => {
assert_eq!(
denial.sdk_kind(),
Some(crate::sdk::SdkDenialKind::EvidenceUnavailable)
);
}
crate::sdk::GuardError::Operation(_) => panic!("operation must not run"),
}
}
#[test]
fn best_effort_persist_failure_still_allows() {
use crate::constraints::ConstraintSet;
use crate::planes::Authorizer;
use crate::sdk::signer::LocalSigner;
use crate::sdk::{Call, Guard, PresentedAuthority, RevocationMode};
use crate::warrant::Warrant;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
struct FailSink;
impl ReceiptSink for FailSink {
fn persist(&self, _: &Receipt) -> Result<ReceiptRef, ReceiptSinkError> {
Err(ReceiptSinkError::Unavailable)
}
}
let issuer = SigningKey::generate();
let holder = SigningKey::generate();
let warrant = Warrant::builder()
.capability("read", ConstraintSet::new())
.holder(holder.public_key())
.ttl(Duration::from_secs(300))
.build(&issuer)
.unwrap();
let mut authorizer = Authorizer::new();
authorizer.add_trusted_root(issuer.public_key());
let guard = Guard::builder()
.authorizer(authorizer)
.revocation(RevocationMode::TtlOnly {
max_lifetime: Duration::from_secs(3600),
})
.evidence_policy(EvidencePolicy::BestEffort)
.receipt_signer(Arc::new(LocalReceiptSigner::for_development()))
.receipt_sink(Arc::new(FailSink))
.build()
.unwrap();
let authority =
PresentedAuthority::new(vec![warrant], Arc::new(LocalSigner::new(holder))).unwrap();
let args = HashMap::new();
let call = Call::borrowed("read", &args);
let decision = guard.check(&authority, &call).expect("allow");
assert!(decision.receipt.is_some());
decision.receipt.unwrap().verify_signature().unwrap();
}
#[test]
fn observer_does_not_emit_receipts() {
use crate::constraints::ConstraintSet;
use crate::planes::Authorizer;
use crate::sdk::signer::LocalSigner;
use crate::sdk::{Call, Guard, PresentedAuthority, PresentedRequest, RevocationMode};
use crate::warrant::Warrant;
use chrono::Utc;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
let issuer = SigningKey::generate();
let holder = SigningKey::generate();
let warrant = Warrant::builder()
.capability("read", ConstraintSet::new())
.holder(holder.public_key())
.ttl(Duration::from_secs(300))
.build(&issuer)
.unwrap();
let mut authorizer = Authorizer::new();
authorizer.add_trusted_root(issuer.public_key());
let sink = Arc::new(MemoryReceiptSink::new());
let guard = Guard::builder()
.authorizer(authorizer)
.revocation(RevocationMode::TtlOnly {
max_lifetime: Duration::from_secs(3600),
})
.evidence_policy(EvidencePolicy::BestEffort)
.receipt_signer(Arc::new(LocalReceiptSigner::for_development()))
.receipt_sink(sink.clone())
.build()
.unwrap();
let authority =
PresentedAuthority::new(vec![warrant], Arc::new(LocalSigner::new(holder))).unwrap();
let args = HashMap::new();
let call = Call::borrowed("read", &args);
assert!(guard.check(&authority, &call).unwrap().receipt.is_some());
assert_eq!(sink.stored().len(), 1);
guard
.observe_until(Utc::now() + chrono::Duration::hours(1))
.observe(PresentedRequest::Holder(&authority), &call, || {
Ok::<_, &str>(())
})
.unwrap();
assert_eq!(sink.stored().len(), 1);
}
}