use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use auths_crypto::{CurveType, SecureSeed, TypedSignerKey};
use auths_keri::{Prefix, Said};
use auths_verifier::types::CanonicalDid;
use axum::{
Json, Router,
extract::{Path as AxumPath, State},
http::StatusCode,
routing::{get, post},
};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
use auths_keri::{KeriSequence, KeyStateRecord, TrustedKel, VersionString, parse_kel_json};
use super::error::{DuplicityEvidence, WitnessError};
use super::receipt::{Receipt, ReceiptTag, SignedReceipt};
use super::storage::WitnessStorage;
#[derive(Clone)]
pub struct WitnessServerState {
inner: Arc<WitnessServerInner>,
}
#[allow(dead_code)]
struct WitnessServerInner {
witness_did: CanonicalDid,
signer: TypedSignerKey,
storage: Mutex<WitnessStorage>,
clock: Box<dyn Fn() -> DateTime<Utc> + Send + Sync>,
build_proof: Option<BuildProof>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuildProof {
pub version: String,
pub running_digest: String,
pub attestation: serde_json::Value,
}
impl BuildProof {
pub fn measure_self(
version: impl Into<String>,
attestation: serde_json::Value,
) -> std::io::Result<Self> {
let exe = std::env::current_exe()?;
let running_digest = sha256_file_hex(&exe)?;
Ok(Self {
version: version.into(),
running_digest,
attestation,
})
}
}
fn sha256_file_hex(path: &std::path::Path) -> std::io::Result<String> {
use sha2::{Digest, Sha256};
let bytes = std::fs::read(path)?;
Ok(hex::encode(Sha256::digest(&bytes)))
}
pub struct WitnessServerConfig {
pub witness_did: CanonicalDid,
pub signer: TypedSignerKey,
pub db_path: std::path::PathBuf,
pub tls_cert_path: Option<PathBuf>,
pub tls_key_path: Option<PathBuf>,
pub build_proof: Option<BuildProof>,
}
impl WitnessServerConfig {
pub fn with_generated_keypair(
db_path: std::path::PathBuf,
curve: CurveType,
) -> Result<Self, WitnessError> {
let (seed_bytes, pubkey_bytes) = generate_keypair_for_curve(curve)?;
let typed_seed = match curve {
CurveType::Ed25519 => auths_crypto::TypedSeed::Ed25519(seed_bytes),
CurveType::P256 => auths_crypto::TypedSeed::P256(seed_bytes),
};
let signer = TypedSignerKey::from_parts(typed_seed, pubkey_bytes)
.map_err(|e| WitnessError::Network(format!("invalid witness signer: {e}")))?;
Self::from_signer(db_path, signer)
}
pub fn from_signer(
db_path: std::path::PathBuf,
signer: TypedSignerKey,
) -> Result<Self, WitnessError> {
let witness_did = derive_witness_did(signer.curve(), signer.public_key())?;
Ok(Self {
witness_did,
signer,
db_path,
tls_cert_path: None,
tls_key_path: None,
build_proof: None,
})
}
pub fn with_build_proof(mut self, proof: BuildProof) -> Self {
self.build_proof = Some(proof);
self
}
}
pub(crate) fn generate_keypair_for_curve(
curve: CurveType,
) -> Result<([u8; 32], Vec<u8>), WitnessError> {
match curve {
CurveType::Ed25519 => {
use crate::crypto::provider_bridge;
let (seed, pubkey) = provider_bridge::generate_ed25519_keypair_sync()
.map_err(|e| WitnessError::Network(format!("Ed25519 keygen: {e}")))?;
Ok((*seed.as_bytes(), pubkey.to_vec()))
}
CurveType::P256 => {
use p256::ecdsa::{SigningKey, VerifyingKey};
use p256::elliptic_curve::rand_core::OsRng;
let sk = SigningKey::random(&mut OsRng);
let mut scalar = [0u8; 32];
scalar.copy_from_slice(&sk.to_bytes());
let vk = VerifyingKey::from(&sk);
let pubkey = vk.to_encoded_point(true).as_bytes().to_vec();
Ok((scalar, pubkey))
}
}
}
fn derive_witness_did(curve: CurveType, pubkey_bytes: &[u8]) -> Result<CanonicalDid, WitnessError> {
Ok(CanonicalDid::from_public_key_did_key(pubkey_bytes, curve))
}
#[derive(Debug, Deserialize)]
pub struct SubmitEventRequest {
pub d: String,
pub s: u64,
pub t: String,
#[serde(flatten)]
pub extra: serde_json::Value,
}
#[derive(Debug, Serialize)]
pub struct HealthResponse {
pub status: String,
pub witness_did: CanonicalDid,
pub first_seen_count: usize,
pub receipt_count: usize,
}
#[derive(Debug, Serialize)]
pub struct HeadResponse {
pub prefix: Prefix,
pub latest_seq: Option<u64>,
}
#[derive(Debug, Serialize)]
pub struct SaidAtSeqResponse {
pub prefix: Prefix,
pub seq: u64,
pub said: Said,
}
#[derive(Debug, Serialize)]
pub struct ErrorResponse {
pub error: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub duplicity: Option<DuplicityEvidence>,
}
#[allow(dead_code)]
impl WitnessServerState {
#[allow(clippy::disallowed_methods)] pub fn new(config: WitnessServerConfig) -> Result<Self, WitnessError> {
let storage = WitnessStorage::open(&config.db_path)?;
Ok(Self {
inner: Arc::new(WitnessServerInner {
witness_did: config.witness_did,
signer: config.signer,
storage: Mutex::new(storage),
clock: Box::new(Utc::now),
build_proof: config.build_proof,
}),
})
}
#[allow(clippy::disallowed_methods)] pub fn in_memory(
witness_did: CanonicalDid,
signer: TypedSignerKey,
) -> Result<Self, WitnessError> {
let storage = WitnessStorage::in_memory()?;
Ok(Self {
inner: Arc::new(WitnessServerInner {
witness_did,
signer,
storage: Mutex::new(storage),
clock: Box::new(Utc::now),
build_proof: None,
}),
})
}
#[allow(clippy::disallowed_methods)]
pub fn in_memory_ed25519(
witness_did: CanonicalDid,
seed: SecureSeed,
public_key: [u8; 32],
) -> Result<Self, WitnessError> {
let typed_seed = auths_crypto::TypedSeed::Ed25519(*seed.as_bytes());
let signer = TypedSignerKey::from_parts(typed_seed, public_key.to_vec())
.map_err(|e| WitnessError::Network(format!("invalid witness signer: {e}")))?;
Self::in_memory(witness_did, signer)
}
#[allow(clippy::disallowed_methods)] pub fn in_memory_generated() -> Result<Self, WitnessError> {
Self::in_memory_generated_for_curve(CurveType::Ed25519)
}
#[allow(clippy::disallowed_methods)]
pub fn in_memory_generated_for_curve(curve: CurveType) -> Result<Self, WitnessError> {
let (seed_bytes, pubkey_bytes) = generate_keypair_for_curve(curve)?;
let typed_seed = match curve {
CurveType::Ed25519 => auths_crypto::TypedSeed::Ed25519(seed_bytes),
CurveType::P256 => auths_crypto::TypedSeed::P256(seed_bytes),
};
let signer = TypedSignerKey::from_parts(typed_seed, pubkey_bytes.clone())
.map_err(|e| WitnessError::Network(format!("invalid witness signer: {e}")))?;
let witness_did = derive_witness_did(curve, &pubkey_bytes)?;
let storage = WitnessStorage::in_memory()?;
Ok(Self {
inner: Arc::new(WitnessServerInner {
witness_did,
signer,
storage: Mutex::new(storage),
clock: Box::new(Utc::now),
build_proof: None,
}),
})
}
pub fn witness_did(&self) -> &str {
&self.inner.witness_did
}
fn create_receipt(
&self,
prefix: &Prefix,
seq: u128,
event_said: &Said,
) -> Result<Receipt, WitnessError> {
let receipt = Receipt {
v: VersionString::placeholder(),
t: ReceiptTag,
d: event_said.clone(),
i: prefix.clone(),
s: KeriSequence::new(seq),
};
Ok(receipt)
}
fn create_signed_receipt(
&self,
prefix: &Prefix,
seq: u128,
event_said: &Said,
) -> Result<SignedReceipt, WitnessError> {
let receipt = self.create_receipt(prefix, seq, event_said)?;
let signing_payload =
serde_json::to_vec(&receipt).map_err(|e| WitnessError::Serialization(e.to_string()))?;
let signature = self.sign_payload(&signing_payload)?;
Ok(SignedReceipt { receipt, signature })
}
fn sign_payload(&self, payload: &[u8]) -> Result<Vec<u8>, WitnessError> {
self.inner
.signer
.sign(payload)
.map_err(|e| WitnessError::Serialization(format!("signing failed: {e}")))
}
pub fn public_key(&self) -> Vec<u8> {
self.inner.signer.public_key().to_vec()
}
pub fn curve(&self) -> CurveType {
self.inner.signer.curve()
}
}
pub fn router(state: WitnessServerState) -> Router {
Router::new()
.route("/witness/{prefix}/event", post(submit_event))
.route("/witness/{prefix}/head", get(get_head))
.route("/witness/{prefix}/said/{seq}", get(get_said_at_seq))
.route("/witness/{prefix}/receipt/{said}", get(get_receipt))
.route("/witness/{prefix}/key-state", get(get_key_state))
.route("/build", get(get_build))
.route("/health", get(health))
.with_state(state)
}
pub async fn run_server(state: WitnessServerState, addr: SocketAddr) -> Result<(), WitnessError> {
let app = router(state);
let listener = tokio::net::TcpListener::bind(addr)
.await
.map_err(|e| WitnessError::Network(format!("failed to bind: {}", e)))?;
axum::serve(listener, app)
.await
.map_err(|e| WitnessError::Network(format!("server error: {}", e)))?;
Ok(())
}
#[cfg(feature = "tls")]
#[allow(dead_code)] pub async fn run_server_tls(
state: WitnessServerState,
addr: SocketAddr,
cert_path: &std::path::Path,
key_path: &std::path::Path,
) -> Result<(), WitnessError> {
let app = router(state);
let tls_config = axum_server::tls_rustls::RustlsConfig::from_pem_file(cert_path, key_path)
.await
.map_err(|e| WitnessError::Network(format!("failed to load TLS config: {}", e)))?;
axum_server::bind_rustls(addr, tls_config)
.serve(app.into_make_service())
.await
.map_err(|e| WitnessError::Network(format!("TLS server error: {}", e)))?;
Ok(())
}
fn verify_event_said(event: &serde_json::Value) -> Result<(), String> {
let claimed_d = event
.get("d")
.and_then(|v| v.as_str())
.ok_or("missing 'd' (SAID) field")?;
let computed = crate::crypto::said::compute_said(event)
.map_err(|e| format!("failed to compute SAID: {}", e))?;
if computed.as_str() != claimed_d {
return Err(format!(
"SAID mismatch: claimed {} but computed {}",
claimed_d, computed
));
}
Ok(())
}
fn validate_event_structure(event: &serde_json::Value) -> Result<(), String> {
let obj = event.as_object().ok_or("event must be a JSON object")?;
for field in &["v", "t", "d", "i", "s"] {
if !obj.contains_key(*field) {
return Err(format!("missing required field '{}'", field));
}
}
let event_type = obj
.get("t")
.and_then(|v| v.as_str())
.ok_or("'t' must be a string")?;
match event_type {
"icp" => {
for field in &["k", "x"] {
if !obj.contains_key(*field) {
return Err(format!(
"inception event missing required field '{}'",
field
));
}
}
}
"rot" | "ixn" => {
if !obj.contains_key("p") {
return Err(format!(
"{} event missing required field 'p' (prior event reference)",
event_type
));
}
}
_ => {
}
}
Ok(())
}
#[derive(Debug, thiserror::Error)]
enum EventSignatureError {
#[error("'x' (signature) must be a string")]
SignatureNotString,
#[error("'x' (signature) is not valid hex: {0}")]
SignatureNotHex(String),
#[error("inception event missing 'x' (signature)")]
SignatureMissing,
#[error("inception event 'k' must be a non-empty array")]
MissingKeys,
#[error("'k[0]' must be a string")]
KeyNotString,
#[error(
"'k[0]' is not a curve-tagged CESR verkey (expected `D…` Ed25519 or \
`1AAI…`/`1AAJ…` P-256); untagged or unknown-curve keys are rejected: {0}"
)]
UnroutableKey(String),
#[error("event is not a JSON object")]
NotAnObject,
#[error("failed to serialize signing payload: {0}")]
PayloadSerialization(String),
#[error("inception self-signature did not verify against k[0]: {0}")]
SelfSignatureInvalid(String),
}
fn validate_signature_format(event: &serde_json::Value) -> Result<(), EventSignatureError> {
if let Some(sig_val) = event.get("x") {
let sig_hex = sig_val
.as_str()
.ok_or(EventSignatureError::SignatureNotString)?;
hex::decode(sig_hex).map_err(|e| EventSignatureError::SignatureNotHex(e.to_string()))?;
}
Ok(())
}
fn verify_inception_self_signature(event: &serde_json::Value) -> Result<(), EventSignatureError> {
let event_type = event.get("t").and_then(|v| v.as_str()).unwrap_or("");
if event_type != "icp" {
return Ok(());
}
let k = event
.get("k")
.and_then(|v| v.as_array())
.filter(|a| !a.is_empty())
.ok_or(EventSignatureError::MissingKeys)?;
let k0_str = k[0].as_str().ok_or(EventSignatureError::KeyNotString)?;
let keri_key = auths_keri::KeriPublicKey::parse(k0_str)
.map_err(|e| EventSignatureError::UnroutableKey(e.to_string()))?;
let sig_hex = event
.get("x")
.and_then(|v| v.as_str())
.ok_or(EventSignatureError::SignatureMissing)?;
let sig_bytes =
hex::decode(sig_hex).map_err(|e| EventSignatureError::SignatureNotHex(e.to_string()))?;
let mut payload_event = event.clone();
let obj = payload_event
.as_object_mut()
.ok_or(EventSignatureError::NotAnObject)?;
obj.insert("d".to_string(), serde_json::Value::String(String::new()));
obj.insert("x".to_string(), serde_json::Value::String(String::new()));
let payload = serde_json::to_vec(&payload_event)
.map_err(|e| EventSignatureError::PayloadSerialization(e.to_string()))?;
keri_key
.verify_signature(&payload, &sig_bytes)
.map_err(EventSignatureError::SelfSignatureInvalid)
}
#[allow(clippy::too_many_lines)]
async fn submit_event(
State(state): State<WitnessServerState>,
AxumPath(prefix_str): AxumPath<String>,
Json(event): Json<serde_json::Value>,
) -> Result<Json<SignedReceipt>, (StatusCode, Json<ErrorResponse>)> {
let prefix = Prefix::new_unchecked(prefix_str);
if let Err(e) = validate_event_structure(&event) {
return Err((
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: format!("invalid event structure: {}", e),
duplicity: None,
}),
));
}
if let Err(e) = verify_event_said(&event) {
return Err((
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: format!("SAID verification failed: {}", e),
duplicity: None,
}),
));
}
if let Err(e) = validate_signature_format(&event) {
return Err((
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: format!("invalid signature format: {}", e),
duplicity: None,
}),
));
}
if let Err(e) = verify_inception_self_signature(&event) {
return Err((
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: format!("signature verification failed: {}", e),
duplicity: None,
}),
));
}
let event_d = Said::new_unchecked(
event
.get("d")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string(),
);
let event_s: u128 = event
.get("s")
.and_then(|v| v.as_str())
.and_then(|s| u128::from_str_radix(s, 16).ok())
.unwrap_or(0);
let now = (state.inner.clock)();
let storage = state.inner.storage.lock().map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "internal lock error".to_string(),
duplicity: None,
}),
)
})?;
match storage.check_duplicity(now, &prefix, event_s, &event_d) {
Ok(None) => {
let signed = state
.create_signed_receipt(&prefix, event_s, &event_d)
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("failed to create receipt: {e}"),
duplicity: None,
}),
)
})?;
if let Err(e) = storage.store_receipt(now, &prefix, &signed.receipt) {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("failed to store receipt: {}", e),
duplicity: None,
}),
));
}
let event_json = serde_json::to_string(&event).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("failed to serialize event for retention: {e}"),
duplicity: None,
}),
)
})?;
if let Err(e) = storage.store_event(now, &prefix, event_s, &event_json) {
return Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("failed to store event: {}", e),
duplicity: None,
}),
));
}
Ok(Json(signed))
}
Ok(Some(existing_said)) => {
Err((
StatusCode::CONFLICT,
Json(ErrorResponse {
error: "duplicity detected".to_string(),
duplicity: Some(DuplicityEvidence {
prefix,
sequence: event_s,
event_a_said: existing_said,
event_b_said: event_d,
witness_reports: vec![],
}),
}),
))
}
Err(e) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("storage error: {}", e),
duplicity: None,
}),
)),
}
}
async fn get_head(
State(state): State<WitnessServerState>,
AxumPath(prefix_str): AxumPath<String>,
) -> Result<Json<HeadResponse>, (StatusCode, Json<ErrorResponse>)> {
let prefix = Prefix::new_unchecked(prefix_str);
let storage = state.inner.storage.lock().map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "internal lock error".to_string(),
duplicity: None,
}),
)
})?;
match storage.get_latest_seq(&prefix) {
Ok(latest_seq) => Ok(Json(HeadResponse { prefix, latest_seq })),
Err(e) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("storage error: {}", e),
duplicity: None,
}),
)),
}
}
async fn get_said_at_seq(
State(state): State<WitnessServerState>,
AxumPath((prefix_str, seq_str)): AxumPath<(String, String)>,
) -> Result<Json<SaidAtSeqResponse>, (StatusCode, Json<ErrorResponse>)> {
let prefix = Prefix::new_unchecked(prefix_str);
let seq: u64 = seq_str.parse().map_err(|_| {
(
StatusCode::BAD_REQUEST,
Json(ErrorResponse {
error: format!("seq must be a non-negative integer, got '{seq_str}'"),
duplicity: None,
}),
)
})?;
let storage = state.inner.storage.lock().map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "internal lock error".to_string(),
duplicity: None,
}),
)
})?;
match storage.get_first_seen(&prefix, seq as u128) {
Ok(Some(said)) => Ok(Json(SaidAtSeqResponse { prefix, seq, said })),
Ok(None) => Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: format!(
"no event seen for prefix {} at seq {}",
prefix.as_str(),
seq
),
duplicity: None,
}),
)),
Err(e) => Err((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("storage error: {}", e),
duplicity: None,
}),
)),
}
}
async fn get_receipt(
State(state): State<WitnessServerState>,
AxumPath((prefix_str, said_str)): AxumPath<(String, String)>,
) -> Result<Json<Receipt>, StatusCode> {
let prefix = Prefix::new_unchecked(prefix_str);
let said = Said::new_unchecked(said_str);
let storage = state
.inner
.storage
.lock()
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
match storage.get_receipt(&prefix, &said) {
Ok(Some(receipt)) => Ok(Json(receipt)),
Ok(None) => Err(StatusCode::NOT_FOUND),
Err(_) => Err(StatusCode::INTERNAL_SERVER_ERROR),
}
}
async fn get_key_state(
State(state): State<WitnessServerState>,
AxumPath(prefix_str): AxumPath<String>,
) -> Result<Json<KeyStateRecord>, (StatusCode, Json<ErrorResponse>)> {
let prefix = Prefix::new_unchecked(prefix_str);
let kel_json = {
let storage = state.inner.storage.lock().map_err(|_| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "internal lock error".to_string(),
duplicity: None,
}),
)
})?;
storage.get_kel(&prefix).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("storage error: {e}"),
duplicity: None,
}),
)
})?
};
if kel_json.is_empty() {
return Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: format!(
"no key-state for {} — this witness has corroborated no events for it",
prefix.as_str()
),
duplicity: None,
}),
));
}
let kel_array = format!("[{}]", kel_json.join(","));
let events = parse_kel_json(&kel_array).map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("retained KEL did not parse: {e}"),
duplicity: None,
}),
)
})?;
let key_state = TrustedKel::from_trusted_source(&events)
.replay()
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: format!("retained KEL did not replay: {e}"),
duplicity: None,
}),
)
})?;
let dt = (state.inner.clock)().to_rfc3339();
let record = KeyStateRecord::from_kel(&events, &key_state, dt).ok_or((
StatusCode::INTERNAL_SERVER_ERROR,
Json(ErrorResponse {
error: "retained KEL is empty after replay".to_string(),
duplicity: None,
}),
))?;
Ok(Json(record))
}
async fn get_build(
State(state): State<WitnessServerState>,
) -> Result<Json<BuildProof>, (StatusCode, Json<ErrorResponse>)> {
match &state.inner.build_proof {
Some(proof) => Ok(Json(proof.clone())),
None => Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: "this node was started without a build attestation — \
it cannot prove which binary it runs"
.to_string(),
duplicity: None,
}),
)),
}
}
async fn health(State(state): State<WitnessServerState>) -> Json<HealthResponse> {
let storage = state
.inner
.storage
.lock()
.unwrap_or_else(|e| e.into_inner());
let first_seen_count = storage.count_first_seen().unwrap_or(0);
let receipt_count = storage.count_receipts().unwrap_or(0);
Json(HealthResponse {
status: "ok".to_string(),
witness_did: state.inner.witness_did.clone(),
first_seen_count,
receipt_count,
})
}
#[cfg(test)]
mod tests {
use super::*;
use axum::body::Body;
use axum::http::{Request, StatusCode};
use ring::signature::{Ed25519KeyPair, KeyPair};
use tower::ServiceExt;
fn test_state() -> WitnessServerState {
WitnessServerState::in_memory_generated().unwrap()
}
fn test_keypair() -> (Vec<u8>, String) {
let rng = ring::rand::SystemRandom::new();
let pkcs8 = Ed25519KeyPair::generate_pkcs8(&rng).unwrap();
let kp = Ed25519KeyPair::from_pkcs8(pkcs8.as_ref()).unwrap();
let pk_hex = hex::encode(kp.public_key().as_ref());
(pkcs8.as_ref().to_vec(), pk_hex)
}
fn make_valid_icp_event(prefix: &str, seq: u128) -> serde_json::Value {
let (pkcs8, _pk_hex) = test_keypair();
let kp = Ed25519KeyPair::from_pkcs8(&pkcs8).unwrap();
let k0 = auths_keri::KeriPublicKey::ed25519(kp.public_key().as_ref())
.unwrap()
.to_qb64()
.unwrap();
let mut event = serde_json::json!({
"v": "KERI10JSON000000_",
"t": "icp",
"d": "",
"i": prefix,
"s": seq,
"k": [k0],
"x": ""
});
let payload = serde_json::to_vec(&event).unwrap();
let sig = kp.sign(&payload);
event["x"] = serde_json::Value::String(hex::encode(sig.as_ref()));
let said = crate::crypto::said::compute_said(&event).unwrap();
event["d"] = serde_json::Value::String(said.into_inner());
event
}
fn make_full_icp_event() -> serde_json::Value {
let (pkcs8, _pk_hex) = test_keypair();
let kp = Ed25519KeyPair::from_pkcs8(&pkcs8).unwrap();
let k0 = auths_keri::KeriPublicKey::ed25519(kp.public_key().as_ref())
.unwrap()
.to_qb64()
.unwrap();
let next = auths_keri::KeriPublicKey::ed25519(&[9u8; 32]).unwrap();
let ncommit = auths_keri::compute_next_commitment(&next);
let mut event = serde_json::json!({
"v": "KERI10JSON000000_",
"t": "icp",
"d": "",
"i": "",
"s": "0",
"kt": "1",
"k": [k0],
"nt": "1",
"n": [ncommit.as_str()],
"bt": "0",
"b": [],
"c": [],
"a": [],
"x": ""
});
let said = crate::crypto::said::compute_said(&event).unwrap();
let said_str = said.into_inner();
event["d"] = serde_json::Value::String(said_str.clone());
event["i"] = serde_json::Value::String(said_str);
let mut to_sign = event.clone();
to_sign["d"] = serde_json::Value::String(String::new());
to_sign["x"] = serde_json::Value::String(String::new());
let payload = serde_json::to_vec(&to_sign).unwrap();
let sig = kp.sign(&payload);
event["x"] = serde_json::Value::String(hex::encode(sig.as_ref()));
event
}
async fn submit_to_fresh_server(event: &serde_json::Value) -> (Router, String) {
let prefix = event["i"].as_str().unwrap().to_string();
let state = test_state();
let app = router(state);
let resp = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri(format!("/witness/{prefix}/event"))
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(event).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK, "full icp must be witnessed");
(app, prefix)
}
#[tokio::test(flavor = "multi_thread")]
async fn key_state_serves_keri_conformant_record() {
let event = make_full_icp_event();
let (app, prefix) = submit_to_fresh_server(&event).await;
let resp = app
.oneshot(
Request::builder()
.uri(format!("/witness/{prefix}/key-state"))
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
let body = axum::body::to_bytes(resp.into_body(), usize::MAX)
.await
.unwrap();
let record: serde_json::Value = serde_json::from_slice(&body).unwrap();
let obj = record.as_object().unwrap();
let keys: Vec<&str> = obj.keys().map(String::as_str).collect();
assert_eq!(
keys,
vec![
"vn", "i", "s", "p", "d", "f", "dt", "et", "kt", "k", "nt", "n", "bt", "b", "c",
"ee", "di"
]
);
assert_eq!(obj["vn"], serde_json::json!([1, 0]));
assert_eq!(obj["i"], serde_json::Value::String(prefix.clone()));
assert_eq!(obj["s"], "0");
assert_eq!(obj["et"], "icp");
assert_eq!(obj["d"], serde_json::Value::String(prefix));
let parsed: auths_keri::KeyStateRecord = serde_json::from_slice(&body).unwrap();
let state = parsed.into_key_state();
assert_eq!(state.sequence, 0);
}
#[tokio::test(flavor = "multi_thread")]
async fn key_state_unknown_prefix_is_404() {
let app = router(test_state());
let resp = app
.oneshot(
Request::builder()
.uri("/witness/ENeverWitnessed/key-state")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[tokio::test(flavor = "multi_thread")]
async fn health_endpoint() {
let state = test_state();
let app = router(state);
let response = app
.oneshot(
Request::builder()
.uri("/health")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test(flavor = "multi_thread")]
async fn build_endpoint_absent_without_a_proof_is_404() {
let state = test_state();
let app = router(state);
let response = app
.oneshot(
Request::builder()
.uri("/build")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test(flavor = "multi_thread")]
async fn build_endpoint_serves_the_configured_proof() {
let mut state = test_state();
let proof = BuildProof {
version: "1.2.3".to_string(),
running_digest: "abc123".to_string(),
attestation: serde_json::json!({"issuer": "did:key:zTest"}),
};
let inner = Arc::get_mut(&mut state.inner).expect("sole owner in test");
inner.build_proof = Some(proof.clone());
let app = router(state);
let response = app
.oneshot(
Request::builder()
.uri("/build")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let served: BuildProof = serde_json::from_slice(&body).unwrap();
assert_eq!(served.version, "1.2.3");
assert_eq!(served.running_digest, "abc123");
}
#[tokio::test(flavor = "multi_thread")]
async fn submit_valid_icp_event_success() {
let state = test_state();
let app = router(state);
let event = make_valid_icp_event("EPrefix", 0);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/witness/EPrefix/event")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&event).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test(flavor = "multi_thread")]
async fn server_signs_receipt() {
let state = test_state();
let app = router(state.clone());
let event = make_valid_icp_event("EPrefix", 0);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/witness/EPrefix/event")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&event).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
let body = axum::body::to_bytes(response.into_body(), usize::MAX)
.await
.unwrap();
let signed: SignedReceipt =
serde_json::from_slice(&body).expect("witness response must be a SignedReceipt");
assert!(
!signed.signature.is_empty(),
"witness must attach a signature"
);
let key = auths_keri::KeriPublicKey::from_verkey_bytes(&state.public_key(), state.curve())
.unwrap();
let payload = serde_json::to_vec(&signed.receipt).unwrap();
assert!(
key.verify_signature(&payload, &signed.signature).is_ok(),
"witness signature must verify against its advertised key"
);
}
#[tokio::test(flavor = "multi_thread")]
async fn submit_event_with_mismatched_said_rejected() {
let state = test_state();
let app = router(state);
let mut event = make_valid_icp_event("EPrefix", 0);
event["d"] = serde_json::Value::String(
"EFakeSAID_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string(),
);
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/witness/EPrefix/event")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&event).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test(flavor = "multi_thread")]
async fn submit_event_missing_required_fields_rejected() {
let state = test_state();
let app = router(state);
let body = serde_json::json!({"v": "KERI10JSON000000_"});
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/witness/EPrefix/event")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&body).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test(flavor = "multi_thread")]
async fn submit_event_invalid_signature_hex_rejected() {
let state = test_state();
let app = router(state);
let mut event = serde_json::json!({
"v": "KERI10JSON000000_",
"t": "icp",
"d": "",
"i": "EPrefix",
"s": 0,
"k": ["0000000000000000000000000000000000000000000000000000000000000000"],
"x": "not_valid_hex!!!"
});
let said = crate::crypto::said::compute_said(&event).unwrap();
event["d"] = serde_json::Value::String(said.into_inner());
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/witness/EPrefix/event")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&event).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test(flavor = "multi_thread")]
async fn submit_icp_event_wrong_self_signature_rejected() {
let state = test_state();
let app = router(state);
let (pkcs8, _) = test_keypair();
let kp = Ed25519KeyPair::from_pkcs8(&pkcs8).unwrap();
let k0 = auths_keri::KeriPublicKey::ed25519(kp.public_key().as_ref())
.unwrap()
.to_qb64()
.unwrap();
let (wrong_pkcs8, _) = test_keypair();
let wrong_kp = Ed25519KeyPair::from_pkcs8(&wrong_pkcs8).unwrap();
let mut event = serde_json::json!({
"v": "KERI10JSON000000_",
"t": "icp",
"d": "",
"i": "EPrefix",
"s": 0,
"k": [k0],
"x": ""
});
let payload = serde_json::to_vec(&event).unwrap();
let sig = wrong_kp.sign(&payload);
event["x"] = serde_json::Value::String(hex::encode(sig.as_ref()));
let said = crate::crypto::said::compute_said(&event).unwrap();
event["d"] = serde_json::Value::String(said.into_inner());
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/witness/EPrefix/event")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&event).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
}
#[tokio::test(flavor = "multi_thread")]
async fn submit_event_duplicity() {
let state = test_state();
let event_a = make_valid_icp_event("EPrefix", 0);
let event_a_said = event_a["d"].as_str().unwrap().to_string();
{
let app = router(state.clone());
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/witness/EPrefix/event")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&event_a).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
{
let app = router(state.clone());
let event_b = make_valid_icp_event("EPrefix", 0);
assert_ne!(event_a_said, event_b["d"].as_str().unwrap());
let response = app
.oneshot(
Request::builder()
.method("POST")
.uri("/witness/EPrefix/event")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&event_b).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::CONFLICT);
}
}
#[tokio::test(flavor = "multi_thread")]
async fn get_head_empty() {
let state = test_state();
let app = router(state);
let response = app
.oneshot(
Request::builder()
.uri("/witness/EPrefix/head")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test(flavor = "multi_thread")]
async fn get_receipt_not_found() {
let state = test_state();
let app = router(state);
let response = app
.oneshot(
Request::builder()
.uri("/witness/EPrefix/receipt/NONEXISTENT")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[tokio::test(flavor = "multi_thread")]
async fn said_at_seq_returns_first_seen() {
let state = test_state();
let app = router(state);
let event = make_valid_icp_event("EPrefix", 0);
let submit = app
.clone()
.oneshot(
Request::builder()
.method("POST")
.uri("/witness/EPrefix/event")
.header("content-type", "application/json")
.body(Body::from(serde_json::to_string(&event).unwrap()))
.unwrap(),
)
.await
.unwrap();
assert_eq!(submit.status(), StatusCode::OK);
let response = app
.oneshot(
Request::builder()
.uri("/witness/EPrefix/said/0")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::OK);
}
#[tokio::test(flavor = "multi_thread")]
async fn said_at_seq_unseen_is_404() {
let app = router(test_state());
let response = app
.oneshot(
Request::builder()
.uri("/witness/EUnseen/said/0")
.body(Body::empty())
.unwrap(),
)
.await
.unwrap();
assert_eq!(response.status(), StatusCode::NOT_FOUND);
}
#[test]
fn receipt_d_matches_event_said() {
let state = test_state();
let prefix = Prefix::new_unchecked("EPrefix".into());
let event_said = Said::new_unchecked("ESAID123".into());
let receipt = state.create_receipt(&prefix, 0, &event_said).unwrap();
assert_eq!(receipt.d, event_said);
}
#[test]
fn receipt_d_changes_with_event_said() {
let state = test_state();
let prefix = Prefix::new_unchecked("EPrefix".into());
let receipt_a = state
.create_receipt(&prefix, 0, &Said::new_unchecked("ESAID_A".into()))
.unwrap();
let receipt_b = state
.create_receipt(&prefix, 0, &Said::new_unchecked("ESAID_B".into()))
.unwrap();
assert_ne!(receipt_a.d, receipt_b.d);
}
#[test]
fn signed_receipt_signature_verifies() {
let state = test_state();
let prefix = Prefix::new_unchecked("EPrefix".into());
let signed = state
.create_signed_receipt(&prefix, 0, &Said::new_unchecked("ESAID123".into()))
.unwrap();
let public_key = state.public_key();
let payload = serde_json::to_vec(&signed.receipt).unwrap();
let pk = ring::signature::UnparsedPublicKey::new(&ring::signature::ED25519, &public_key);
pk.verify(&payload, &signed.signature)
.expect("signed receipt signature should verify against serialized receipt");
}
#[test]
fn ed25519_inception_self_signature_accepted() {
let event = make_valid_icp_event("EPrefix", 0);
assert!(verify_inception_self_signature(&event).is_ok());
}
#[test]
fn p256_inception_self_signature_accepted() {
use p256::ecdsa::{Signature, SigningKey, signature::Signer};
let sk = SigningKey::from_slice(&[7u8; 32]).unwrap();
let compressed = sk
.verifying_key()
.to_encoded_point(true)
.as_bytes()
.to_vec();
let k0 = auths_keri::KeriPublicKey::from_verkey_bytes(&compressed, CurveType::P256)
.unwrap()
.to_qb64()
.unwrap();
let mut event = serde_json::json!({
"v": "KERI10JSON000000_",
"t": "icp",
"d": "",
"i": "EPrefix",
"s": 0,
"k": [k0],
"x": ""
});
let payload = serde_json::to_vec(&event).unwrap();
let sig: Signature = sk.sign(&payload);
event["x"] = serde_json::Value::String(hex::encode(sig.to_bytes()));
assert!(
verify_inception_self_signature(&event).is_ok(),
"P-256 inception with a valid self-signature must be accepted"
);
}
#[test]
fn untagged_hex_key_rejected_as_unroutable() {
let (_, pk_hex) = test_keypair();
let event = serde_json::json!({
"v": "KERI10JSON000000_",
"t": "icp",
"d": "",
"i": "EPrefix",
"s": 0,
"k": [pk_hex],
"x": hex::encode([0u8; 64])
});
let err = verify_inception_self_signature(&event).unwrap_err();
assert!(
matches!(err, EventSignatureError::UnroutableKey(_)),
"untagged key must surface as a typed routing error, got: {err:?}"
);
}
#[test]
fn verify_event_said_valid() {
let event = make_valid_icp_event("EPrefix", 0);
assert!(verify_event_said(&event).is_ok());
}
#[test]
fn verify_event_said_tampered() {
let mut event = make_valid_icp_event("EPrefix", 0);
event["d"] = serde_json::Value::String(
"EFakeSAID_AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA".to_string(),
);
assert!(verify_event_said(&event).is_err());
}
#[test]
fn validate_structure_missing_fields() {
let event = serde_json::json!({"v": "KERI10JSON000000_"});
assert!(validate_event_structure(&event).is_err());
}
#[test]
fn validate_structure_icp_missing_k() {
let event = serde_json::json!({
"v": "KERI10JSON000000_",
"t": "icp",
"d": "E123",
"i": "EPrefix",
"s": 0
});
assert!(validate_event_structure(&event).is_err());
}
#[test]
fn validate_structure_rot_missing_p() {
let event = serde_json::json!({
"v": "KERI10JSON000000_",
"t": "rot",
"d": "E123",
"i": "EPrefix",
"s": 1
});
assert!(validate_event_structure(&event).is_err());
}
}