use auths_keri::{Event, Prefix, Seal, verify_event_said};
use serde::Serialize;
use super::bundle::{AirGappedOrgBundle, BundledKel};
use super::error::OrgBundleError;
use super::record::find_revocation_event;
use crate::duplicity::{KelEventRef, detect_duplicity};
use crate::types::IdentityDID;
pub const MAX_BUNDLE_JSON_BYTES: usize = 16 * 1024 * 1024;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
#[serde(tag = "authority_at_signing", rename_all = "snake_case")]
pub enum AuthorityAtSigning {
AuthorizedBeforeRevocation,
RejectedAfterRevocation {
#[serde(with = "u128_str")]
revoked_at: u128,
},
RejectedRevokedPositionUnknown {
#[serde(with = "u128_str")]
revoked_at: u128,
},
NeverDelegated,
}
mod u128_str {
use serde::{Deserialize, Deserializer, Serializer};
pub fn serialize<S: Serializer>(value: &u128, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&value.to_string())
}
pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<u128, D::Error> {
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct OfflineVerifyReport {
pub org_did: IdentityDID,
pub as_of_org_seq: u128,
pub root_pinned: bool,
pub duplicity_detected: bool,
pub authority: Option<AuthorityAtSigning>,
}
fn check_kel_integrity(kel: &BundledKel) -> Result<(), OrgBundleError> {
for event in &kel.events {
verify_event_said(event).map_err(|e| OrgBundleError::Integrity {
id: kel.prefix.as_str().to_string(),
reason: e.to_string(),
})?;
}
Ok(())
}
pub fn authenticate_bundled_kel(
kel: &BundledKel,
lookup: Option<&dyn auths_keri::DelegatorKelLookup>,
) -> Result<auths_keri::KeyState, OrgBundleError> {
let integrity_err = |reason: String| OrgBundleError::Integrity {
id: kel.prefix.as_str().to_string(),
reason,
};
if kel.attachments.len() != kel.events.len() {
return Err(integrity_err(format!(
"{} events but {} signature attachments — cannot authenticate",
kel.events.len(),
kel.attachments.len()
)));
}
let mut signed = Vec::with_capacity(kel.events.len());
for (event, att_hex) in kel.events.iter().zip(kel.attachments.iter()) {
let bytes =
hex::decode(att_hex).map_err(|e| integrity_err(format!("non-hex attachment: {e}")))?;
let (sigs, seals) = auths_keri::parse_delegated_attachment(&bytes)
.map_err(|e| integrity_err(format!("unparseable attachment: {e}")))?;
let event = rehydrate_event_source_seal(event.clone(), seals.into_iter().next());
signed.push(auths_keri::SignedEvent::new(event, sigs));
}
auths_keri::validate_signed_kel(&signed, lookup)
.map_err(|e| integrity_err(format!("KEL signature authentication failed (RT-002): {e}")))
}
fn rehydrate_event_source_seal(event: Event, seal: Option<auths_keri::SourceSeal>) -> Event {
let Some(seal) = seal else {
return event;
};
match event {
Event::Dip(mut e) => {
e.source_seal = Some(seal);
Event::Dip(e)
}
Event::Drt(mut e) => {
e.source_seal = Some(seal);
Event::Drt(e)
}
other => other,
}
}
fn is_delegated(org_kel: &[Event], member_prefix: &Prefix) -> bool {
org_kel.iter().any(|event| {
event.anchors().iter().any(
|seal| matches!(seal, Seal::KeyEvent { i, .. } if i.as_str() == member_prefix.as_str()),
)
})
}
pub fn classify_authority_in_bundle(
bundle: &AirGappedOrgBundle,
member_prefix: &Prefix,
signed_at: Option<u128>,
) -> AuthorityAtSigning {
classify_from_bundle(&bundle.org_kel.events, member_prefix, signed_at)
}
fn classify_from_bundle(
org_kel: &[Event],
member_prefix: &Prefix,
signed_at: Option<u128>,
) -> AuthorityAtSigning {
if !is_delegated(org_kel, member_prefix) {
return AuthorityAtSigning::NeverDelegated;
}
match find_revocation_event(org_kel, member_prefix) {
None => AuthorityAtSigning::AuthorizedBeforeRevocation,
Some((_, revoked_at)) => match signed_at {
None => AuthorityAtSigning::RejectedRevokedPositionUnknown { revoked_at },
Some(seq) if seq < revoked_at => AuthorityAtSigning::AuthorizedBeforeRevocation,
Some(_) => AuthorityAtSigning::RejectedAfterRevocation { revoked_at },
},
}
}
pub fn verify_org_bundle(
bundle: &AirGappedOrgBundle,
pinned_roots: &[IdentityDID],
query: Option<(&Prefix, Option<u128>)>,
) -> Result<OfflineVerifyReport, OrgBundleError> {
check_kel_integrity(&bundle.org_kel)?;
authenticate_bundled_kel(&bundle.org_kel, None)?;
let org_lookup = auths_keri::KelSealIndex::from_events(&bundle.org_kel.events);
for member_kel in &bundle.member_kels {
check_kel_integrity(member_kel)?;
authenticate_bundled_kel(member_kel, Some(&org_lookup))?;
}
for event in &bundle.org_kel.events {
for seal in event.anchors() {
if let Seal::KeyEvent { i, .. } = seal {
let present = bundle
.member_kels
.iter()
.any(|m| m.prefix.as_str() == i.as_str());
if !present {
return Err(OrgBundleError::MissingMemberKel {
member: format!("did:keri:{}", i.as_str()),
});
}
}
}
}
let root_pinned = pinned_roots
.iter()
.any(|r| r.as_str() == bundle.org_did.as_str());
let org_did_str = bundle.org_did.as_str().to_string();
let refs: Vec<KelEventRef<'_>> = bundle
.org_kel
.events
.iter()
.map(|e| KelEventRef {
prefix: org_did_str.as_str(),
seq: e.sequence().value() as u64,
said: e.said().as_str(),
})
.collect();
let duplicity_detected = detect_duplicity(&refs).is_diverging();
let authority = match query {
Some((member_prefix, signed_at)) => {
if !is_delegated(&bundle.org_kel.events, member_prefix) {
return Err(OrgBundleError::MissingDelegatorSeal {
member: format!("did:keri:{}", member_prefix.as_str()),
});
}
Some(classify_from_bundle(
&bundle.org_kel.events,
member_prefix,
signed_at,
))
}
None => None,
};
Ok(OfflineVerifyReport {
org_did: bundle.org_did.clone(),
as_of_org_seq: bundle.built_at_org_seq,
root_pinned,
duplicity_detected,
authority,
})
}
#[derive(Serialize)]
#[serde(tag = "kind", rename_all = "camelCase")]
enum BundleVerdictJson {
#[serde(rename = "report")]
Report {
report: OfflineVerifyReport,
},
#[serde(rename = "error")]
Error {
code: String,
message: String,
},
}
const SERIALIZE_FALLBACK: &str =
r#"{"kind":"error","code":"AUTHS-E2204","message":"verdict serialization failed"}"#;
fn envelope_to_string(envelope: &BundleVerdictJson) -> String {
serde_json::to_string(envelope).unwrap_or_else(|_| SERIALIZE_FALLBACK.to_string())
}
fn error_envelope(e: &OrgBundleError) -> String {
use auths_crypto::AuthsErrorInfo;
envelope_to_string(&BundleVerdictJson::Error {
code: e.error_code().to_string(),
message: e.to_string(),
})
}
pub fn verify_org_bundle_json(
bundle_json: &str,
pinned_roots_json: &str,
member_did: Option<&str>,
signed_at: Option<&str>,
) -> String {
match verify_org_bundle_json_inner(bundle_json, pinned_roots_json, member_did, signed_at) {
Ok(report) => envelope_to_string(&BundleVerdictJson::Report { report }),
Err(e) => error_envelope(&e),
}
}
fn verify_org_bundle_json_inner(
bundle_json: &str,
pinned_roots_json: &str,
member_did: Option<&str>,
signed_at: Option<&str>,
) -> Result<OfflineVerifyReport, OrgBundleError> {
if bundle_json.len() > MAX_BUNDLE_JSON_BYTES {
return Err(OrgBundleError::Parse(format!(
"bundle JSON too large: {} bytes, max {}",
bundle_json.len(),
MAX_BUNDLE_JSON_BYTES
)));
}
let bundle = AirGappedOrgBundle::from_json(bundle_json)?;
let pinned_roots: Vec<IdentityDID> = serde_json::from_str(pinned_roots_json)
.map_err(|e| OrgBundleError::Parse(format!("pinned roots: {e}")))?;
let member_prefix = member_did
.map(|m| {
Prefix::new(m.strip_prefix("did:keri:").unwrap_or(m).to_string())
.map_err(|e| OrgBundleError::Parse(format!("member prefix: {e}")))
})
.transpose()?;
let signed_at = signed_at
.map(|s| {
s.parse::<u128>()
.map_err(|e| OrgBundleError::Parse(format!("signed_at: {e}")))
})
.transpose()?;
let query = member_prefix.as_ref().map(|p| (p, signed_at));
verify_org_bundle(&bundle, &pinned_roots, query)
}