use std::sync::Arc;
use async_trait::async_trait;
use thiserror::Error;
#[derive(Debug, Clone, Copy)]
pub struct ProofEnvelope<'a> {
pub proof: &'a str,
pub method: &'a str,
pub uri: &'a str,
pub now_unix: u64,
pub expected_subject_hint: Option<&'a str>,
}
#[derive(Debug, Clone)]
pub struct VerifiedSubject {
pub did: String,
pub verification_method: String,
}
#[derive(Debug, Error)]
pub enum SelfSignedError {
#[error("malformed proof: {0}")]
Malformed(String),
#[error("proof scope mismatch: {0}")]
ScopeMismatch(String),
#[error("signature invalid: {0}")]
InvalidSignature(String),
#[error("proof timestamp out of range: {0}")]
OutOfTimeWindow(String),
#[error("no verifier matched the proof format")]
UnrecognisedFormat,
#[error("verifier: {0}")]
Other(String),
}
#[async_trait]
pub trait SelfSignedVerifier: Send + Sync {
async fn verify(
&self,
envelope: &ProofEnvelope<'_>,
) -> Result<Option<VerifiedSubject>, SelfSignedError>;
fn name(&self) -> &'static str;
}
pub struct CidVerifier {
inner: Vec<Arc<dyn SelfSignedVerifier>>,
}
impl CidVerifier {
pub fn new() -> Self {
Self { inner: Vec::new() }
}
#[must_use]
pub fn with(mut self, verifier: Arc<dyn SelfSignedVerifier>) -> Self {
self.inner.push(verifier);
self
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
pub fn registered(&self) -> Vec<&'static str> {
self.inner.iter().map(|v| v.name()).collect()
}
}
impl Default for CidVerifier {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl SelfSignedVerifier for CidVerifier {
async fn verify(
&self,
envelope: &ProofEnvelope<'_>,
) -> Result<Option<VerifiedSubject>, SelfSignedError> {
if self.inner.is_empty() {
return Err(SelfSignedError::UnrecognisedFormat);
}
for v in &self.inner {
match v.verify(envelope).await {
Ok(Some(subj)) => return Ok(Some(subj)),
Ok(None) => continue,
Err(SelfSignedError::UnrecognisedFormat) => continue,
Err(e) => {
return Err(e);
}
}
}
Err(SelfSignedError::UnrecognisedFormat)
}
fn name(&self) -> &'static str {
"cid:Verifier"
}
}
#[cfg(test)]
mod tests {
use super::*;
struct EchoVerifier {
name: &'static str,
want_prefix: &'static str,
did: &'static str,
}
#[async_trait]
impl SelfSignedVerifier for EchoVerifier {
async fn verify(
&self,
envelope: &ProofEnvelope<'_>,
) -> Result<Option<VerifiedSubject>, SelfSignedError> {
if envelope.proof.starts_with(self.want_prefix) {
Ok(Some(VerifiedSubject {
did: self.did.to_string(),
verification_method: format!("{}#keys-0", self.did),
}))
} else {
Ok(None)
}
}
fn name(&self) -> &'static str {
self.name
}
}
struct BrokenVerifier;
#[async_trait]
impl SelfSignedVerifier for BrokenVerifier {
async fn verify(
&self,
envelope: &ProofEnvelope<'_>,
) -> Result<Option<VerifiedSubject>, SelfSignedError> {
if envelope.proof.starts_with("broken:") {
Err(SelfSignedError::InvalidSignature("stub".into()))
} else {
Ok(None)
}
}
fn name(&self) -> &'static str {
"broken"
}
}
fn envelope(proof: &str) -> ProofEnvelope<'_> {
ProofEnvelope {
proof,
method: "GET",
uri: "https://pod.example/r",
now_unix: 1_700_000_000,
expected_subject_hint: None,
}
}
#[tokio::test]
async fn empty_dispatcher_returns_unrecognised() {
let c = CidVerifier::new();
let env = envelope("anything");
let err = c.verify(&env).await.unwrap_err();
assert!(matches!(err, SelfSignedError::UnrecognisedFormat));
}
#[tokio::test]
async fn first_matching_wins() {
let c = CidVerifier::new()
.with(Arc::new(EchoVerifier {
name: "a",
want_prefix: "a:",
did: "did:a:1",
}))
.with(Arc::new(EchoVerifier {
name: "b",
want_prefix: "b:",
did: "did:b:1",
}));
let env = envelope("b:hello");
let subj = c.verify(&env).await.unwrap().unwrap();
assert_eq!(subj.did, "did:b:1");
}
#[tokio::test]
async fn broken_matching_verifier_short_circuits() {
let c = CidVerifier::new()
.with(Arc::new(BrokenVerifier))
.with(Arc::new(EchoVerifier {
name: "a",
want_prefix: "a:",
did: "did:a:1",
}));
let env = envelope("broken:sigbad");
let err = c.verify(&env).await.unwrap_err();
assert!(matches!(err, SelfSignedError::InvalidSignature(_)));
}
#[tokio::test]
async fn no_matching_verifier_returns_unrecognised() {
let c = CidVerifier::new().with(Arc::new(EchoVerifier {
name: "a",
want_prefix: "a:",
did: "did:a:1",
}));
let env = envelope("z:none");
let err = c.verify(&env).await.unwrap_err();
assert!(matches!(err, SelfSignedError::UnrecognisedFormat));
}
#[test]
fn registered_lists_names() {
let c = CidVerifier::new()
.with(Arc::new(EchoVerifier {
name: "first",
want_prefix: "f:",
did: "did:a",
}))
.with(Arc::new(BrokenVerifier));
assert_eq!(c.registered(), vec!["first", "broken"]);
assert_eq!(c.len(), 2);
}
}