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, VersionString};
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>,
}
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>,
}
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,
})
}
}
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),
}),
})
}
#[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),
}),
})
}
#[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),
}),
})
}
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("/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,
}),
));
}
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 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
}
#[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 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());
}
}