use serde::ser::SerializeMap;
use serde::{Deserialize, Serialize, Serializer};
use std::fmt;
use crate::capability::Capability;
use crate::types::{CesrKey, ConfigTrait, Prefix, Said, Threshold, VersionString};
pub const KERI_VERSION_PREFIX: &str = "KERI10JSON";
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct KeriSequence(u128);
#[cfg(feature = "schema")]
impl schemars::JsonSchema for KeriSequence {
fn schema_name() -> String {
"KeriSequence".to_string()
}
fn json_schema(_gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
..Default::default()
}
.into()
}
}
impl KeriSequence {
pub fn new(value: u128) -> Self {
Self(value)
}
pub fn value(self) -> u128 {
self.0
}
}
impl fmt::Display for KeriSequence {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:x}", self.0)
}
}
impl Serialize for KeriSequence {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&format!("{:x}", self.0))
}
}
impl<'de> Deserialize<'de> for KeriSequence {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
let value = u128::from_str_radix(&s, 16)
.map_err(|_| serde::de::Error::custom(format!("invalid hex sequence: {s:?}")))?;
Ok(KeriSequence(value))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum Seal {
Digest {
d: Said,
},
SourceEvent {
s: KeriSequence,
d: Said,
},
KeyEvent {
i: Prefix,
s: KeriSequence,
d: Said,
},
EventLocation {
i: Prefix,
s: KeriSequence,
p: Said,
t: String,
d: Said,
},
LatestEstablishment {
i: Prefix,
},
#[cfg(feature = "seal-extensions")]
MerkleRoot {
rd: Said,
},
#[cfg(feature = "seal-extensions")]
RegistrarBacker {
bi: Prefix,
d: Said,
},
}
impl Seal {
pub fn digest(said: impl Into<String>) -> Self {
Self::Digest {
d: Said::new_unchecked(said.into()),
}
}
pub fn key_event(prefix: Prefix, sequence: KeriSequence, said: Said) -> Self {
Self::KeyEvent {
i: prefix,
s: sequence,
d: said,
}
}
pub fn digest_value(&self) -> Option<&Said> {
match self {
Seal::Digest { d } => Some(d),
Seal::SourceEvent { d, .. } => Some(d),
Seal::KeyEvent { d, .. } => Some(d),
Seal::EventLocation { d, .. } => Some(d),
Seal::LatestEstablishment { .. } => None,
#[cfg(feature = "seal-extensions")]
Seal::RegistrarBacker { d, .. } => Some(d),
#[cfg(feature = "seal-extensions")]
Seal::MerkleRoot { rd } => Some(rd),
}
}
}
impl Serialize for Seal {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Seal::Digest { d } => {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("d", d)?;
map.end()
}
Seal::SourceEvent { s, d } => {
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("s", s)?;
map.serialize_entry("d", d)?;
map.end()
}
Seal::KeyEvent { i, s, d } => {
let mut map = serializer.serialize_map(Some(3))?;
map.serialize_entry("i", i)?;
map.serialize_entry("s", s)?;
map.serialize_entry("d", d)?;
map.end()
}
Seal::EventLocation { i, s, p, t, d } => {
let mut map = serializer.serialize_map(Some(5))?;
map.serialize_entry("i", i)?;
map.serialize_entry("s", s)?;
map.serialize_entry("p", p)?;
map.serialize_entry("t", t)?;
map.serialize_entry("d", d)?;
map.end()
}
Seal::LatestEstablishment { i } => {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("i", i)?;
map.end()
}
#[cfg(feature = "seal-extensions")]
Seal::MerkleRoot { rd } => {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("rd", rd)?;
map.end()
}
#[cfg(feature = "seal-extensions")]
Seal::RegistrarBacker { bi, d } => {
let mut map = serializer.serialize_map(Some(2))?;
map.serialize_entry("bi", bi)?;
map.serialize_entry("d", d)?;
map.end()
}
}
}
}
impl<'de> Deserialize<'de> for Seal {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let map: serde_json::Map<String, serde_json::Value> =
serde_json::Map::deserialize(deserializer)?;
let want_str = |k: &str| -> Result<String, D::Error> {
map.get(k)
.and_then(|v| v.as_str())
.map(|v| v.to_string())
.ok_or_else(|| {
serde::de::Error::custom(format!("seal field `{k}` must be a string"))
})
};
let want_seq =
|k: &str| -> Result<KeriSequence, D::Error> {
serde_json::from_value(map.get(k).cloned().ok_or_else(|| {
serde::de::Error::custom(format!("seal field `{k}` required"))
})?)
.map_err(serde::de::Error::custom)
};
let mut keys: Vec<&str> = map.keys().map(String::as_str).collect();
keys.sort_unstable();
match keys.as_slice() {
["d"] => Ok(Seal::Digest {
d: Said::new_unchecked(want_str("d")?),
}),
["d", "s"] => Ok(Seal::SourceEvent {
s: want_seq("s")?,
d: Said::new_unchecked(want_str("d")?),
}),
["d", "i", "s"] => Ok(Seal::KeyEvent {
i: Prefix::new_unchecked(want_str("i")?),
s: want_seq("s")?,
d: Said::new_unchecked(want_str("d")?),
}),
["d", "i", "p", "s", "t"] => Ok(Seal::EventLocation {
i: Prefix::new_unchecked(want_str("i")?),
s: want_seq("s")?,
p: Said::new_unchecked(want_str("p")?),
t: want_str("t")?,
d: Said::new_unchecked(want_str("d")?),
}),
["i"] => Ok(Seal::LatestEstablishment {
i: Prefix::new_unchecked(want_str("i")?),
}),
#[cfg(feature = "seal-extensions")]
["rd"] => Ok(Seal::MerkleRoot {
rd: Said::new_unchecked(want_str("rd")?),
}),
#[cfg(feature = "seal-extensions")]
["bi", "d"] => Ok(Seal::RegistrarBacker {
bi: Prefix::new_unchecked(want_str("bi")?),
d: Said::new_unchecked(want_str("d")?),
}),
other => Err(serde::de::Error::custom(format!(
"unrecognized seal field set: {other:?}"
))),
}
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct IcpEvent {
pub v: VersionString,
pub d: Said,
pub i: Prefix,
pub s: KeriSequence,
pub kt: Threshold,
pub k: Vec<CesrKey>,
pub nt: Threshold,
pub n: Vec<Said>,
pub bt: Threshold,
#[serde(default)]
pub b: Vec<Prefix>,
#[serde(default)]
pub c: Vec<ConfigTrait>,
#[serde(default)]
pub a: Vec<Seal>,
}
pub struct IcpEventInit {
pub v: VersionString,
pub d: Said,
pub i: Prefix,
pub s: KeriSequence,
pub kt: Threshold,
pub k: Vec<CesrKey>,
pub nt: Threshold,
pub n: Vec<Said>,
pub bt: Threshold,
pub b: Vec<Prefix>,
pub c: Vec<ConfigTrait>,
pub a: Vec<Seal>,
}
impl IcpEvent {
pub fn new(init: IcpEventInit) -> Self {
Self {
v: init.v,
d: init.d,
i: init.i,
s: init.s,
kt: init.kt,
k: init.k,
nt: init.nt,
n: init.n,
bt: init.bt,
b: init.b,
c: init.c,
a: init.a,
}
}
}
impl Serialize for IcpEvent {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let field_count = 13;
let mut map = serializer.serialize_map(Some(field_count))?;
map.serialize_entry("v", &self.v)?;
map.serialize_entry("t", "icp")?;
map.serialize_entry("d", &self.d)?;
map.serialize_entry("i", &self.i)?;
map.serialize_entry("s", &self.s)?;
map.serialize_entry("kt", &self.kt)?;
map.serialize_entry("k", &self.k)?;
map.serialize_entry("nt", &self.nt)?;
map.serialize_entry("n", &self.n)?;
map.serialize_entry("bt", &self.bt)?;
map.serialize_entry("b", &self.b)?;
map.serialize_entry("c", &self.c)?;
map.serialize_entry("a", &self.a)?;
map.end()
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct RotEvent {
pub v: VersionString,
pub d: Said,
pub i: Prefix,
pub s: KeriSequence,
pub p: Said,
pub kt: Threshold,
pub k: Vec<CesrKey>,
pub nt: Threshold,
pub n: Vec<Said>,
pub bt: Threshold,
#[serde(default)]
pub br: Vec<Prefix>,
#[serde(default)]
pub ba: Vec<Prefix>,
#[serde(default)]
pub c: Vec<ConfigTrait>,
#[serde(default)]
pub a: Vec<Seal>,
}
pub struct RotEventInit {
pub v: VersionString,
pub d: Said,
pub i: Prefix,
pub s: KeriSequence,
pub p: Said,
pub kt: Threshold,
pub k: Vec<CesrKey>,
pub nt: Threshold,
pub n: Vec<Said>,
pub bt: Threshold,
pub br: Vec<Prefix>,
pub ba: Vec<Prefix>,
pub c: Vec<ConfigTrait>,
pub a: Vec<Seal>,
}
impl RotEvent {
pub fn new(init: RotEventInit) -> Self {
Self {
v: init.v,
d: init.d,
i: init.i,
s: init.s,
p: init.p,
kt: init.kt,
k: init.k,
nt: init.nt,
n: init.n,
bt: init.bt,
br: init.br,
ba: init.ba,
c: init.c,
a: init.a,
}
}
}
impl Serialize for RotEvent {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let field_count = 14;
let mut map = serializer.serialize_map(Some(field_count))?;
map.serialize_entry("v", &self.v)?;
map.serialize_entry("t", "rot")?;
map.serialize_entry("d", &self.d)?;
map.serialize_entry("i", &self.i)?;
map.serialize_entry("s", &self.s)?;
map.serialize_entry("p", &self.p)?;
map.serialize_entry("kt", &self.kt)?;
map.serialize_entry("k", &self.k)?;
map.serialize_entry("nt", &self.nt)?;
map.serialize_entry("n", &self.n)?;
map.serialize_entry("bt", &self.bt)?;
map.serialize_entry("br", &self.br)?;
map.serialize_entry("ba", &self.ba)?;
map.serialize_entry("a", &self.a)?;
map.end()
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct IxnEvent {
pub v: VersionString,
pub d: Said,
pub i: Prefix,
pub s: KeriSequence,
pub p: Said,
pub a: Vec<Seal>,
}
pub struct IxnEventInit {
pub v: VersionString,
pub d: Said,
pub i: Prefix,
pub s: KeriSequence,
pub p: Said,
pub a: Vec<Seal>,
}
impl IxnEvent {
pub fn new(init: IxnEventInit) -> Self {
Self {
v: init.v,
d: init.d,
i: init.i,
s: init.s,
p: init.p,
a: init.a,
}
}
}
impl Serialize for IxnEvent {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let field_count = 7;
let mut map = serializer.serialize_map(Some(field_count))?;
map.serialize_entry("v", &self.v)?;
map.serialize_entry("t", "ixn")?;
map.serialize_entry("d", &self.d)?;
map.serialize_entry("i", &self.i)?;
map.serialize_entry("s", &self.s)?;
map.serialize_entry("p", &self.p)?;
map.serialize_entry("a", &self.a)?;
map.end()
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct DipEvent {
pub v: VersionString,
pub d: Said,
pub i: Prefix,
pub s: KeriSequence,
pub kt: Threshold,
pub k: Vec<CesrKey>,
pub nt: Threshold,
pub n: Vec<Said>,
pub bt: Threshold,
#[serde(default)]
pub b: Vec<Prefix>,
#[serde(default)]
pub c: Vec<ConfigTrait>,
#[serde(default)]
pub a: Vec<Seal>,
pub di: Prefix,
#[serde(skip)]
pub source_seal: Option<SourceSeal>,
}
pub struct DipEventInit {
pub v: VersionString,
pub d: Said,
pub i: Prefix,
pub s: KeriSequence,
pub kt: Threshold,
pub k: Vec<CesrKey>,
pub nt: Threshold,
pub n: Vec<Said>,
pub bt: Threshold,
pub b: Vec<Prefix>,
pub c: Vec<ConfigTrait>,
pub a: Vec<Seal>,
pub di: Prefix,
}
impl DipEvent {
pub fn new(init: DipEventInit) -> Self {
Self {
v: init.v,
d: init.d,
i: init.i,
s: init.s,
kt: init.kt,
k: init.k,
nt: init.nt,
n: init.n,
bt: init.bt,
b: init.b,
c: init.c,
a: init.a,
di: init.di,
source_seal: None,
}
}
}
impl Serialize for DipEvent {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let field_count = 14;
let mut map = serializer.serialize_map(Some(field_count))?;
map.serialize_entry("v", &self.v)?;
map.serialize_entry("t", "dip")?;
map.serialize_entry("d", &self.d)?;
map.serialize_entry("i", &self.i)?;
map.serialize_entry("s", &self.s)?;
map.serialize_entry("kt", &self.kt)?;
map.serialize_entry("k", &self.k)?;
map.serialize_entry("nt", &self.nt)?;
map.serialize_entry("n", &self.n)?;
map.serialize_entry("bt", &self.bt)?;
map.serialize_entry("b", &self.b)?;
map.serialize_entry("c", &self.c)?;
map.serialize_entry("a", &self.a)?;
map.serialize_entry("di", &self.di)?;
map.end()
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct DrtEvent {
pub v: VersionString,
pub d: Said,
pub i: Prefix,
pub s: KeriSequence,
pub p: Said,
pub kt: Threshold,
pub k: Vec<CesrKey>,
pub nt: Threshold,
pub n: Vec<Said>,
pub bt: Threshold,
#[serde(default)]
pub br: Vec<Prefix>,
#[serde(default)]
pub ba: Vec<Prefix>,
#[serde(default)]
pub c: Vec<ConfigTrait>,
#[serde(default)]
pub a: Vec<Seal>,
pub di: Prefix,
#[serde(skip)]
pub source_seal: Option<SourceSeal>,
}
pub struct DrtEventInit {
pub v: VersionString,
pub d: Said,
pub i: Prefix,
pub s: KeriSequence,
pub p: Said,
pub kt: Threshold,
pub k: Vec<CesrKey>,
pub nt: Threshold,
pub n: Vec<Said>,
pub bt: Threshold,
pub br: Vec<Prefix>,
pub ba: Vec<Prefix>,
pub c: Vec<ConfigTrait>,
pub a: Vec<Seal>,
pub di: Prefix,
}
impl DrtEvent {
pub fn new(init: DrtEventInit) -> Self {
Self {
v: init.v,
d: init.d,
i: init.i,
s: init.s,
p: init.p,
kt: init.kt,
k: init.k,
nt: init.nt,
n: init.n,
bt: init.bt,
br: init.br,
ba: init.ba,
c: init.c,
a: init.a,
di: init.di,
source_seal: None,
}
}
}
impl Serialize for DrtEvent {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let field_count = 15;
let mut map = serializer.serialize_map(Some(field_count))?;
map.serialize_entry("v", &self.v)?;
map.serialize_entry("t", "drt")?;
map.serialize_entry("d", &self.d)?;
map.serialize_entry("i", &self.i)?;
map.serialize_entry("s", &self.s)?;
map.serialize_entry("p", &self.p)?;
map.serialize_entry("kt", &self.kt)?;
map.serialize_entry("k", &self.k)?;
map.serialize_entry("nt", &self.nt)?;
map.serialize_entry("n", &self.n)?;
map.serialize_entry("bt", &self.bt)?;
map.serialize_entry("br", &self.br)?;
map.serialize_entry("ba", &self.ba)?;
map.serialize_entry("a", &self.a)?;
map.serialize_entry("di", &self.di)?;
map.end()
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(tag = "t")]
pub enum Event {
#[serde(rename = "icp")]
Icp(IcpEvent),
#[serde(rename = "rot")]
Rot(RotEvent),
#[serde(rename = "ixn")]
Ixn(IxnEvent),
#[serde(rename = "dip")]
Dip(DipEvent),
#[serde(rename = "drt")]
Drt(DrtEvent),
}
impl Serialize for Event {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Event::Icp(e) => e.serialize(serializer),
Event::Rot(e) => e.serialize(serializer),
Event::Ixn(e) => e.serialize(serializer),
Event::Dip(e) => e.serialize(serializer),
Event::Drt(e) => e.serialize(serializer),
}
}
}
impl Event {
pub fn said(&self) -> &Said {
match self {
Event::Icp(e) => &e.d,
Event::Rot(e) => &e.d,
Event::Ixn(e) => &e.d,
Event::Dip(e) => &e.d,
Event::Drt(e) => &e.d,
}
}
pub fn sequence(&self) -> KeriSequence {
match self {
Event::Icp(e) => e.s,
Event::Rot(e) => e.s,
Event::Ixn(e) => e.s,
Event::Dip(e) => e.s,
Event::Drt(e) => e.s,
}
}
pub fn prefix(&self) -> &Prefix {
match self {
Event::Icp(e) => &e.i,
Event::Rot(e) => &e.i,
Event::Ixn(e) => &e.i,
Event::Dip(e) => &e.i,
Event::Drt(e) => &e.i,
}
}
pub fn previous(&self) -> Option<&Said> {
match self {
Event::Icp(_) | Event::Dip(_) => None,
Event::Rot(e) => Some(&e.p),
Event::Ixn(e) => Some(&e.p),
Event::Drt(e) => Some(&e.p),
}
}
pub fn keys(&self) -> Option<&[CesrKey]> {
match self {
Event::Icp(e) => Some(&e.k),
Event::Rot(e) => Some(&e.k),
Event::Dip(e) => Some(&e.k),
Event::Drt(e) => Some(&e.k),
Event::Ixn(_) => None,
}
}
pub fn next_commitments(&self) -> Option<&[Said]> {
match self {
Event::Icp(e) => Some(&e.n),
Event::Rot(e) => Some(&e.n),
Event::Dip(e) => Some(&e.n),
Event::Drt(e) => Some(&e.n),
Event::Ixn(_) => None,
}
}
pub fn anchors(&self) -> &[Seal] {
match self {
Event::Icp(e) => &e.a,
Event::Rot(e) => &e.a,
Event::Ixn(e) => &e.a,
Event::Dip(e) => &e.a,
Event::Drt(e) => &e.a,
}
}
pub fn delegator(&self) -> Option<&Prefix> {
match self {
Event::Dip(e) => Some(&e.di),
_ => None,
}
}
pub fn is_inception(&self) -> bool {
matches!(self, Event::Icp(_) | Event::Dip(_))
}
pub fn is_rotation(&self) -> bool {
matches!(self, Event::Rot(_) | Event::Drt(_))
}
pub fn is_interaction(&self) -> bool {
matches!(self, Event::Ixn(_))
}
pub fn is_delegated(&self) -> bool {
matches!(self, Event::Dip(_) | Event::Drt(_))
}
pub fn source_seal(&self) -> Option<&SourceSeal> {
match self {
Event::Dip(e) => e.source_seal.as_ref(),
Event::Drt(e) => e.source_seal.as_ref(),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct IndexedSignature {
pub index: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub prior_index: Option<u32>,
#[serde(with = "hex::serde")]
pub sig: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct SourceSeal {
pub s: KeriSequence,
pub d: Said,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
pub struct AgentScope {
pub capabilities: Vec<Capability>,
pub expires_at: Option<i64>,
}
const AGENT_SCOPE_MARKER: &str = "agentscope:";
pub fn encode_agent_scope(agent_prefix: &str, scope: &AgentScope) -> String {
let expires = scope.expires_at.unwrap_or(0);
let caps = scope
.capabilities
.iter()
.map(Capability::as_str)
.collect::<Vec<_>>()
.join(",");
format!("{AGENT_SCOPE_MARKER}{agent_prefix}:{expires}:{caps}")
}
pub fn decode_agent_scope(value: &str) -> Option<(String, AgentScope)> {
let rest = value.strip_prefix(AGENT_SCOPE_MARKER)?;
let mut parts = rest.splitn(3, ':');
let prefix = parts.next()?.to_string();
let expires: i64 = parts.next()?.parse().ok()?;
let caps_csv = parts.next().unwrap_or("");
let capabilities = if caps_csv.is_empty() {
Vec::new()
} else {
caps_csv
.split(',')
.map(Capability::parse)
.collect::<Result<Vec<_>, _>>()
.ok()?
};
Some((
prefix,
AgentScope {
capabilities,
expires_at: (expires != 0).then_some(expires),
},
))
}
pub fn serialize_source_seal_couples(couples: &[SourceSeal]) -> Result<Vec<u8>, AttachmentError> {
use cesride::{Counter, Matter, Saider, Seqner, counter};
let count = u32::try_from(couples.len())
.map_err(|_| AttachmentError::Encode("too many source seal couples".into()))?;
let mut out = Counter::new_with_code_and_count(counter::Codex::SealSourceCouples, count)
.and_then(|c| c.qb64())
.map_err(|e| AttachmentError::Encode(e.to_string()))?;
for couple in couples {
let seqner = Seqner::new_with_sn(couple.s.value())
.and_then(|s| s.qb64())
.map_err(|e| AttachmentError::Encode(e.to_string()))?;
let saider = Saider::new_with_qb64(couple.d.as_str())
.and_then(|s| s.qb64())
.map_err(|e| AttachmentError::Encode(e.to_string()))?;
out.push_str(&seqner);
out.push_str(&saider);
}
Ok(out.into_bytes())
}
pub fn parse_source_seal_couples(bytes: &[u8]) -> Result<Vec<SourceSeal>, AttachmentError> {
let s = std::str::from_utf8(bytes)
.map_err(|e| AttachmentError::Decode(format!("non-utf8 source-seal group: {e}")))?;
let (couples, rest) = parse_source_seal_group(s)?;
if !rest.is_empty() {
return Err(AttachmentError::Decode(format!(
"trailing bytes after source-seal group: {rest:?}"
)));
}
Ok(couples)
}
fn parse_source_seal_group(s: &str) -> Result<(Vec<SourceSeal>, &str), AttachmentError> {
use cesride::{Matter, Saider, Seqner};
let rest = s.strip_prefix("-G").ok_or_else(|| {
AttachmentError::Decode("source-seal group must start with -G counter code".into())
})?;
if rest.len() < 2 {
return Err(AttachmentError::Decode(
"truncated -G counter header".into(),
));
}
let (count_b64, mut cursor) = rest.split_at(2);
let count = decode_count_b64(count_b64)?;
let mut out = Vec::with_capacity(count);
for _ in 0..count {
if cursor.len() < SEQNER_QB64_LEN {
return Err(AttachmentError::Decode(
"truncated Seqner in -G couple".into(),
));
}
let (seqner_qb64, after_seqner) = cursor.split_at(SEQNER_QB64_LEN);
let seqner = Seqner::new_with_qb64(seqner_qb64)
.map_err(|e| AttachmentError::Decode(format!("Seqner: {e}")))?;
let sn = seqner
.sn()
.map_err(|e| AttachmentError::Decode(format!("Seqner sn: {e}")))?;
let first = *after_seqner.as_bytes().first().ok_or_else(|| {
AttachmentError::Decode("truncated -G couple: expected a Saider".into())
})?;
let fs = saider_qb64_len(first).ok_or_else(|| {
AttachmentError::Decode(format!("unsupported Saider code byte {first:?}"))
})?;
if after_seqner.len() < fs {
return Err(AttachmentError::Decode(
"truncated Saider in -G couple".into(),
));
}
let (saider_qb64, remainder) = after_seqner.split_at(fs);
let saider = Saider::new_with_qb64(saider_qb64)
.and_then(|s| s.qb64())
.map_err(|e| AttachmentError::Decode(format!("Saider: {e}")))?;
cursor = remainder;
out.push(SourceSeal {
s: KeriSequence::new(sn),
d: Said::new_unchecked(saider),
});
}
Ok((out, cursor))
}
pub fn parse_delegated_attachment(
bytes: &[u8],
) -> Result<(Vec<IndexedSignature>, Vec<SourceSeal>), AttachmentError> {
let s = std::str::from_utf8(bytes)
.map_err(|e| AttachmentError::Decode(format!("non-utf8 attachment: {e}")))?;
if s.is_empty() {
return Ok((vec![], vec![]));
}
let (sigs, rest) = parse_sig_group(s)?;
if rest.is_empty() {
return Ok((sigs, vec![]));
}
let (couples, tail) = parse_source_seal_group(rest)?;
if !tail.is_empty() {
return Err(AttachmentError::Decode(format!(
"trailing bytes after delegated attachment: {tail:?}"
)));
}
Ok((sigs, couples))
}
const SEQNER_QB64_LEN: usize = 24;
fn saider_qb64_len(first: u8) -> Option<usize> {
matches!(first, b'E' | b'F' | b'G' | b'H' | b'I').then_some(44)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SignedEvent {
pub event: Event,
pub signatures: Vec<IndexedSignature>,
}
impl SignedEvent {
pub fn new(event: Event, signatures: Vec<IndexedSignature>) -> Self {
Self { event, signatures }
}
pub fn said(&self) -> &Said {
self.event.said()
}
pub fn sequence(&self) -> KeriSequence {
self.event.sequence()
}
pub fn prefix(&self) -> &Prefix {
self.event.prefix()
}
}
pub fn serialize_attachment(signatures: &[IndexedSignature]) -> Result<Vec<u8>, AttachmentError> {
use cesride::{Indexer, Siger, indexer};
let mut out = String::new();
out.push_str("-A");
out.push_str(&encode_count_b64(signatures.len())?);
for sig in signatures {
let (code, ondex) = if sig.prior_index.is_some_and(|j| j != sig.index) {
(indexer::Codex::Ed25519_Big, sig.prior_index)
} else {
(indexer::Codex::Ed25519, None)
};
let siger = Siger::new(
None,
Some(sig.index),
ondex,
Some(code),
Some(&sig.sig),
None,
None,
None,
)
.map_err(|e| AttachmentError::Encode(e.to_string()))?;
let qb64 = siger
.qb64()
.map_err(|e| AttachmentError::Encode(e.to_string()))?;
out.push_str(&qb64);
}
Ok(out.into_bytes())
}
fn indexer_hard_len(first: u8) -> usize {
if first.is_ascii_digit() { 2 } else { 1 }
}
fn indexer_sizage(code: &str) -> Option<(usize, usize)> {
Some(match code {
"A" | "B" | "C" | "D" | "E" | "F" => (88, 0),
"2A" | "2B" | "2C" | "2D" | "2E" | "2F" => (92, 2),
"0A" | "0B" => (156, 1),
"3A" | "3B" => (160, 3),
_ => return None,
})
}
pub fn parse_attachment(bytes: &[u8]) -> Result<Vec<IndexedSignature>, AttachmentError> {
let s = std::str::from_utf8(bytes)
.map_err(|e| AttachmentError::Decode(format!("non-utf8 attachment: {e}")))?;
if s.is_empty() {
return Ok(vec![]);
}
let (sigs, rest) = parse_sig_group(s)?;
if !rest.is_empty() {
return Err(AttachmentError::Decode(format!(
"trailing bytes after signature group: {rest:?}"
)));
}
Ok(sigs)
}
fn parse_sig_group(s: &str) -> Result<(Vec<IndexedSignature>, &str), AttachmentError> {
use cesride::{Indexer, Siger};
let rest = s.strip_prefix("-A").ok_or_else(|| {
AttachmentError::Decode("attachment must start with -A counter code".into())
})?;
if rest.len() < 2 {
return Err(AttachmentError::Decode("truncated counter header".into()));
}
let (count_b64, body) = rest.split_at(2);
let count = decode_count_b64(count_b64)?;
let mut out = Vec::with_capacity(count);
let mut cursor = body;
for _ in 0..count {
let first = *cursor.as_bytes().first().ok_or_else(|| {
AttachmentError::Decode("truncated group: expected another signature".into())
})?;
let hs = indexer_hard_len(first);
if cursor.len() < hs {
return Err(AttachmentError::Decode("truncated siger hard code".into()));
}
let code = &cursor[..hs];
let (fs, os) = indexer_sizage(code)
.ok_or_else(|| AttachmentError::Decode(format!("unknown indexer code {code:?}")))?;
if cursor.len() < fs {
return Err(AttachmentError::Decode(format!(
"insufficient bytes for {code} siger: need {fs}, have {}",
cursor.len()
)));
}
let (siger_qb64, remainder) = cursor.split_at(fs);
cursor = remainder;
let siger = Siger::new(None, None, None, None, None, None, Some(siger_qb64), None)
.map_err(|e| AttachmentError::Decode(format!("Siger: {e}")))?;
let prior_index = (os > 0).then(|| siger.ondex());
out.push(IndexedSignature {
index: siger.index(),
prior_index,
sig: siger.raw(),
});
}
Ok((out, cursor))
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum AttachmentError {
#[error("attachment encode: {0}")]
Encode(String),
#[error("attachment decode: {0}")]
Decode(String),
}
const B64_ALPHA: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
fn encode_count_b64(count: usize) -> Result<String, AttachmentError> {
if count >= 64 * 64 {
return Err(AttachmentError::Encode(format!(
"count {count} exceeds 2-char base64url max (4095)"
)));
}
let hi = B64_ALPHA[(count >> 6) & 0x3f] as char;
let lo = B64_ALPHA[count & 0x3f] as char;
Ok(format!("{hi}{lo}"))
}
fn decode_count_b64(s: &str) -> Result<usize, AttachmentError> {
let mut it = s.chars();
let hi = it
.next()
.and_then(b64_index)
.ok_or_else(|| AttachmentError::Decode(format!("invalid count hi char: {s:?}")))?;
let lo = it
.next()
.and_then(b64_index)
.ok_or_else(|| AttachmentError::Decode(format!("invalid count lo char: {s:?}")))?;
Ok((hi << 6) | lo)
}
fn b64_index(c: char) -> Option<usize> {
B64_ALPHA.iter().position(|&b| b as char == c)
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use super::*;
#[test]
fn attachment_roundtrip_single_sig() {
let sigs = vec![IndexedSignature {
index: 0,
prior_index: None,
sig: vec![0x42u8; 64],
}];
let bytes = serialize_attachment(&sigs).unwrap();
let s = std::str::from_utf8(&bytes).unwrap();
assert!(s.starts_with("-AAB"), "expected -AAB prefix, got {s:?}");
let back = parse_attachment(&bytes).unwrap();
assert_eq!(back.len(), 1);
assert_eq!(back[0].index, 0);
assert_eq!(back[0].sig, vec![0x42u8; 64]);
}
#[test]
fn attachment_roundtrip_three_sigs() {
let sigs = vec![
IndexedSignature {
index: 0,
prior_index: None,
sig: vec![0x01u8; 64],
},
IndexedSignature {
index: 1,
prior_index: None,
sig: vec![0x02u8; 64],
},
IndexedSignature {
index: 2,
prior_index: None,
sig: vec![0x03u8; 64],
},
];
let bytes = serialize_attachment(&sigs).unwrap();
let s = std::str::from_utf8(&bytes).unwrap();
assert!(s.starts_with("-AAD"), "expected -AAD prefix, got {s:?}");
let back = parse_attachment(&bytes).unwrap();
assert_eq!(back.len(), 3);
for (i, sig) in back.iter().enumerate() {
assert_eq!(sig.index, i as u32);
}
}
#[test]
fn attachment_empty() {
let bytes = serialize_attachment(&[]).unwrap();
assert_eq!(bytes, b"-AAA");
let back = parse_attachment(&bytes).unwrap();
assert!(back.is_empty());
}
#[test]
fn keri_sequence_serializes_as_hex() {
assert_eq!(
serde_json::to_string(&KeriSequence::new(0)).unwrap(),
"\"0\""
);
assert_eq!(
serde_json::to_string(&KeriSequence::new(10)).unwrap(),
"\"a\""
);
assert_eq!(
serde_json::to_string(&KeriSequence::new(255)).unwrap(),
"\"ff\""
);
}
#[test]
fn keri_sequence_deserializes_from_hex() {
let s: KeriSequence = serde_json::from_str("\"0\"").unwrap();
assert_eq!(s.value(), 0);
let s: KeriSequence = serde_json::from_str("\"a\"").unwrap();
assert_eq!(s.value(), 10);
let s: KeriSequence = serde_json::from_str("\"ff\"").unwrap();
assert_eq!(s.value(), 255);
}
#[test]
fn keri_sequence_rejects_invalid_hex() {
assert!(serde_json::from_str::<KeriSequence>("\"not_hex\"").is_err());
}
#[test]
fn icp_event_always_serializes_d_a_c() {
use crate::types::{CesrKey, Threshold, VersionString};
let icp = IcpEvent {
v: VersionString::placeholder(),
d: Said::default(),
i: Prefix::new_unchecked("ETest123".to_string()),
s: KeriSequence::new(0),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked("DKey123".to_string())],
nt: Threshold::Simple(1),
n: vec![Said::new_unchecked("ENext456".to_string())],
bt: Threshold::Simple(0),
b: vec![],
c: vec![],
a: vec![],
};
let json = serde_json::to_string(&icp).unwrap();
assert!(json.contains("\"d\":"), "d must always be present");
assert!(json.contains("\"a\":"), "a must always be present");
assert!(json.contains("\"c\":"), "c must always be present");
assert!(!json.contains("\"x\":"), "empty x must be omitted");
assert!(json.contains("\"s\":\"0\""), "s must serialize as hex");
}
#[test]
fn event_enum_deserializes_by_t_field() {
let json = r#"{"v":"KERI10JSON000000_","t":"icp","d":"ETestSaid","i":"E123","s":"0","kt":"1","k":["DKey"],"nt":"1","n":["ENext"],"bt":"0","b":[]}"#;
let event: Event = serde_json::from_str(json).unwrap();
assert!(event.is_inception());
assert_eq!(event.sequence().value(), 0);
}
#[test]
fn digest_seal_roundtrips() {
let seal = Seal::digest("EDigest123");
let json = serde_json::to_string(&seal).unwrap();
assert_eq!(json, r#"{"d":"EDigest123"}"#);
let parsed: Seal = serde_json::from_str(&json).unwrap();
assert_eq!(seal, parsed);
}
#[test]
fn seal_event_location_roundtrips() {
let seal = Seal::EventLocation {
i: Prefix::new_unchecked("EPrefix".to_string()),
s: KeriSequence::new(3),
p: Said::new_unchecked("EPrior".to_string()),
t: "ixn".to_string(),
d: Said::new_unchecked("EDigest".to_string()),
};
let json = serde_json::to_string(&seal).unwrap();
let parsed: Seal = serde_json::from_str(&json).unwrap();
assert_eq!(seal, parsed);
assert_eq!(parsed.digest_value().map(|d| d.as_str()), Some("EDigest"));
}
#[test]
fn seal_rejects_malformed_i_s() {
let r: Result<Seal, _> = serde_json::from_str(r#"{"i":"EPrefix","s":"3"}"#);
assert!(r.is_err(), "malformed {{i,s}} seal must be rejected");
let r2: Result<Seal, _> = serde_json::from_str(r#"{"zz":"x"}"#);
assert!(r2.is_err());
}
#[test]
fn key_event_seal_roundtrips() {
let seal = Seal::key_event(
Prefix::new_unchecked("EPrefix".to_string()),
KeriSequence::new(1),
Said::new_unchecked("ESaid".to_string()),
);
let json = serde_json::to_string(&seal).unwrap();
let parsed: Seal = serde_json::from_str(&json).unwrap();
assert_eq!(seal, parsed);
}
#[test]
fn indexed_signature_serde_roundtrip() {
let sig = IndexedSignature {
index: 2,
prior_index: None,
sig: vec![0xab; 64],
};
let json = serde_json::to_string(&sig).unwrap();
assert!(json.contains("\"index\":2"));
assert!(
!json.contains("prior_index"),
"a None prior_index must be omitted from the wire: {json}"
);
let parsed: IndexedSignature = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, sig);
}
#[test]
fn signed_event_accessors() {
use crate::types::{CesrKey, Threshold, VersionString};
let icp = IcpEvent {
v: VersionString::placeholder(),
d: Said::new_unchecked("ESAID123".to_string()),
i: Prefix::new_unchecked("EPrefix".to_string()),
s: KeriSequence::new(0),
kt: Threshold::Simple(1),
k: vec![CesrKey::new_unchecked("DKey".to_string())],
nt: Threshold::Simple(1),
n: vec![Said::new_unchecked("ENext".to_string())],
bt: Threshold::Simple(0),
b: vec![],
c: vec![],
a: vec![],
};
let signed = SignedEvent::new(
Event::Icp(icp),
vec![IndexedSignature {
index: 0,
prior_index: None,
sig: vec![0u8; 64],
}],
);
assert_eq!(signed.said().as_str(), "ESAID123");
assert_eq!(signed.sequence().value(), 0);
assert_eq!(signed.prefix().as_str(), "EPrefix");
assert_eq!(signed.signatures.len(), 1);
assert_eq!(signed.signatures[0].index, 0);
}
#[test]
fn seal_digest_value() {
let seal = Seal::digest("ETest123");
assert_eq!(seal.digest_value().unwrap().as_str(), "ETest123");
let latest = Seal::LatestEstablishment {
i: Prefix::new_unchecked("EPrefix".to_string()),
};
assert!(latest.digest_value().is_none());
}
}