use serde_json::Value;
use sha2::{Digest, Sha256};
use crate::approval::VerifiedResponse;
use crate::canon::canon_args;
use crate::{Signer, verify};
const KIND_INTENT: &str = "ap2.intent.v1";
const KIND_CART: &str = "ap2.cart.v1";
const KIND_PAYMENT: &str = "ap2.payment.v1";
#[must_use]
pub fn mandate_hash(signed_payload: &[u8]) -> String {
let mut hasher = Sha256::new();
hasher.update(signed_payload);
crate::hex::lower(&hasher.finalize())
}
#[derive(Debug, Clone, Copy)]
pub struct IntentFields<'a> {
pub caller: &'a str,
pub conversation_id: &'a str,
pub scope_description: &'a str,
pub currency: &'a str,
pub max_total_base_units: &'a str,
pub issued_at_unix: u64,
pub expires_at_unix: u64,
pub nonce: &'a str,
}
impl IntentFields<'_> {
fn canonical_json(&self) -> Value {
serde_json::json!({
"kind": KIND_INTENT,
"caller": self.caller,
"conversation_id": self.conversation_id,
"scope_description": self.scope_description,
"currency": self.currency,
"max_total_base_units": self.max_total_base_units,
"issued_at_unix": self.issued_at_unix,
"expires_at_unix": self.expires_at_unix,
"nonce": self.nonce,
})
}
}
#[derive(Debug, Clone)]
pub struct VerifiedIntentMandate {
pub caller: String,
pub conversation_id: String,
pub scope_description: String,
pub currency: String,
pub max_total_base_units: String,
pub issued_at_unix: u64,
pub expires_at_unix: u64,
pub nonce: String,
pub signer_public_key: Vec<u8>,
}
#[must_use]
pub fn sign_intent_mandate(
fields: &IntentFields<'_>,
signer: &Signer,
) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
sign_envelope(fields.canonical_json(), signer)
}
#[must_use]
pub fn verify_signed_intent_mandate(payload: &[u8]) -> Option<VerifiedIntentMandate> {
let v: Value = serde_json::from_slice(payload).ok()?;
if v.get("kind")?.as_str()? != KIND_INTENT {
return None;
}
let caller = v.get("caller")?.as_str()?.to_owned();
let conversation_id = v.get("conversation_id")?.as_str()?.to_owned();
let scope_description = v.get("scope_description")?.as_str()?.to_owned();
let currency = v.get("currency")?.as_str()?.to_owned();
let max_total_base_units = v.get("max_total_base_units")?.as_str()?.to_owned();
let issued_at_unix = v.get("issued_at_unix")?.as_u64()?;
let expires_at_unix = v.get("expires_at_unix")?.as_u64()?;
let nonce = v.get("nonce")?.as_str()?.to_owned();
let (pk, sig) = envelope_signature(&v)?;
let fields = IntentFields {
caller: &caller,
conversation_id: &conversation_id,
scope_description: &scope_description,
currency: ¤cy,
max_total_base_units: &max_total_base_units,
issued_at_unix,
expires_at_unix,
nonce: &nonce,
};
if verify(&pk, &fields.canonical_json().to_string().into_bytes(), &sig) {
Some(VerifiedIntentMandate {
caller,
conversation_id,
scope_description,
currency,
max_total_base_units,
issued_at_unix,
expires_at_unix,
nonce,
signer_public_key: pk,
})
} else {
None
}
}
#[derive(Debug, Clone, Copy)]
pub struct CartFields<'a> {
pub intent_hash: &'a str,
pub caller: &'a str,
pub conversation_id: &'a str,
pub merchant_host: &'a str,
pub currency: &'a str,
pub amount_base_units: &'a str,
pub issued_at_unix: u64,
pub expires_at_unix: u64,
pub nonce: &'a str,
}
impl CartFields<'_> {
fn canonical_json(&self) -> Value {
serde_json::json!({
"kind": KIND_CART,
"intent_hash": self.intent_hash,
"caller": self.caller,
"conversation_id": self.conversation_id,
"merchant_host": self.merchant_host,
"currency": self.currency,
"amount_base_units": self.amount_base_units,
"issued_at_unix": self.issued_at_unix,
"expires_at_unix": self.expires_at_unix,
"nonce": self.nonce,
})
}
}
#[derive(Debug, Clone)]
pub struct VerifiedCartMandate {
pub intent_hash: String,
pub caller: String,
pub conversation_id: String,
pub merchant_host: String,
pub currency: String,
pub amount_base_units: String,
pub issued_at_unix: u64,
pub expires_at_unix: u64,
pub nonce: String,
pub signer_public_key: Vec<u8>,
}
#[must_use]
pub fn sign_cart_mandate(fields: &CartFields<'_>, signer: &Signer) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
sign_envelope(fields.canonical_json(), signer)
}
#[must_use]
pub fn verify_signed_cart_mandate(payload: &[u8]) -> Option<VerifiedCartMandate> {
let v: Value = serde_json::from_slice(payload).ok()?;
if v.get("kind")?.as_str()? != KIND_CART {
return None;
}
let intent_hash = v.get("intent_hash")?.as_str()?.to_owned();
let caller = v.get("caller")?.as_str()?.to_owned();
let conversation_id = v.get("conversation_id")?.as_str()?.to_owned();
let merchant_host = v.get("merchant_host")?.as_str()?.to_owned();
let currency = v.get("currency")?.as_str()?.to_owned();
let amount_base_units = v.get("amount_base_units")?.as_str()?.to_owned();
let issued_at_unix = v.get("issued_at_unix")?.as_u64()?;
let expires_at_unix = v.get("expires_at_unix")?.as_u64()?;
let nonce = v.get("nonce")?.as_str()?.to_owned();
let (pk, sig) = envelope_signature(&v)?;
let fields = CartFields {
intent_hash: &intent_hash,
caller: &caller,
conversation_id: &conversation_id,
merchant_host: &merchant_host,
currency: ¤cy,
amount_base_units: &amount_base_units,
issued_at_unix,
expires_at_unix,
nonce: &nonce,
};
if verify(&pk, &fields.canonical_json().to_string().into_bytes(), &sig) {
Some(VerifiedCartMandate {
intent_hash,
caller,
conversation_id,
merchant_host,
currency,
amount_base_units,
issued_at_unix,
expires_at_unix,
nonce,
signer_public_key: pk,
})
} else {
None
}
}
#[derive(Debug, Clone, Copy)]
pub struct PaymentFields<'a> {
pub cart_hash: &'a str,
pub caller: &'a str,
pub conversation_id: &'a str,
pub args_json: &'a str,
pub currency: &'a str,
pub amount_base_units: &'a str,
pub issued_at_unix: u64,
pub expires_at_unix: u64,
pub nonce: &'a str,
}
impl PaymentFields<'_> {
fn canonical_json(&self) -> Value {
serde_json::json!({
"kind": KIND_PAYMENT,
"cart_hash": self.cart_hash,
"caller": self.caller,
"conversation_id": self.conversation_id,
"args_json": self.args_json,
"currency": self.currency,
"amount_base_units": self.amount_base_units,
"issued_at_unix": self.issued_at_unix,
"expires_at_unix": self.expires_at_unix,
"nonce": self.nonce,
})
}
}
#[derive(Debug, Clone)]
pub struct VerifiedPaymentMandate {
pub cart_hash: String,
pub caller: String,
pub conversation_id: String,
pub args_json: String,
pub currency: String,
pub amount_base_units: String,
pub issued_at_unix: u64,
pub expires_at_unix: u64,
pub nonce: String,
pub signer_public_key: Vec<u8>,
}
#[must_use]
pub fn sign_payment_mandate(
fields: &PaymentFields<'_>,
signer: &Signer,
) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
sign_envelope(fields.canonical_json(), signer)
}
#[must_use]
pub fn verify_signed_payment_mandate(payload: &[u8]) -> Option<VerifiedPaymentMandate> {
let v: Value = serde_json::from_slice(payload).ok()?;
if v.get("kind")?.as_str()? != KIND_PAYMENT {
return None;
}
let cart_hash = v.get("cart_hash")?.as_str()?.to_owned();
let caller = v.get("caller")?.as_str()?.to_owned();
let conversation_id = v.get("conversation_id")?.as_str()?.to_owned();
let args_json = v.get("args_json")?.as_str()?.to_owned();
let currency = v.get("currency")?.as_str()?.to_owned();
let amount_base_units = v.get("amount_base_units")?.as_str()?.to_owned();
let issued_at_unix = v.get("issued_at_unix")?.as_u64()?;
let expires_at_unix = v.get("expires_at_unix")?.as_u64()?;
let nonce = v.get("nonce")?.as_str()?.to_owned();
let (pk, sig) = envelope_signature(&v)?;
let fields = PaymentFields {
cart_hash: &cart_hash,
caller: &caller,
conversation_id: &conversation_id,
args_json: &args_json,
currency: ¤cy,
amount_base_units: &amount_base_units,
issued_at_unix,
expires_at_unix,
nonce: &nonce,
};
if verify(&pk, &fields.canonical_json().to_string().into_bytes(), &sig) {
Some(VerifiedPaymentMandate {
cart_hash,
caller,
conversation_id,
args_json,
currency,
amount_base_units,
issued_at_unix,
expires_at_unix,
nonce,
signer_public_key: pk,
})
} else {
None
}
}
fn sign_envelope(mut canonical: Value, signer: &Signer) -> (Vec<u8>, Vec<u8>, Vec<u8>) {
let canonical_bytes = canonical.to_string().into_bytes();
let signature = signer.sign(&canonical_bytes);
let pk = signer.public_key_bytes();
if let Value::Object(map) = &mut canonical {
map.insert(
"signed_by".to_owned(),
Value::String(crate::hex::lower(&pk)),
);
map.insert(
"signature_hex".to_owned(),
Value::String(crate::hex::lower(&signature)),
);
}
(canonical.to_string().into_bytes(), signature, pk)
}
fn envelope_signature(v: &Value) -> Option<(Vec<u8>, Vec<u8>)> {
let pk = crate::hex::decode(v.get("signed_by")?.as_str()?)?;
let sig = crate::hex::decode(v.get("signature_hex")?.as_str()?)?;
Some((pk, sig))
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum MandateError {
#[error("intent mandate is malformed or its signature does not verify")]
InvalidIntent,
#[error("cart mandate is malformed or its signature does not verify")]
InvalidCart,
#[error("payment mandate is malformed or its signature does not verify")]
InvalidPayment,
#[error("{0} mandate is not signed by a trusted key")]
UntrustedSigner(&'static str),
#[error("{0} mandate is bound to a different conversation")]
ConversationMismatch(&'static str),
#[error("mandate chain is not bound to one consistent caller")]
CallerMismatch,
#[error("{kind} mandate expired at {expires_at_unix}")]
Expired {
kind: &'static str,
expires_at_unix: u64,
},
#[error("cart mandate does not chain to the presented intent mandate")]
CartNotChainedToIntent,
#[error("payment mandate does not chain to the presented cart mandate")]
PaymentNotChainedToCart,
#[error("{0} mandate carries an unparseable base-unit amount")]
MalformedAmount(&'static str),
#[error("cart amount {cart} exceeds the intent ceiling {intent}")]
AmountWidensAtCart {
cart: u128,
intent: u128,
},
#[error("payment amount {payment} exceeds the cart amount {cart}")]
AmountWidensAtPayment {
payment: u128,
cart: u128,
},
#[error("mandate currency does not match across the chain")]
CurrencyMismatch,
#[error("cart mandate carries no merchant scope")]
EmptyScope,
#[error(
"mandate authorizes at most {authorized} base units against {authorized_host}; \
requested {requested} base units against {requested_host}"
)]
ExceedsAuthorization {
authorized: u128,
authorized_host: String,
requested: u128,
requested_host: String,
},
#[error("the payment mandate is bound to a different tool call's args")]
ArgsBindingMismatch,
#[error("the HITL approval does not authorize this exact call; no mandate was minted")]
ApprovalDoesNotAuthorizeCall,
}
#[derive(Debug, Clone, Default)]
pub struct MandateChain {
pub intent: Vec<u8>,
pub cart: Vec<u8>,
pub payment: Vec<u8>,
}
const PERSONA_TRUST_MAX_AGE_SECS: u64 = 300;
#[derive(Debug, Clone, Copy)]
pub struct PersonaSignerTrust<'a> {
pub signing_public_key: &'a [u8],
pub revoked: bool,
pub checked_at_unix: u64,
}
impl PersonaSignerTrust<'_> {
const fn is_trustworthy(&self, now_unix: u64) -> bool {
!self.revoked
&& self.checked_at_unix <= now_unix
&& now_unix.saturating_sub(self.checked_at_unix) <= PERSONA_TRUST_MAX_AGE_SECS
}
}
impl MandateChain {
pub fn verify(
&self,
conversation_id: &str,
issuer_public_key: &[u8],
persona_signer: Option<PersonaSignerTrust<'_>>,
now_unix: u64,
) -> Result<VerifiedMandateChain, MandateError> {
let intent =
verify_signed_intent_mandate(&self.intent).ok_or(MandateError::InvalidIntent)?;
let cart = verify_signed_cart_mandate(&self.cart).ok_or(MandateError::InvalidCart)?;
let payment =
verify_signed_payment_mandate(&self.payment).ok_or(MandateError::InvalidPayment)?;
let links: [(&'static str, &str, u64); 3] = [
("intent", &intent.conversation_id, intent.expires_at_unix),
("cart", &cart.conversation_id, cart.expires_at_unix),
("payment", &payment.conversation_id, payment.expires_at_unix),
];
for (label, conv, expires_at_unix) in links {
if conv != conversation_id {
return Err(MandateError::ConversationMismatch(label));
}
if expires_at_unix <= now_unix {
return Err(MandateError::Expired {
kind: label,
expires_at_unix,
});
}
}
let intent_user_signed = persona_signer.is_some_and(|trust| {
trust.is_trustworthy(now_unix) && intent.signer_public_key == trust.signing_public_key
});
if !intent_user_signed && intent.signer_public_key != issuer_public_key {
return Err(MandateError::UntrustedSigner("intent"));
}
if cart.signer_public_key != issuer_public_key {
return Err(MandateError::UntrustedSigner("cart"));
}
if payment.signer_public_key != issuer_public_key {
return Err(MandateError::UntrustedSigner("payment"));
}
if intent.caller != cart.caller || cart.caller != payment.caller {
return Err(MandateError::CallerMismatch);
}
if cart.intent_hash != mandate_hash(&self.intent) {
return Err(MandateError::CartNotChainedToIntent);
}
if payment.cart_hash != mandate_hash(&self.cart) {
return Err(MandateError::PaymentNotChainedToCart);
}
let cart_amount: u128 = cart
.amount_base_units
.parse()
.map_err(|_| MandateError::MalformedAmount("cart"))?;
let payment_amount: u128 = payment
.amount_base_units
.parse()
.map_err(|_| MandateError::MalformedAmount("payment"))?;
if !intent.max_total_base_units.is_empty() {
let ceiling: u128 = intent
.max_total_base_units
.parse()
.map_err(|_| MandateError::MalformedAmount("intent"))?;
if cart_amount > ceiling {
return Err(MandateError::AmountWidensAtCart {
cart: cart_amount,
intent: ceiling,
});
}
}
if payment_amount > cart_amount {
return Err(MandateError::AmountWidensAtPayment {
payment: payment_amount,
cart: cart_amount,
});
}
if intent.currency != cart.currency || cart.currency != payment.currency {
return Err(MandateError::CurrencyMismatch);
}
if cart.merchant_host.is_empty() {
return Err(MandateError::EmptyScope);
}
Ok(VerifiedMandateChain {
authorized_amount_base_units: payment_amount,
merchant_host: cart.merchant_host.clone(),
currency: payment.currency.clone(),
caller: payment.caller.clone(),
conversation_id: payment.conversation_id.clone(),
args_json: payment.args_json.clone(),
intent,
cart,
payment,
})
}
}
#[derive(Debug, Clone)]
pub struct VerifiedMandateChain {
pub authorized_amount_base_units: u128,
pub merchant_host: String,
pub currency: String,
pub caller: String,
pub conversation_id: String,
pub args_json: String,
pub intent: VerifiedIntentMandate,
pub cart: VerifiedCartMandate,
pub payment: VerifiedPaymentMandate,
}
impl VerifiedMandateChain {
pub fn authorize(
&self,
requested_base_units: u128,
requested_host: &str,
args_json: &str,
) -> Result<(), MandateError> {
if canon_args(args_json) != canon_args(&self.args_json) {
return Err(MandateError::ArgsBindingMismatch);
}
let host_ok = self.merchant_host.eq_ignore_ascii_case(requested_host);
if !host_ok || requested_base_units > self.authorized_amount_base_units {
return Err(MandateError::ExceedsAuthorization {
authorized: self.authorized_amount_base_units,
authorized_host: self.merchant_host.clone(),
requested: requested_base_units,
requested_host: requested_host.to_owned(),
});
}
Ok(())
}
}
pub fn resolve(
chain: Option<&MandateChain>,
issuer_public_key: Option<&[u8]>,
persona_signer: Option<PersonaSignerTrust<'_>>,
conversation_id: &str,
now_unix: u64,
) -> Result<Option<VerifiedMandateChain>, MandateError> {
match (chain, issuer_public_key) {
(Some(c), Some(key)) => c
.verify(conversation_id, key, persona_signer, now_unix)
.map(Some),
_ => Ok(None),
}
}
#[allow(clippy::too_many_arguments)] pub fn payment_mandate_from_approval(
approved: &VerifiedResponse,
request_id: &str,
tool_name: &str,
args_json: &str,
cart_payload: &[u8],
issued_at_unix: u64,
expires_at_unix: u64,
nonce: &str,
signer: &Signer,
) -> Result<Vec<u8>, MandateError> {
if !approved.authorizes_call(request_id, tool_name, args_json) {
return Err(MandateError::ApprovalDoesNotAuthorizeCall);
}
let cart = verify_signed_cart_mandate(cart_payload).ok_or(MandateError::InvalidCart)?;
if cart.caller != approved.caller {
return Err(MandateError::CallerMismatch);
}
if cart.conversation_id != approved.conversation_id {
return Err(MandateError::ConversationMismatch("cart"));
}
let fields = PaymentFields {
cart_hash: &mandate_hash(cart_payload),
caller: &approved.caller,
conversation_id: &approved.conversation_id,
args_json,
currency: &cart.currency,
amount_base_units: &cart.amount_base_units,
issued_at_unix,
expires_at_unix,
nonce,
};
let (payload, _sig, _pk) = sign_payment_mandate(&fields, signer);
Ok(payload)
}
#[cfg(test)]
mod tests {
#![allow(clippy::pedantic, clippy::nursery, missing_docs)]
use super::*;
use crate::approval::{ApprovalSigner, response_payload, verify_signed_response};
fn active_persona_trust(signing_public_key: &[u8], now_unix: u64) -> PersonaSignerTrust<'_> {
PersonaSignerTrust {
signing_public_key,
revoked: false,
checked_at_unix: now_unix,
}
}
fn revoked_persona_trust(signing_public_key: &[u8], now_unix: u64) -> PersonaSignerTrust<'_> {
PersonaSignerTrust {
signing_public_key,
revoked: true,
checked_at_unix: now_unix,
}
}
fn intent(signer: &Signer) -> (Vec<u8>, IntentFields<'static>) {
let fields = IntentFields {
caller: "slack:T1:U9",
conversation_id: "conv-1",
scope_description: "research report purchases",
currency: "0xUSD",
max_total_base_units: "1000000",
issued_at_unix: 1_000,
expires_at_unix: 10_000,
nonce: "intent-nonce-1",
};
let (payload, _sig, _pk) = sign_intent_mandate(&fields, signer);
(payload, fields)
}
#[test]
fn intent_mandate_round_trips() {
let signer = Signer::from_seed(1);
let (payload, fields) = intent(&signer);
let verified = verify_signed_intent_mandate(&payload).expect("verifies");
assert_eq!(verified.caller, fields.caller);
assert_eq!(verified.conversation_id, fields.conversation_id);
assert_eq!(verified.max_total_base_units, fields.max_total_base_units);
assert_eq!(verified.signer_public_key, signer.public_key_bytes());
}
#[test]
fn intent_mandate_tampered_amount_fails() {
let signer = Signer::from_seed(1);
let (payload, _fields) = intent(&signer);
let mut v: Value = serde_json::from_slice(&payload).unwrap();
v["max_total_base_units"] = Value::String("999999999".to_owned());
assert!(verify_signed_intent_mandate(&v.to_string().into_bytes()).is_none());
}
#[test]
fn intent_mandate_wrong_kind_rejected() {
let signer = Signer::from_seed(1);
let cart_fields = CartFields {
intent_hash: "deadbeef",
caller: "slack:T1:U9",
conversation_id: "conv-1",
merchant_host: "api.example.com",
currency: "0xUSD",
amount_base_units: "500000",
issued_at_unix: 1_000,
expires_at_unix: 5_000,
nonce: "cart-nonce-1",
};
let (cart_payload, _sig, _pk) = sign_cart_mandate(&cart_fields, &signer);
assert!(verify_signed_intent_mandate(&cart_payload).is_none());
}
#[test]
fn cart_mandate_round_trips_and_chains_by_hash() {
let signer = Signer::from_seed(2);
let (intent_payload, _fields) = intent(&signer);
let intent_hash = mandate_hash(&intent_payload);
let cart_fields = CartFields {
intent_hash: &intent_hash,
caller: "slack:T1:U9",
conversation_id: "conv-1",
merchant_host: "api.example.com",
currency: "0xUSD",
amount_base_units: "500000",
issued_at_unix: 1_000,
expires_at_unix: 5_000,
nonce: "cart-nonce-1",
};
let (cart_payload, _sig, _pk) = sign_cart_mandate(&cart_fields, &signer);
let verified = verify_signed_cart_mandate(&cart_payload).expect("cart verifies");
assert_eq!(verified.intent_hash, intent_hash);
assert_eq!(verified.merchant_host, "api.example.com");
}
#[test]
fn cart_mandate_tampered_intent_hash_fails() {
let signer = Signer::from_seed(2);
let (intent_payload, _fields) = intent(&signer);
let intent_hash = mandate_hash(&intent_payload);
let cart_fields = CartFields {
intent_hash: &intent_hash,
caller: "slack:T1:U9",
conversation_id: "conv-1",
merchant_host: "api.example.com",
currency: "0xUSD",
amount_base_units: "500000",
issued_at_unix: 1_000,
expires_at_unix: 5_000,
nonce: "cart-nonce-1",
};
let (payload, _sig, _pk) = sign_cart_mandate(&cart_fields, &signer);
let mut v: Value = serde_json::from_slice(&payload).unwrap();
v["intent_hash"] = Value::String("0".repeat(64));
assert!(verify_signed_cart_mandate(&v.to_string().into_bytes()).is_none());
}
#[test]
fn payment_mandate_round_trips_and_chains_by_hash() {
let signer = Signer::from_seed(3);
let (intent_payload, _fields) = intent(&signer);
let intent_hash = mandate_hash(&intent_payload);
let cart_fields = CartFields {
intent_hash: &intent_hash,
caller: "slack:T1:U9",
conversation_id: "conv-1",
merchant_host: "api.example.com",
currency: "0xUSD",
amount_base_units: "500000",
issued_at_unix: 1_000,
expires_at_unix: 5_000,
nonce: "cart-nonce-1",
};
let (cart_payload, _sig, _pk) = sign_cart_mandate(&cart_fields, &signer);
let cart_hash = mandate_hash(&cart_payload);
let payment_fields = PaymentFields {
cart_hash: &cart_hash,
caller: "slack:T1:U9",
conversation_id: "conv-1",
args_json: r#"{"url":"https://api.example.com/report"}"#,
currency: "0xUSD",
amount_base_units: "250000",
issued_at_unix: 1_000,
expires_at_unix: 4_000,
nonce: "payment-nonce-1",
};
let (payment_payload, _sig, _pk) = sign_payment_mandate(&payment_fields, &signer);
let verified = verify_signed_payment_mandate(&payment_payload).expect("payment verifies");
assert_eq!(verified.cart_hash, cart_hash);
assert_eq!(verified.amount_base_units, "250000");
assert_eq!(verified.args_json, payment_fields.args_json);
}
#[test]
fn payment_mandate_tampered_args_json_fails() {
let signer = Signer::from_seed(3);
let payment_fields = PaymentFields {
cart_hash: "deadbeef",
caller: "slack:T1:U9",
conversation_id: "conv-1",
args_json: r#"{"url":"https://api.example.com/report"}"#,
currency: "0xUSD",
amount_base_units: "250000",
issued_at_unix: 1_000,
expires_at_unix: 4_000,
nonce: "payment-nonce-1",
};
let (payload, _sig, _pk) = sign_payment_mandate(&payment_fields, &signer);
let mut v: Value = serde_json::from_slice(&payload).unwrap();
v["args_json"] = Value::String(r#"{"url":"https://evil.example.com/steal"}"#.to_owned());
assert!(verify_signed_payment_mandate(&v.to_string().into_bytes()).is_none());
}
#[test]
fn mandate_hash_is_stable_and_sensitive_to_signature() {
let signer_a = Signer::from_seed(9);
let signer_b = Signer::from_seed(10);
let fields = IntentFields {
caller: "slack:T1:U9",
conversation_id: "conv-1",
scope_description: "x",
currency: "0xUSD",
max_total_base_units: "1000",
issued_at_unix: 1,
expires_at_unix: 2,
nonce: "n",
};
let (payload_a, _s, _p) = sign_intent_mandate(&fields, &signer_a);
let (payload_b, _s, _p) = sign_intent_mandate(&fields, &signer_b);
assert_eq!(mandate_hash(&payload_a), mandate_hash(&payload_a));
assert_ne!(mandate_hash(&payload_a), mandate_hash(&payload_b));
}
#[test]
fn garbage_payload_returns_none_not_panic() {
assert!(verify_signed_intent_mandate(b"not json").is_none());
assert!(verify_signed_cart_mandate(b"{}").is_none());
assert!(verify_signed_payment_mandate(b"[]").is_none());
}
const CONV: &str = "conv-1";
const CALLER: &str = "slack:T1:U9";
const HOST: &str = "api.example.com";
const USD: &str = "0x20c0000000000000000000000000000000000000";
const ARGS: &str = r#"{"url":"https://api.example.com/report"}"#;
fn issuer() -> Signer {
Signer::from_seed(99)
}
fn intent_fields(max_total: &str) -> IntentFields<'_> {
IntentFields {
caller: CALLER,
conversation_id: CONV,
scope_description: "research report purchases",
currency: USD,
max_total_base_units: max_total,
issued_at_unix: 100,
expires_at_unix: 10_000,
nonce: "n-intent",
}
}
fn valid_chain(signer: &Signer) -> MandateChain {
let (intent, _s, _p) = sign_intent_mandate(&intent_fields("1000000"), signer);
let intent_hash = mandate_hash(&intent);
let (cart, _s, _p) = sign_cart_mandate(
&CartFields {
intent_hash: &intent_hash,
caller: CALLER,
conversation_id: CONV,
merchant_host: HOST,
currency: USD,
amount_base_units: "500000",
issued_at_unix: 100,
expires_at_unix: 9_000,
nonce: "n-cart",
},
signer,
);
let cart_hash = mandate_hash(&cart);
let (payment, _s, _p) = sign_payment_mandate(
&PaymentFields {
cart_hash: &cart_hash,
caller: CALLER,
conversation_id: CONV,
args_json: ARGS,
currency: USD,
amount_base_units: "500000",
issued_at_unix: 100,
expires_at_unix: 8_000,
nonce: "n-payment",
},
signer,
);
MandateChain {
intent,
cart,
payment,
}
}
#[test]
fn valid_chain_verifies_and_authorizes_within_bounds() {
let signer = issuer();
let chain = valid_chain(&signer);
let verified = chain
.verify(CONV, &signer.public_key_bytes(), None, 1_000)
.expect("a mutually consistent, unexpired chain must verify");
assert_eq!(verified.authorized_amount_base_units, 500_000);
assert_eq!(verified.merchant_host, HOST);
assert_eq!(verified.caller, CALLER);
verified.authorize(500_000, HOST, ARGS).expect("at-cap ok");
verified.authorize(1, HOST, ARGS).expect("under-cap ok");
verified
.authorize(500_000, "API.Example.COM", ARGS)
.expect("case-insensitive host");
assert!(matches!(
verified.authorize(500_001, HOST, ARGS),
Err(MandateError::ExceedsAuthorization { .. })
));
assert!(matches!(
verified.authorize(1, "evil.example.com", ARGS),
Err(MandateError::ExceedsAuthorization { .. })
));
assert!(matches!(
verified.authorize(1, HOST, r#"{"url":"https://api.example.com/OTHER"}"#),
Err(MandateError::ArgsBindingMismatch)
));
}
#[test]
fn args_binding_matches_by_value_not_key_order() {
let signer = issuer();
let (intent, _s, _p) = sign_intent_mandate(&intent_fields(""), &signer);
let intent_hash = mandate_hash(&intent);
let (cart, _s, _p) = sign_cart_mandate(
&CartFields {
intent_hash: &intent_hash,
caller: CALLER,
conversation_id: CONV,
merchant_host: HOST,
currency: USD,
amount_base_units: "500000",
issued_at_unix: 100,
expires_at_unix: 9_000,
nonce: "n-cart",
},
&signer,
);
let (payment, _s, _p) = sign_payment_mandate(
&PaymentFields {
cart_hash: &mandate_hash(&cart),
caller: CALLER,
conversation_id: CONV,
args_json: r#"{"max_spend":"0.10","url":"https://api.example.com/r"}"#,
currency: USD,
amount_base_units: "500000",
issued_at_unix: 100,
expires_at_unix: 8_000,
nonce: "n-payment",
},
&signer,
);
let verified = MandateChain {
intent,
cart,
payment,
}
.verify(CONV, &signer.public_key_bytes(), None, 1_000)
.expect("chain verifies");
verified
.authorize(
1,
HOST,
r#"{"url":"https://api.example.com/r","max_spend":"0.10"}"#,
)
.expect("reordered-but-equal args must pass the binding");
}
#[test]
fn tampered_link_fails_signature_verification() {
let signer = issuer();
let mut chain = valid_chain(&signer);
let mut v: serde_json::Value = serde_json::from_slice(&chain.cart).unwrap();
v["amount_base_units"] = serde_json::Value::String("999999999".to_owned());
chain.cart = v.to_string().into_bytes();
assert_eq!(
chain
.verify(CONV, &signer.public_key_bytes(), None, 1_000)
.unwrap_err(),
MandateError::InvalidCart
);
}
#[test]
fn validly_signed_but_unchained_links_are_rejected() {
let signer = issuer();
let chain = valid_chain(&signer);
let (other_intent, _s, _p) = sign_intent_mandate(&intent_fields("2000000"), &signer);
let other_hash = mandate_hash(&other_intent);
let (unchained_cart, _s, _p) = sign_cart_mandate(
&CartFields {
intent_hash: &other_hash, caller: CALLER,
conversation_id: CONV,
merchant_host: HOST,
currency: USD,
amount_base_units: "500000",
issued_at_unix: 100,
expires_at_unix: 9_000,
nonce: "n-cart",
},
&signer,
);
let broken = MandateChain {
intent: chain.intent.clone(),
cart: unchained_cart.clone(),
payment: chain.payment.clone(),
};
assert_eq!(
broken
.verify(CONV, &signer.public_key_bytes(), None, 1_000)
.unwrap_err(),
MandateError::CartNotChainedToIntent
);
let (payment_for_other, _s, _p) = sign_payment_mandate(
&PaymentFields {
cart_hash: &mandate_hash(&unchained_cart),
caller: CALLER,
conversation_id: CONV,
args_json: ARGS,
currency: USD,
amount_base_units: "500000",
issued_at_unix: 100,
expires_at_unix: 8_000,
nonce: "n-payment",
},
&signer,
);
let broken = MandateChain {
intent: chain.intent,
cart: chain.cart,
payment: payment_for_other,
};
assert_eq!(
broken
.verify(CONV, &signer.public_key_bytes(), None, 1_000)
.unwrap_err(),
MandateError::PaymentNotChainedToCart
);
}
#[test]
fn widened_amounts_are_rejected() {
let signer = issuer();
let (intent, _s, _p) = sign_intent_mandate(&intent_fields("100"), &signer);
let intent_hash = mandate_hash(&intent);
let (cart, _s, _p) = sign_cart_mandate(
&CartFields {
intent_hash: &intent_hash,
caller: CALLER,
conversation_id: CONV,
merchant_host: HOST,
currency: USD,
amount_base_units: "999", issued_at_unix: 100,
expires_at_unix: 9_000,
nonce: "n-cart",
},
&signer,
);
let (payment, _s, _p) = sign_payment_mandate(
&PaymentFields {
cart_hash: &mandate_hash(&cart),
caller: CALLER,
conversation_id: CONV,
args_json: ARGS,
currency: USD,
amount_base_units: "999",
issued_at_unix: 100,
expires_at_unix: 8_000,
nonce: "n-payment",
},
&signer,
);
let chain = MandateChain {
intent,
cart,
payment,
};
assert_eq!(
chain
.verify(CONV, &signer.public_key_bytes(), None, 1_000)
.unwrap_err(),
MandateError::AmountWidensAtCart {
cart: 999,
intent: 100
}
);
let base = valid_chain(&signer);
let (over_payment, _s, _p) = sign_payment_mandate(
&PaymentFields {
cart_hash: &mandate_hash(&base.cart),
caller: CALLER,
conversation_id: CONV,
args_json: ARGS,
currency: USD,
amount_base_units: "500001", issued_at_unix: 100,
expires_at_unix: 8_000,
nonce: "n-payment",
},
&signer,
);
let chain = MandateChain {
intent: base.intent,
cart: base.cart,
payment: over_payment,
};
assert_eq!(
chain
.verify(CONV, &signer.public_key_bytes(), None, 1_000)
.unwrap_err(),
MandateError::AmountWidensAtPayment {
payment: 500_001,
cart: 500_000
}
);
}
#[test]
fn unbounded_intent_ceiling_admits_any_cart_amount() {
let signer = issuer();
let (intent, _s, _p) = sign_intent_mandate(&intent_fields(""), &signer);
let intent_hash = mandate_hash(&intent);
let (cart, _s, _p) = sign_cart_mandate(
&CartFields {
intent_hash: &intent_hash,
caller: CALLER,
conversation_id: CONV,
merchant_host: HOST,
currency: USD,
amount_base_units: "123456789",
issued_at_unix: 100,
expires_at_unix: 9_000,
nonce: "n-cart",
},
&signer,
);
let (payment, _s, _p) = sign_payment_mandate(
&PaymentFields {
cart_hash: &mandate_hash(&cart),
caller: CALLER,
conversation_id: CONV,
args_json: ARGS,
currency: USD,
amount_base_units: "123456789",
issued_at_unix: 100,
expires_at_unix: 8_000,
nonce: "n-payment",
},
&signer,
);
let chain = MandateChain {
intent,
cart,
payment,
};
let verified = chain
.verify(CONV, &signer.public_key_bytes(), None, 1_000)
.expect("empty intent ceiling is unbounded");
assert_eq!(verified.authorized_amount_base_units, 123_456_789);
}
#[test]
fn expired_link_is_rejected() {
let signer = issuer();
let chain = valid_chain(&signer);
assert_eq!(
chain
.verify(CONV, &signer.public_key_bytes(), None, 8_000)
.unwrap_err(),
MandateError::Expired {
kind: "payment",
expires_at_unix: 8_000
}
);
assert_eq!(
chain
.verify(CONV, &signer.public_key_bytes(), None, 50_000)
.unwrap_err(),
MandateError::Expired {
kind: "intent",
expires_at_unix: 10_000
}
);
assert!(
chain
.verify(CONV, &signer.public_key_bytes(), None, 1)
.is_ok()
);
}
#[test]
fn untrusted_issuer_is_rejected() {
let signer = issuer();
let chain = valid_chain(&signer);
let wrong_key = Signer::from_seed(1).public_key_bytes();
assert_eq!(
chain.verify(CONV, &wrong_key, None, 1_000).unwrap_err(),
MandateError::UntrustedSigner("intent")
);
}
fn chain_with_user_signed_intent(
issuer_signer: &Signer,
persona_signer: &Signer,
) -> MandateChain {
let (intent, _s, _p) = sign_intent_mandate(&intent_fields("1000000"), persona_signer);
let intent_hash = mandate_hash(&intent);
let (cart, _s, _p) = sign_cart_mandate(
&CartFields {
intent_hash: &intent_hash,
caller: CALLER,
conversation_id: CONV,
merchant_host: HOST,
currency: USD,
amount_base_units: "500000",
issued_at_unix: 100,
expires_at_unix: 9_000,
nonce: "n-cart",
},
issuer_signer,
);
let cart_hash = mandate_hash(&cart);
let (payment, _s, _p) = sign_payment_mandate(
&PaymentFields {
cart_hash: &cart_hash,
caller: CALLER,
conversation_id: CONV,
args_json: ARGS,
currency: USD,
amount_base_units: "500000",
issued_at_unix: 100,
expires_at_unix: 8_000,
nonce: "n-payment",
},
issuer_signer,
);
MandateChain {
intent,
cart,
payment,
}
}
#[test]
fn user_signed_intent_with_platform_cart_and_payment_verifies() {
let issuer_signer = issuer();
let persona_signer = Signer::from_seed(555);
let persona_key = persona_signer.public_key_bytes();
let chain = chain_with_user_signed_intent(&issuer_signer, &persona_signer);
let verified = chain
.verify(
CONV,
&issuer_signer.public_key_bytes(),
Some(active_persona_trust(&persona_key, 1_000)),
1_000,
)
.expect("a user-signed intent with platform cart/payment must verify");
assert_eq!(verified.intent.signer_public_key, persona_key);
assert_eq!(verified.authorized_amount_base_units, 500_000);
}
#[test]
fn platform_signed_intent_still_verifies_when_persona_has_an_active_credential() {
let issuer_signer = issuer();
let persona_signer = Signer::from_seed(555);
let persona_key = persona_signer.public_key_bytes();
let chain = valid_chain(&issuer_signer); assert!(
chain
.verify(
CONV,
&issuer_signer.public_key_bytes(),
Some(active_persona_trust(&persona_key, 1_000)),
1_000,
)
.is_ok()
);
}
#[test]
fn intent_signed_by_a_non_recorded_key_is_refused() {
let issuer_signer = issuer();
let persona_signer = Signer::from_seed(555);
let persona_key = persona_signer.public_key_bytes();
let stranger = Signer::from_seed(556);
let chain = chain_with_user_signed_intent(&issuer_signer, &stranger);
assert_eq!(
chain
.verify(
CONV,
&issuer_signer.public_key_bytes(),
Some(active_persona_trust(&persona_key, 1_000)),
1_000,
)
.unwrap_err(),
MandateError::UntrustedSigner("intent")
);
}
#[test]
fn user_signed_intent_is_refused_when_no_persona_key_is_resolved() {
let issuer_signer = issuer();
let persona_signer = Signer::from_seed(555);
let chain = chain_with_user_signed_intent(&issuer_signer, &persona_signer);
assert_eq!(
chain
.verify(CONV, &issuer_signer.public_key_bytes(), None, 1_000)
.unwrap_err(),
MandateError::UntrustedSigner("intent")
);
}
#[test]
fn user_signed_intent_is_refused_when_the_credential_is_revoked() {
let issuer_signer = issuer();
let persona_signer = Signer::from_seed(555);
let persona_key = persona_signer.public_key_bytes();
let chain = chain_with_user_signed_intent(&issuer_signer, &persona_signer);
assert_eq!(
chain
.verify(
CONV,
&issuer_signer.public_key_bytes(),
Some(revoked_persona_trust(&persona_key, 1_000)),
1_000,
)
.unwrap_err(),
MandateError::UntrustedSigner("intent")
);
}
#[test]
fn user_signed_intent_is_refused_when_the_revocation_check_is_stale() {
let issuer_signer = issuer();
let persona_signer = Signer::from_seed(555);
let persona_key = persona_signer.public_key_bytes();
let chain = chain_with_user_signed_intent(&issuer_signer, &persona_signer);
let now_unix = 5_000;
let checked_at_unix = now_unix - PERSONA_TRUST_MAX_AGE_SECS - 1;
assert_eq!(
chain
.verify(
CONV,
&issuer_signer.public_key_bytes(),
Some(PersonaSignerTrust {
signing_public_key: &persona_key,
revoked: false,
checked_at_unix,
}),
now_unix,
)
.unwrap_err(),
MandateError::UntrustedSigner("intent")
);
}
#[test]
fn user_signed_intent_is_refused_when_the_revocation_check_is_dated_in_the_future() {
let issuer_signer = issuer();
let persona_signer = Signer::from_seed(555);
let persona_key = persona_signer.public_key_bytes();
let chain = chain_with_user_signed_intent(&issuer_signer, &persona_signer);
assert_eq!(
chain
.verify(
CONV,
&issuer_signer.public_key_bytes(),
Some(PersonaSignerTrust {
signing_public_key: &persona_key,
revoked: false,
checked_at_unix: 1_001,
}),
1_000,
)
.unwrap_err(),
MandateError::UntrustedSigner("intent")
);
}
#[test]
fn cart_and_payment_never_trust_the_persona_key_even_when_intent_does() {
let issuer_signer = issuer();
let persona_signer = Signer::from_seed(555);
let persona_key = persona_signer.public_key_bytes();
let issuer_key = issuer_signer.public_key_bytes();
let (intent, _s, _p) = sign_intent_mandate(&intent_fields("1000000"), &persona_signer);
let intent_hash = mandate_hash(&intent);
let (bad_cart, _s, _p) = sign_cart_mandate(
&CartFields {
intent_hash: &intent_hash,
caller: CALLER,
conversation_id: CONV,
merchant_host: HOST,
currency: USD,
amount_base_units: "500000",
issued_at_unix: 100,
expires_at_unix: 9_000,
nonce: "n-cart",
},
&persona_signer,
);
let (payment_for_bad_cart, _s, _p) = sign_payment_mandate(
&PaymentFields {
cart_hash: &mandate_hash(&bad_cart),
caller: CALLER,
conversation_id: CONV,
args_json: ARGS,
currency: USD,
amount_base_units: "500000",
issued_at_unix: 100,
expires_at_unix: 8_000,
nonce: "n-payment",
},
&issuer_signer,
);
let chain = MandateChain {
intent: intent.clone(),
cart: bad_cart,
payment: payment_for_bad_cart,
};
assert_eq!(
chain
.verify(
CONV,
&issuer_key,
Some(active_persona_trust(&persona_key, 1_000)),
1_000,
)
.unwrap_err(),
MandateError::UntrustedSigner("cart")
);
let (good_cart, _s, _p) = sign_cart_mandate(
&CartFields {
intent_hash: &intent_hash,
caller: CALLER,
conversation_id: CONV,
merchant_host: HOST,
currency: USD,
amount_base_units: "500000",
issued_at_unix: 100,
expires_at_unix: 9_000,
nonce: "n-cart",
},
&issuer_signer,
);
let (bad_payment, _s, _p) = sign_payment_mandate(
&PaymentFields {
cart_hash: &mandate_hash(&good_cart),
caller: CALLER,
conversation_id: CONV,
args_json: ARGS,
currency: USD,
amount_base_units: "500000",
issued_at_unix: 100,
expires_at_unix: 8_000,
nonce: "n-payment",
},
&persona_signer,
);
let chain = MandateChain {
intent,
cart: good_cart,
payment: bad_payment,
};
assert_eq!(
chain
.verify(
CONV,
&issuer_key,
Some(active_persona_trust(&persona_key, 1_000)),
1_000,
)
.unwrap_err(),
MandateError::UntrustedSigner("payment")
);
}
#[test]
fn splicing_a_user_signed_intent_under_a_different_chain_still_breaks_the_hash_link() {
let issuer_signer = issuer();
let persona_signer = Signer::from_seed(555);
let persona_key = persona_signer.public_key_bytes();
let mut foreign_fields = intent_fields("1000000");
foreign_fields.nonce = "n-intent-foreign";
let (foreign_intent, _s, _p) = sign_intent_mandate(&foreign_fields, &persona_signer);
let chain = chain_with_user_signed_intent(&issuer_signer, &persona_signer);
let spliced = MandateChain {
intent: foreign_intent,
cart: chain.cart,
payment: chain.payment,
};
assert_eq!(
spliced
.verify(
CONV,
&issuer_signer.public_key_bytes(),
Some(active_persona_trust(&persona_key, 1_000)),
1_000,
)
.unwrap_err(),
MandateError::CartNotChainedToIntent
);
}
#[test]
fn conversation_and_caller_bindings_are_enforced() {
let signer = issuer();
let chain = valid_chain(&signer);
assert_eq!(
chain
.verify("conv-OTHER", &signer.public_key_bytes(), None, 1_000)
.unwrap_err(),
MandateError::ConversationMismatch("intent")
);
let (payment, _s, _p) = sign_payment_mandate(
&PaymentFields {
cart_hash: &mandate_hash(&chain.cart),
caller: "slack:T1:UATTACKER",
conversation_id: CONV,
args_json: ARGS,
currency: USD,
amount_base_units: "500000",
issued_at_unix: 100,
expires_at_unix: 8_000,
nonce: "n-payment",
},
&signer,
);
let cross_caller = MandateChain {
intent: chain.intent,
cart: chain.cart,
payment,
};
assert_eq!(
cross_caller
.verify(CONV, &signer.public_key_bytes(), None, 1_000)
.unwrap_err(),
MandateError::CallerMismatch
);
}
#[test]
fn currency_mismatch_is_rejected() {
let signer = issuer();
let chain = valid_chain(&signer);
let (payment, _s, _p) = sign_payment_mandate(
&PaymentFields {
cart_hash: &mandate_hash(&chain.cart),
caller: CALLER,
conversation_id: CONV,
args_json: ARGS,
currency: "0xOTHER",
amount_base_units: "500000",
issued_at_unix: 100,
expires_at_unix: 8_000,
nonce: "n-payment",
},
&signer,
);
let cross_currency = MandateChain {
intent: chain.intent,
cart: chain.cart,
payment,
};
assert_eq!(
cross_currency
.verify(CONV, &signer.public_key_bytes(), None, 1_000)
.unwrap_err(),
MandateError::CurrencyMismatch
);
}
#[test]
fn resolve_is_a_noop_when_unconfigured_or_absent() {
let signer = issuer();
let chain = valid_chain(&signer);
let key = signer.public_key_bytes();
assert!(matches!(
resolve(None, Some(&key), None, CONV, 1_000),
Ok(None)
));
assert!(matches!(resolve(None, None, None, CONV, 1_000), Ok(None)));
assert!(matches!(
resolve(Some(&chain), None, None, CONV, 1_000),
Ok(None)
));
assert!(matches!(
resolve(Some(&chain), Some(&key), None, CONV, 1_000),
Ok(Some(_))
));
}
#[test]
fn resolve_fails_closed_on_an_invalid_presented_chain() {
let signer = issuer();
let chain = valid_chain(&signer);
let key = signer.public_key_bytes();
assert!(matches!(
resolve(Some(&chain), Some(&key), None, CONV, 50_000).unwrap_err(),
MandateError::Expired { .. }
));
}
#[test]
fn resolve_refuses_a_revoked_persona_signer_at_the_same_seam_the_proxy_calls() {
let issuer_signer = issuer();
let persona_signer = Signer::from_seed(555);
let persona_key = persona_signer.public_key_bytes();
let chain = chain_with_user_signed_intent(&issuer_signer, &persona_signer);
let issuer_key = issuer_signer.public_key_bytes();
assert_eq!(
resolve(
Some(&chain),
Some(&issuer_key),
Some(revoked_persona_trust(&persona_key, 1_000)),
CONV,
1_000,
)
.unwrap_err(),
MandateError::UntrustedSigner("intent")
);
}
fn hitl_approval(approved: bool, args_json: &str) -> VerifiedResponse {
let approval_signer = ApprovalSigner::from_seed(7);
let (payload, _sig, _pk) = response_payload(
"req-1",
"paid_fetch",
args_json,
"",
approved,
false,
&[],
CALLER,
"",
"workspace-write",
if approved { "looks fine" } else { "no" },
"",
CONV,
"approval-nonce-1",
&approval_signer,
);
verify_signed_response(&payload).expect("approval signature verifies")
}
fn chain_prefix(signer: &Signer) -> (Vec<u8>, Vec<u8>, String) {
let (intent, _s, _p) = sign_intent_mandate(&intent_fields("1000000"), signer);
let intent_hash = mandate_hash(&intent);
let (cart, _s, _p) = sign_cart_mandate(
&CartFields {
intent_hash: &intent_hash,
caller: CALLER,
conversation_id: CONV,
merchant_host: HOST,
currency: USD,
amount_base_units: "500000",
issued_at_unix: 100,
expires_at_unix: 9_000,
nonce: "n-cart",
},
signer,
);
(intent, cart, intent_hash)
}
#[test]
fn hitl_approval_mints_a_payment_mandate_that_completes_the_chain() {
let signer = issuer();
let (intent, cart, _ih) = chain_prefix(&signer);
let approved = hitl_approval(true, ARGS);
let payment = payment_mandate_from_approval(
&approved,
"req-1",
"paid_fetch",
ARGS,
&cart,
200,
8_000,
"n-payment",
&signer,
)
.expect("the approval authorizes this exact call");
let chain = MandateChain {
intent,
cart,
payment,
};
let verified = chain
.verify(CONV, &signer.public_key_bytes(), None, 1_000)
.expect("the minted payment chains to the cart");
assert_eq!(verified.authorized_amount_base_units, 500_000);
assert_eq!(verified.caller, CALLER);
verified
.authorize(500_000, HOST, ARGS)
.expect("authorizes the approved call");
assert!(matches!(
verified.authorize(1, HOST, r#"{"url":"https://evil.example.com/x"}"#),
Err(MandateError::ArgsBindingMismatch)
));
}
#[test]
fn hitl_bridge_refuses_denials_and_mismatched_scopes() {
let signer = issuer();
let (_intent, cart, intent_hash) = chain_prefix(&signer);
let denied = hitl_approval(false, ARGS);
assert_eq!(
payment_mandate_from_approval(
&denied,
"req-1",
"paid_fetch",
ARGS,
&cart,
200,
8_000,
"n",
&signer
)
.unwrap_err(),
MandateError::ApprovalDoesNotAuthorizeCall
);
let approved = hitl_approval(true, ARGS);
assert_eq!(
payment_mandate_from_approval(
&approved,
"req-1",
"paid_fetch",
r#"{"url":"https://evil.example.com/x"}"#,
&cart,
200,
8_000,
"n",
&signer
)
.unwrap_err(),
MandateError::ApprovalDoesNotAuthorizeCall
);
let (foreign_cart, _s, _p) = sign_cart_mandate(
&CartFields {
intent_hash: &intent_hash,
caller: CALLER,
conversation_id: "conv-OTHER",
merchant_host: HOST,
currency: USD,
amount_base_units: "500000",
issued_at_unix: 100,
expires_at_unix: 9_000,
nonce: "n-cart",
},
&signer,
);
assert_eq!(
payment_mandate_from_approval(
&approved,
"req-1",
"paid_fetch",
ARGS,
&foreign_cart,
200,
8_000,
"n",
&signer
)
.unwrap_err(),
MandateError::ConversationMismatch("cart")
);
let (foreign_caller_cart, _s, _p) = sign_cart_mandate(
&CartFields {
intent_hash: &intent_hash,
caller: "slack:T1:USOMEONE",
conversation_id: CONV,
merchant_host: HOST,
currency: USD,
amount_base_units: "500000",
issued_at_unix: 100,
expires_at_unix: 9_000,
nonce: "n-cart",
},
&signer,
);
assert_eq!(
payment_mandate_from_approval(
&approved,
"req-1",
"paid_fetch",
ARGS,
&foreign_caller_cart,
200,
8_000,
"n",
&signer
)
.unwrap_err(),
MandateError::CallerMismatch
);
}
}