use ed25519_dalek::VerifyingKey;
use serde::{Deserialize, Serialize};
use crate::cert::DelegationCert;
use crate::chain::{DyoloChain, VerificationReceipt};
use crate::error::KyaError;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignedChain {
pub version: u8,
pub principal_pk: String,
pub principal_scope: String,
pub certs: Vec<DelegationCert>,
}
impl SignedChain {
pub fn from_chain(chain: &DyoloChain) -> Self {
Self {
version: 1,
principal_pk: hex::encode(chain.principal_pk.as_bytes()),
principal_scope: hex::encode(chain.principal_scope),
certs: chain.certs().to_vec(),
}
}
pub fn from_json(json: &str) -> Result<Self, KyaError> {
serde_json::from_str(json).map_err(|e| KyaError::WireFormatError(e.to_string()))
}
#[cfg(feature = "cbor")]
#[cfg_attr(docsrs, doc(cfg(feature = "cbor")))]
pub fn from_cbor(cbor: &[u8]) -> Result<Self, KyaError> {
ciborium::from_reader(cbor).map_err(|e| KyaError::WireFormatError(e.to_string()))
}
#[cfg(feature = "cbor")]
#[cfg_attr(docsrs, doc(cfg(feature = "cbor")))]
pub fn to_cbor(&self) -> Result<Vec<u8>, KyaError> {
let mut buf = Vec::new();
ciborium::into_writer(self, &mut buf)
.map_err(|e| KyaError::WireFormatError(e.to_string()))?;
Ok(buf)
}
pub fn into_chain_with_drift(self, drift_tolerance_secs: u64) -> Result<DyoloChain, KyaError> {
if self.version != 1 {
return Err(KyaError::UnsupportedVersion {
expected: 1,
got: self.version,
});
}
let pk_bytes: [u8; 32] = hex::decode(&self.principal_pk)
.map_err(|e| KyaError::WireFormatError(format!("principal_pk: {e}")))?
.try_into()
.map_err(|_| KyaError::WireFormatError("principal_pk must be 32 bytes".into()))?;
let scope_bytes: [u8; 32] = hex::decode(&self.principal_scope)
.map_err(|e| KyaError::WireFormatError(format!("principal_scope: {e}")))?
.try_into()
.map_err(|_| KyaError::WireFormatError("principal_scope must be 32 bytes".into()))?;
let pk = VerifyingKey::from_bytes(&pk_bytes)
.map_err(|e| KyaError::WireFormatError(format!("invalid principal_pk: {e}")))?;
let mut chain = DyoloChain::new(pk, scope_bytes).with_drift_tolerance(drift_tolerance_secs);
for cert in self.certs {
chain.push(cert);
}
Ok(chain)
}
#[deprecated(since = "2.0.0", note = "Use `into_chain_with_drift` instead.")]
pub fn into_chain(self) -> Result<DyoloChain, KyaError> {
self.into_chain_with_drift(15)
}
pub fn to_json(&self) -> Result<String, KyaError> {
serde_json::to_string(self).map_err(|e| KyaError::WireFormatError(e.to_string()))
}
pub fn to_json_pretty(&self) -> Result<String, KyaError> {
serde_json::to_string_pretty(self).map_err(|e| KyaError::WireFormatError(e.to_string()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VerifiedToken {
pub receipt: VerificationReceipt,
pub mac: String,
}
impl VerifiedToken {
pub fn sign(receipt: &VerificationReceipt, mac_key: &[u8; 32]) -> Self {
let mac = Self::compute_mac(receipt, mac_key);
Self {
receipt: receipt.clone(),
mac: hex::encode(mac),
}
}
pub fn verify(&self, mac_key: &[u8; 32]) -> Result<&VerificationReceipt, KyaError> {
let mac_bytes: [u8; 32] = hex::decode(&self.mac)
.map_err(|_| KyaError::WireFormatError("invalid MAC hex".into()))?
.try_into()
.map_err(|_| KyaError::WireFormatError("MAC must be 32 bytes".into()))?;
let expected = Self::compute_mac(&self.receipt, mac_key);
use subtle::ConstantTimeEq;
if mac_bytes.ct_eq(&expected).unwrap_u8() == 0 {
return Err(KyaError::MacVerificationFailed);
}
Ok(&self.receipt)
}
fn compute_mac(receipt: &VerificationReceipt, key: &[u8; 32]) -> [u8; 32] {
let mut h = blake3::Hasher::new_keyed(key);
h.update(&receipt.canonical_bytes());
h.finalize().into()
}
}
#[cfg(feature = "schema")]
#[cfg_attr(docsrs, doc(cfg(feature = "schema")))]
pub const SIGNED_CHAIN_SCHEMA_V1: &str = include_str!("../wire/schema.json");
#[cfg(not(feature = "schema"))]
pub const SIGNED_CHAIN_SCHEMA_V1: &str = "Enable the `schema` feature to include the JSON schema.";