use crate::core::matter::code::CesrCode;
use crate::core::matter::code::DigestCode;
use crate::core::matter::sizage::SizeType;
use crate::core::primitives::Saider;
use crate::crypto::digest::digest;
#[cfg(feature = "alloc")]
#[allow(
unused_imports,
reason = "alloc prelude items; subset used per cfg/feature combination"
)]
use alloc::{borrow::ToOwned, string::String, string::ToString, vec::Vec};
use core::ops::Range;
use crate::serder::deserialize::canonical::{ParsedDip, ParsedEvent, Spanned, parse_event};
use crate::serder::error::SerderError;
use crate::serder::primitives::to_qb64_string;
pub const DUMMY_CHAR: char = '#';
pub(crate) const DUMMY_BYTE: u8 = b'#';
pub fn said_placeholder(code: DigestCode) -> Result<String, SerderError> {
let sizage = code.get_sizage();
let len = match *sizage.fs() {
SizeType::Fixed(n) => usize::from(n),
SizeType::Small | SizeType::Large => {
return Err(SerderError::DigestError(
"digest code has variable size, expected fixed".to_owned(),
));
}
};
Ok(core::iter::repeat_n(DUMMY_CHAR, len).collect())
}
pub fn compute_digest(data: &[u8], code: DigestCode) -> Result<Saider<'static>, SerderError> {
digest(code, data).map_err(|e| SerderError::DigestError(e.to_string()))
}
pub fn verify_said(raw: &[u8], code: DigestCode) -> Result<(), SerderError> {
match parse_event(raw)? {
ParsedEvent::Inception(p) => {
let prefix = (p.said.value == p.prefix.value).then_some(&p.prefix);
verify_said_spans(raw, &p.said, prefix, code)
}
ParsedEvent::DelegatedInception(ParsedDip { icp, .. }) => {
let prefix = (icp.said.value == icp.prefix.value).then_some(&icp.prefix);
verify_said_spans(raw, &icp.said, prefix, code)
}
ParsedEvent::Rotation(p) | ParsedEvent::DelegatedRotation(p) => {
verify_said_spans(raw, &p.said, None, code)
}
ParsedEvent::Interaction(p) => verify_said_spans(raw, &p.said, None, code),
}
}
pub(crate) fn verify_said_spans(
raw: &[u8],
said: &Spanned<'_>,
prefix: Option<&Spanned<'_>>,
code: DigestCode,
) -> Result<(), SerderError> {
let mut scratch = raw.to_vec();
fill_span(&mut scratch, &said.span)?;
if let Some(p) = prefix {
fill_span(&mut scratch, &p.span)?;
}
let computed = compute_digest(&scratch, code)?;
let computed_qb64 = to_qb64_string(&computed);
if said.value == computed_qb64 {
Ok(())
} else {
Err(SerderError::SaidMismatch {
expected: said.value.to_owned(),
computed: computed_qb64,
})
}
}
fn fill_span(scratch: &mut [u8], span: &Range<usize>) -> Result<(), SerderError> {
scratch
.get_mut(span.clone())
.ok_or(SerderError::InvalidEventLayout("SAID span out of bounds"))?
.fill(DUMMY_BYTE);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::matter::builder::MatterBuilder;
use crate::core::matter::code::{DigestCode, VerKeyCode};
use crate::core::primitives::Seqner;
use crate::keri::InteractionEvent;
use crate::serder::builder::icp::InceptionBuilder;
use crate::serder::serialize::serialize_interaction;
use alloc::borrow::Cow;
use alloc::vec;
use alloc::vec::Vec;
#[test]
fn placeholder_blake3_256_is_44_chars() {
let ph = said_placeholder(DigestCode::Blake3_256).expect("fixed-size code");
assert_eq!(ph.len(), 44);
assert!(ph.chars().all(|c| c == DUMMY_CHAR));
}
#[test]
fn placeholder_sha3_256_is_44_chars() {
let ph = said_placeholder(DigestCode::SHA3_256).expect("fixed-size code");
assert_eq!(ph.len(), 44);
assert!(ph.chars().all(|c| c == DUMMY_CHAR));
}
#[test]
fn compute_digest_produces_valid_qb64() {
let data = b"hello KERI world";
let saider = compute_digest(data, DigestCode::Blake3_256).expect("digest should succeed");
let qb64 = to_qb64_string(&saider);
assert_eq!(qb64.len(), 44);
assert!(
qb64.starts_with('E'),
"Blake3_256 qb64 should start with 'E'"
);
}
#[test]
fn compute_digest_deterministic() {
let data = b"deterministic input";
let a = compute_digest(data, DigestCode::Blake3_256).expect("digest a");
let b = compute_digest(data, DigestCode::Blake3_256).expect("digest b");
assert_eq!(to_qb64_string(&a), to_qb64_string(&b));
}
#[test]
fn different_data_different_said() {
let a = compute_digest(b"alpha", DigestCode::Blake3_256).expect("digest alpha");
let b = compute_digest(b"bravo", DigestCode::Blake3_256).expect("digest bravo");
assert_ne!(to_qb64_string(&a), to_qb64_string(&b));
}
fn probe_ixn_raw() -> (Vec<u8>, String) {
let prefixer = MatterBuilder::new()
.with_code(VerKeyCode::Ed25519)
.with_raw(Cow::<[u8]>::Owned(vec![0u8; 32]))
.unwrap()
.build()
.unwrap();
let saider_fixture = compute_digest(b"seed", DigestCode::Blake3_256).unwrap();
let event = InteractionEvent::new(
prefixer.into(),
Seqner::new(1),
saider_fixture.clone(),
saider_fixture,
vec![],
);
let ser = serialize_interaction(&event).unwrap();
let said = to_qb64_string(ser.said());
(ser.as_bytes().to_vec(), said)
}
#[test]
fn verify_said_spans_accepts_writer_output() {
let (raw, said) = probe_ixn_raw();
let start = raw
.windows(6)
.position(|w| w == b"\"d\":\"E")
.expect("d field present")
+ 5;
let span = start..start + 44;
assert_eq!(&raw[span.clone()], said.as_bytes());
let spanned = Spanned { value: &said, span };
verify_said_spans(&raw, &spanned, None, DigestCode::Blake3_256)
.expect("writer output must verify");
}
#[test]
fn verify_said_spans_rejects_tamper() {
let (mut raw, said) = probe_ixn_raw();
let start = raw.windows(6).position(|w| w == b"\"d\":\"E").unwrap() + 5;
let span = start..start + 44;
let s_pos = raw.windows(8).position(|w| w == b",\"s\":\"1\"").unwrap();
raw[s_pos + 6] = b'2';
let spanned = Spanned { value: &said, span };
assert!(matches!(
verify_said_spans(&raw, &spanned, None, DigestCode::Blake3_256),
Err(SerderError::SaidMismatch { .. })
));
}
#[test]
fn verify_said_spans_rejects_out_of_bounds_span() {
let (raw, said) = probe_ixn_raw();
let bogus = Spanned {
value: &said,
span: raw.len()..raw.len() + 44,
};
assert!(matches!(
verify_said_spans(&raw, &bogus, None, DigestCode::Blake3_256),
Err(SerderError::InvalidEventLayout(_))
));
}
#[test]
fn verify_said_spans_wrong_width_span_is_said_mismatch() {
let (raw, said) = probe_ixn_raw();
let start = raw.windows(6).position(|w| w == b"\"d\":\"E").unwrap() + 5;
let short = Spanned {
value: &said,
span: start..start + 43,
};
assert!(matches!(
verify_said_spans(&raw, &short, None, DigestCode::Blake3_256),
Err(SerderError::SaidMismatch { .. })
));
}
#[test]
fn verify_said_spans_double_said_matches_reference() {
let verfer = MatterBuilder::new()
.with_code(VerKeyCode::Ed25519)
.with_raw(Cow::<[u8]>::Owned(vec![7u8; 32]))
.unwrap()
.build()
.unwrap();
let icp = InceptionBuilder::new().keys(vec![verfer]).build().unwrap();
let raw = icp.as_bytes().to_vec();
let said = to_qb64_string(icp.said());
let d_start = raw.windows(5).position(|w| w == b"\"d\":\"").unwrap() + 5;
let i_start = raw.windows(5).position(|w| w == b"\"i\":\"").unwrap() + 5;
let d_span = d_start..d_start + 44;
let i_span = i_start..i_start + 44;
assert_eq!(&raw[d_span.clone()], said.as_bytes());
assert_eq!(&raw[i_span.clone()], said.as_bytes());
let d_spanned = Spanned {
value: &said,
span: d_span,
};
let i_spanned = Spanned {
value: &said,
span: i_span,
};
verify_said_spans(&raw, &d_spanned, Some(&i_spanned), DigestCode::Blake3_256)
.expect("double-SAID writer output must verify by span");
}
#[test]
fn verify_said_accepts_serialized_event() {
let (raw, _) = probe_ixn_raw();
verify_said(&raw, DigestCode::Blake3_256).expect("writer output must verify");
}
#[test]
fn verify_said_rejects_tampered_event() {
let (mut raw, _) = probe_ixn_raw();
let s_pos = raw.windows(8).position(|w| w == b",\"s\":\"1\"").unwrap();
raw[s_pos + 6] = b'2';
assert!(matches!(
verify_said(&raw, DigestCode::Blake3_256),
Err(SerderError::SaidMismatch { .. })
));
}
#[test]
fn verify_said_wrong_code_is_said_mismatch() {
let (raw, _) = probe_ixn_raw();
assert!(matches!(
verify_said(&raw, DigestCode::SHA3_256),
Err(SerderError::SaidMismatch { .. })
));
}
#[test]
fn verify_said_rejects_non_canonical_input() {
assert!(matches!(
verify_said(b"not an event", DigestCode::Blake3_256),
Err(SerderError::NonCanonical { .. } | SerderError::InvalidVersionString(_))
));
}
#[test]
fn verify_said_double_said_inception_verifies() {
let verfer = MatterBuilder::new()
.with_code(VerKeyCode::Ed25519)
.with_raw(Cow::<[u8]>::Owned(vec![7u8; 32]))
.unwrap()
.build()
.unwrap();
let icp = InceptionBuilder::new().keys(vec![verfer]).build().unwrap();
verify_said(icp.as_bytes(), DigestCode::Blake3_256)
.expect("double-SAID inception must verify through the strict path");
}
}