use crate::error::{Error, Result};
use crate::warrant::Warrant;
use base64::Engine;
use serde::{Deserialize, Serialize};
pub const MAX_WARRANT_SIZE: usize = 64 * 1024;
pub const MAX_TOOLS_PER_WARRANT: usize = 256;
pub const MAX_CONSTRAINTS_PER_TOOL: usize = 64;
pub const MAX_EXTENSION_KEYS: usize = 64;
pub const MAX_EXTENSION_VALUE_SIZE: usize = 8 * 1024;
pub const MAX_EXTENSION_KEY_SIZE: usize = 255;
pub const KEY_DISPLAY_TRUNCATION: usize = 32;
pub fn encode(warrant: &Warrant) -> Result<Vec<u8>> {
let mut buf = Vec::new();
ciborium::ser::into_writer(warrant, &mut buf)?;
Ok(buf)
}
pub fn to_vec<T: Serialize>(value: &T) -> Result<Vec<u8>> {
let mut buf = Vec::new();
ciborium::ser::into_writer(value, &mut buf)?;
Ok(buf)
}
pub fn decode(data: &[u8]) -> Result<Warrant> {
if data.len() > MAX_WARRANT_SIZE {
return Err(Error::PayloadTooLarge {
size: data.len(),
max: MAX_WARRANT_SIZE,
});
}
let warrant: Warrant = ciborium::de::from_reader(data)?;
warrant.validate_constraint_depth()?;
warrant.validate()?;
if warrant.payload.version != crate::warrant::WARRANT_VERSION as u8 {
return Err(Error::UnsupportedVersion(warrant.payload.version));
}
Ok(warrant)
}
pub fn encode_base64(warrant: &Warrant) -> Result<String> {
let bytes = encode(warrant)?;
Ok(base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes))
}
pub fn encode_pem(warrant: &Warrant) -> Result<String> {
let b64 = encode_base64(warrant)?;
let mut pem = String::new();
pem.push_str("-----BEGIN TENUO WARRANT-----\n");
for chunk in b64.as_bytes().chunks(64) {
pem.push_str(
std::str::from_utf8(chunk).map_err(|e| Error::SerializationError(e.to_string()))?,
);
pem.push('\n');
}
pem.push_str("-----END TENUO WARRANT-----\n");
Ok(pem)
}
pub const WARRANT_HEADER: &str = "X-Tenuo-Warrant";
use std::borrow::Cow;
pub const WARRANT_ID_HEADER: &str = "X-Tenuo-Warrant-Id";
pub fn normalize_token(token: &str) -> Cow<'_, str> {
let s = token.trim();
if s.starts_with("-----BEGIN TENUO WARRANT-----") {
Cow::Owned(
s.lines()
.filter(|line| !line.trim().starts_with("-----"))
.collect::<String>()
.replace(|c: char| c.is_whitespace(), ""),
)
} else if s.chars().any(|c| c.is_whitespace()) {
Cow::Owned(s.replace(|c: char| c.is_whitespace(), ""))
} else {
Cow::Borrowed(s)
}
}
pub fn decode_base64(s: &str) -> Result<Warrant> {
let clean = normalize_token(s);
let estimated_size = (clean.len() * 3) / 4;
if estimated_size > MAX_WARRANT_SIZE {
return Err(Error::PayloadTooLarge {
size: estimated_size,
max: MAX_WARRANT_SIZE,
});
}
let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(clean.as_ref())
.map_err(|e| Error::DeserializationError(e.to_string()))?;
decode(&bytes)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(transparent)]
pub struct WarrantStack(pub Vec<Warrant>);
impl WarrantStack {
pub fn new(warrants: Vec<Warrant>) -> Self {
Self(warrants)
}
pub fn is_valid(&self) -> bool {
!self.0.is_empty()
}
pub fn leaf(&self) -> Option<&Warrant> {
self.0.last()
}
pub fn root(&self) -> Option<&Warrant> {
self.0.first()
}
}
pub fn encode_stack(stack: &WarrantStack) -> Result<Vec<u8>> {
to_vec(stack)
}
pub const MAX_STACK_SIZE: usize = 256 * 1024;
pub fn decode_stack(data: &[u8]) -> Result<WarrantStack> {
if data.len() > MAX_STACK_SIZE {
return Err(Error::PayloadTooLarge {
size: data.len(),
max: MAX_STACK_SIZE,
});
}
let stack: WarrantStack = ciborium::de::from_reader(data)?;
if stack.0.is_empty() {
return Err(Error::DeserializationError(
"Warrant stack cannot be empty".to_string(),
));
}
Ok(stack)
}
pub fn encode_pem_stack(stack: &WarrantStack) -> Result<String> {
let bytes = encode_stack(stack)?;
let b64 = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes);
let mut pem = String::new();
pem.push_str("-----BEGIN TENUO WARRANT CHAIN-----\n");
for chunk in b64.as_bytes().chunks(64) {
pem.push_str(
std::str::from_utf8(chunk).map_err(|e| Error::SerializationError(e.to_string()))?,
);
pem.push('\n');
}
pem.push_str("-----END TENUO WARRANT CHAIN-----\n");
Ok(pem)
}
pub fn decode_pem_chain(input: &str) -> Result<WarrantStack> {
if let Some(start) = input.find("-----BEGIN TENUO WARRANT CHAIN-----") {
let start_processed = start + "-----BEGIN TENUO WARRANT CHAIN-----".len();
if let Some(end) = input[start_processed..].find("-----END TENUO WARRANT CHAIN-----") {
let content = &input[start_processed..start_processed + end];
let clean = content.replace(|c: char| c.is_whitespace(), "");
let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(clean)
.map_err(|e| Error::DeserializationError(e.to_string()))?;
return decode_stack(&bytes);
}
}
let mut warrants = Vec::new();
let mut current_pos = 0;
let mut found_pem = false;
while let Some(start) = input[current_pos..].find("-----BEGIN TENUO WARRANT-----") {
found_pem = true;
let abs_start = current_pos + start;
if let Some(end) = input[abs_start..].find("-----END TENUO WARRANT-----") {
let abs_end = abs_start + end + "-----END TENUO WARRANT-----".len();
let block = &input[abs_start..abs_end];
warrants.push(decode_base64(block)?);
current_pos = abs_end;
} else {
break;
}
}
if !found_pem {
if !input.trim().is_empty() {
warrants.push(decode_base64(input)?);
}
} else if warrants.is_empty() {
return Err(Error::DeserializationError(
"Found PEM headers but failed to extract valid warrants".to_string(),
));
}
if warrants.is_empty() {
return Err(Error::DeserializationError(
"No valid warrants found".to_string(),
));
}
Ok(WarrantStack(warrants))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::constraints::{ConstraintSet, Pattern};
use crate::crypto::SigningKey;
use std::time::Duration;
#[test]
fn test_normalize_token() {
let clean = "abcdef";
match normalize_token(clean) {
Cow::Borrowed(s) => assert_eq!(s, "abcdef"),
_ => panic!("Expected Borrowed for clean input"),
}
let surrounding = " abcdef ";
match normalize_token(surrounding) {
Cow::Borrowed(s) => assert_eq!(s, "abcdef"),
_ => panic!("Expected Borrowed for surrounding whitespace"),
}
let internal = "abc def";
match normalize_token(internal) {
Cow::Owned(s) => assert_eq!(s, "abcdef"),
_ => panic!("Expected Owned for internal whitespace"),
}
let pem = "-----BEGIN TENUO WARRANT-----\nabc\ndef\n-----END TENUO WARRANT-----";
assert_eq!(normalize_token(pem), "abcdef");
let pem_junk =
" -----BEGIN TENUO WARRANT----- \n abc \n def \n -----END TENUO WARRANT----- ";
assert_eq!(normalize_token(pem_junk), "abcdef");
assert_eq!(
normalize_token("-----BEGIN PUBLIC KEY----- abc"),
"-----BEGINPUBLICKEY-----abc"
);
}
#[test]
fn test_encode_decode_roundtrip() {
let keypair = SigningKey::generate();
let mut constraints = ConstraintSet::new();
constraints.insert("arg", Pattern::new("value-*").unwrap());
let warrant = Warrant::builder()
.capability("test_tool", constraints)
.ttl(Duration::from_secs(300))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
let encoded = encode(&warrant).unwrap();
let decoded = decode(&encoded).unwrap();
assert_eq!(decoded.id(), warrant.id());
assert_eq!(decoded.tools(), warrant.tools()); }
#[test]
fn test_base64_roundtrip() {
let keypair = SigningKey::generate();
let warrant = Warrant::builder()
.capability("test", ConstraintSet::new())
.ttl(Duration::from_secs(60))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
let encoded = encode_base64(&warrant).unwrap();
println!("Base64 warrant length: {} chars", encoded.len());
assert!(
encoded.len() < 1000,
"Warrant too large for typical headers"
);
let decoded = decode_base64(&encoded).unwrap();
assert_eq!(decoded.id(), warrant.id());
}
#[test]
fn test_version_check() {
let keypair = SigningKey::generate();
let warrant = Warrant::builder()
.capability("test", ConstraintSet::new())
.ttl(Duration::from_secs(60))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
let mut encoded = encode(&warrant).unwrap();
if let Some(first_byte) = encoded.first_mut() {
*first_byte = 99; }
let result = decode(&encoded);
assert!(
matches!(result, Err(Error::DeserializationError(_))),
"expected deserialization error for bad envelope_version"
);
}
#[test]
fn test_compact_encoding() {
let keypair = SigningKey::generate();
let minimal = Warrant::builder()
.capability("t", ConstraintSet::new())
.ttl(Duration::from_secs(60))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
let minimal_size = encode(&minimal).unwrap().len();
println!("Minimal warrant size: {} bytes", minimal_size);
let mut constraints = ConstraintSet::new();
constraints.insert("cluster", Pattern::new("staging-*").unwrap());
constraints.insert("version", Pattern::new("1.28.*").unwrap());
let with_constraints = Warrant::builder()
.capability("upgrade_cluster", constraints)
.ttl(Duration::from_secs(600))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
let constrained_size = encode(&with_constraints).unwrap().len();
println!("Constrained warrant size: {} bytes", constrained_size);
assert!(constrained_size < 2000);
}
#[test]
fn test_deterministic_serialization() {
let keypair = SigningKey::generate();
let mut constraints = ConstraintSet::new();
constraints.insert("zebra", Pattern::new("z-*").unwrap()); constraints.insert("alpha", Pattern::new("a-*").unwrap()); constraints.insert("middle", Pattern::new("m-*").unwrap()); let warrant1 = Warrant::builder()
.capability("test", constraints)
.ttl(Duration::from_secs(300))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
let bytes1 = encode(&warrant1).unwrap();
let bytes2 = encode(&warrant1).unwrap();
let bytes3 = encode(&warrant1).unwrap();
assert_eq!(bytes1, bytes2, "Serialization should be deterministic");
assert_eq!(bytes2, bytes3, "Serialization should be deterministic");
let decoded = decode(&bytes1).unwrap();
let bytes_after_roundtrip = encode(&decoded).unwrap();
assert_eq!(
bytes1, bytes_after_roundtrip,
"Serialization after roundtrip should be identical"
);
assert!(
decoded.verify(&keypair.public_key()).is_ok(),
"Signature verification should work after roundtrip"
);
}
#[test]
fn test_deterministic_constraint_set_serialization() {
use crate::constraints::{All, Constraint, Pattern, Range};
let keypair = SigningKey::generate();
let all_constraint1 = All::new([
Constraint::Pattern(Pattern::new("staging-*").unwrap()),
Constraint::Range(Range::max(1000.0).unwrap()),
]);
let all_constraint2 = All::new([
Constraint::Range(Range::max(1000.0).unwrap()),
Constraint::Pattern(Pattern::new("staging-*").unwrap()),
]);
let mut cs1 = ConstraintSet::new();
cs1.insert("cluster", all_constraint1.clone());
let warrant1 = Warrant::builder()
.capability("test", cs1)
.ttl(Duration::from_secs(300))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
let mut cs2 = ConstraintSet::new();
cs2.insert("cluster", all_constraint2.clone());
let warrant2 = Warrant::builder()
.capability("test", cs2)
.ttl(Duration::from_secs(300))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
let bytes1 = encode(&warrant1).unwrap();
let _bytes2 = encode(&warrant2).unwrap();
let bytes1_repeat = encode(&warrant1).unwrap();
assert_eq!(
bytes1, bytes1_repeat,
"Same warrant must serialize identically"
);
let decoded = decode(&bytes1).unwrap();
let bytes_after_roundtrip = encode(&decoded).unwrap();
assert_eq!(
bytes1, bytes_after_roundtrip,
"Serialization after roundtrip must be identical"
);
assert!(
decoded.verify(&keypair.public_key()).is_ok(),
"Signature verification must work after roundtrip"
);
}
#[test]
fn test_cbor_encoding_consistency() {
use std::collections::BTreeMap;
let mut map1: BTreeMap<String, i32> = BTreeMap::new();
map1.insert("zebra".to_string(), 1);
map1.insert("alpha".to_string(), 2);
let mut map2: BTreeMap<String, i32> = BTreeMap::new();
map2.insert("alpha".to_string(), 2); map2.insert("zebra".to_string(), 1);
let mut bytes1 = Vec::new();
let mut bytes2 = Vec::new();
ciborium::ser::into_writer(&map1, &mut bytes1).unwrap();
ciborium::ser::into_writer(&map2, &mut bytes2).unwrap();
assert_eq!(
bytes1, bytes2,
"BTreeMap should serialize identically regardless of insertion order"
);
#[derive(serde::Serialize)]
struct TestStruct {
a: i32,
b: String,
c: Option<f64>,
}
let s1 = TestStruct {
a: 42,
b: "hello".to_string(),
c: Some(1.234),
};
let s2 = TestStruct {
a: 42,
b: "hello".to_string(),
c: Some(1.234),
};
let mut b1 = Vec::new();
let mut b2 = Vec::new();
ciborium::ser::into_writer(&s1, &mut b1).unwrap();
ciborium::ser::into_writer(&s2, &mut b2).unwrap();
assert_eq!(b1, b2, "Identical structs should serialize identically");
let small: i64 = 23; let mut small_bytes = Vec::new();
ciborium::ser::into_writer(&small, &mut small_bytes).unwrap();
assert!(
small_bytes.len() <= 2,
"Small integers should use compact encoding: got {} bytes",
small_bytes.len()
);
}
#[test]
fn test_envelope_is_array() {
let keypair = SigningKey::generate();
let warrant = Warrant::builder()
.capability("test", ConstraintSet::new())
.ttl(Duration::from_secs(60))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
let encoded = encode(&warrant).unwrap();
assert_eq!(encoded[0], 0x83, "Wire format must be CBOR Array(3)");
}
#[test]
fn test_unknown_constraint_fail_closed() {
use crate::constraints::{Constraint, ConstraintValue};
let unknown = Constraint::Unknown {
type_id: 99,
payload: vec![1, 2, 3],
};
let value = ConstraintValue::String("anything".to_string());
assert!(
unknown.matches(&value).is_err(),
"Unknown constraint must fail closed with an error"
);
assert_eq!(unknown.depth(), 0);
let json = r#"{"type": "FutureConstraint", "value": {"foo": "bar"}}"#;
let deserialized: std::result::Result<Constraint, _> = serde_json::from_str(json);
assert!(
deserialized.is_err(),
"Should fail deserialization for unknown constraint with content (current safe-guard)"
);
}
#[test]
fn test_pem_chain_and_encode() {
let keypair = SigningKey::generate();
let warrant1 = Warrant::builder()
.capability("w1", ConstraintSet::new())
.ttl(Duration::from_secs(60))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
let warrant2 = Warrant::builder()
.capability("w2", ConstraintSet::new())
.ttl(Duration::from_secs(60))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
let pem1 = encode_pem(&warrant1).unwrap();
assert!(pem1.starts_with("-----BEGIN TENUO WARRANT-----\n"));
assert!(pem1.ends_with("-----END TENUO WARRANT-----\n"));
let pem2 = encode_pem(&warrant2).unwrap();
let chain_str = format!("{}{}", pem1, pem2);
let stack = decode_pem_chain(&chain_str).unwrap();
assert_eq!(stack.0.len(), 2);
assert_eq!(stack.0[0].id(), warrant1.id());
assert_eq!(stack.0[1].id(), warrant2.id());
let explicit_stack = WarrantStack(vec![warrant1.clone(), warrant2.clone()]);
let explicit_pem = encode_pem_stack(&explicit_stack).unwrap();
assert!(explicit_pem.starts_with("-----BEGIN TENUO WARRANT CHAIN-----"));
let decoded_explicit = decode_pem_chain(&explicit_pem).unwrap();
assert_eq!(decoded_explicit.0.len(), 2);
assert_eq!(decoded_explicit.0[0].id(), warrant1.id());
let bad = decode_pem_chain("junk");
assert!(bad.is_err());
}
#[test]
fn test_stack_size_limit() {
let keypair = SigningKey::generate();
let mut constraints = ConstraintSet::new();
for i in 0..50 {
let large_val = "x".repeat(1000);
constraints.insert(
format!("chk_{}", i),
Pattern::new(&format!("{}-*", large_val)).unwrap(),
);
}
let heavy_warrant = Warrant::builder()
.capability("heavy_loading", constraints)
.ttl(Duration::from_secs(60))
.holder(keypair.public_key())
.build(&keypair)
.unwrap();
let single_size = encode(&heavy_warrant).unwrap().len();
println!("Heavy warrant size: {} bytes", single_size);
assert!(
single_size < MAX_WARRANT_SIZE,
"Single warrant must be valid"
);
let chain = vec![heavy_warrant; 6];
let stack = WarrantStack(chain);
let encoded_stack = encode_stack(&stack).unwrap();
println!("Encoded stack size: {} bytes", encoded_stack.len());
assert!(
encoded_stack.len() > MAX_STACK_SIZE,
"Test setup failed: stack not large enough"
);
let result = decode_stack(&encoded_stack);
match result {
Err(Error::PayloadTooLarge { size, max }) => {
assert_eq!(max, MAX_STACK_SIZE);
assert_eq!(size, encoded_stack.len());
}
res => panic!("Expected PayloadTooLarge, got {:?}", res),
}
}
}