use alloc::string::{String, ToString};
use alloc::vec::Vec;
use serde::{Deserialize, Serialize};
use chio_core_types::canonical_json_bytes;
use chio_core_types::crypto::{PublicKey, Signature};
use crate::clock::Clock;
pub const PORTABLE_PASSPORT_SCHEMA: &str = "chio.portable-agent-passport.v1";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PortablePassportBody {
pub schema: String,
pub subject: String,
pub issuer: PublicKey,
pub issued_at: u64,
pub expires_at: u64,
#[serde(with = "payload_bytes_hex")]
pub payload_canonical_bytes: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct PortablePassportEnvelope {
pub body: PortablePassportBody,
pub signature: Signature,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifiedPassport {
pub subject: String,
pub issuer: PublicKey,
pub issued_at: u64,
pub expires_at: u64,
pub evaluated_at: u64,
pub payload_canonical_bytes: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VerifyError {
InvalidEnvelope(String),
InvalidSchema,
MissingSubject,
InvalidValidityWindow,
UntrustedIssuer,
InvalidSignature,
NotYetValid,
Expired,
Internal(String),
}
pub fn verify_passport(
envelope_bytes: &[u8],
authority_keys: &[PublicKey],
clock: &dyn Clock,
) -> Result<VerifiedPassport, VerifyError> {
let envelope: PortablePassportEnvelope = serde_json::from_slice(envelope_bytes)
.map_err(|error| VerifyError::InvalidEnvelope(error.to_string()))?;
verify_parsed_passport(&envelope, authority_keys, clock)
}
pub fn verify_parsed_passport(
envelope: &PortablePassportEnvelope,
authority_keys: &[PublicKey],
clock: &dyn Clock,
) -> Result<VerifiedPassport, VerifyError> {
if envelope.body.schema != PORTABLE_PASSPORT_SCHEMA {
return Err(VerifyError::InvalidSchema);
}
if envelope.body.subject.is_empty() {
return Err(VerifyError::MissingSubject);
}
if envelope.body.issued_at > envelope.body.expires_at {
return Err(VerifyError::InvalidValidityWindow);
}
if !authority_keys.contains(&envelope.body.issuer) {
return Err(VerifyError::UntrustedIssuer);
}
let body_bytes = canonical_json_bytes(&envelope.body)
.map_err(|error| VerifyError::Internal(error.to_string()))?;
if !envelope
.body
.issuer
.verify(&body_bytes, &envelope.signature)
{
return Err(VerifyError::InvalidSignature);
}
let now = clock.now_unix_secs();
if now < envelope.body.issued_at {
return Err(VerifyError::NotYetValid);
}
if now >= envelope.body.expires_at {
return Err(VerifyError::Expired);
}
Ok(VerifiedPassport {
subject: envelope.body.subject.clone(),
issuer: envelope.body.issuer.clone(),
issued_at: envelope.body.issued_at,
expires_at: envelope.body.expires_at,
evaluated_at: now,
payload_canonical_bytes: envelope.body.payload_canonical_bytes.clone(),
})
}
mod payload_bytes_hex {
use alloc::string::String;
use alloc::vec::Vec;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
pub fn serialize<S: Serializer>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
encode_hex(bytes).serialize(serializer)
}
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<u8>, D::Error> {
let hex_str = String::deserialize(deserializer)?;
decode_hex(&hex_str).map_err(serde::de::Error::custom)
}
fn encode_hex(bytes: &[u8]) -> String {
let mut out = String::with_capacity(bytes.len() * 2);
for byte in bytes {
let hi = NIBBLES[(byte >> 4) as usize];
let lo = NIBBLES[(byte & 0x0f) as usize];
out.push(hi);
out.push(lo);
}
out
}
fn decode_hex(hex_str: &str) -> Result<Vec<u8>, &'static str> {
if !hex_str.len().is_multiple_of(2) {
return Err("odd-length hex string");
}
let bytes_in = hex_str.as_bytes();
let mut out = Vec::with_capacity(bytes_in.len() / 2);
let mut idx = 0;
while idx < bytes_in.len() {
let hi = from_hex_nibble(bytes_in[idx])?;
let lo = from_hex_nibble(bytes_in[idx + 1])?;
out.push((hi << 4) | lo);
idx += 2;
}
Ok(out)
}
const NIBBLES: [char; 16] = [
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f',
];
fn from_hex_nibble(byte: u8) -> Result<u8, &'static str> {
match byte {
b'0'..=b'9' => Ok(byte - b'0'),
b'a'..=b'f' => Ok(byte - b'a' + 10),
b'A'..=b'F' => Ok(byte - b'A' + 10),
_ => Err("invalid hex character"),
}
}
}