use crate::witness::WitnessQuorum;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct VerificationReport {
pub status: VerificationStatus,
pub chain: Vec<ChainLink>,
pub warnings: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub witness_quorum: Option<WitnessQuorum>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub anchored: Option<auths_keri::AnchorStatus>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub duplicity_warning: Option<crate::duplicity::DuplicityReport>,
}
impl VerificationReport {
pub fn is_valid(&self) -> bool {
matches!(self.status, VerificationStatus::Valid)
}
pub fn valid(chain: Vec<ChainLink>) -> Self {
Self {
status: VerificationStatus::Valid,
chain,
warnings: Vec::new(),
witness_quorum: None,
anchored: None,
duplicity_warning: None,
}
}
pub fn with_status(status: VerificationStatus, chain: Vec<ChainLink>) -> Self {
Self {
status,
chain,
warnings: Vec::new(),
witness_quorum: None,
anchored: None,
duplicity_warning: None,
}
}
pub fn with_duplicity_warning(mut self, warning: crate::duplicity::DuplicityReport) -> Self {
self.duplicity_warning = Some(warning);
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(tag = "type")]
pub enum VerificationStatus {
Valid,
Expired {
at: DateTime<Utc>,
},
Revoked {
at: Option<DateTime<Utc>>,
},
InvalidSignature {
step: usize,
},
BrokenChain {
missing_link: String,
},
InsufficientWitnesses {
required: usize,
verified: usize,
},
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ChainLink {
pub issuer: String,
pub subject: String,
pub valid: bool,
pub error: Option<String>,
}
impl ChainLink {
pub fn valid(issuer: String, subject: String) -> Self {
Self {
issuer,
subject,
valid: true,
error: None,
}
}
pub fn invalid(issuer: String, subject: String, error: String) -> Self {
Self {
issuer,
subject,
valid: false,
error: Some(error),
}
}
}
use std::borrow::Borrow;
use std::fmt;
use std::ops::Deref;
use std::str::FromStr;
#[derive(Debug, Clone, Serialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[repr(transparent)]
pub struct IdentityDID(String);
impl IdentityDID {
pub fn new_unchecked<S: Into<String>>(s: S) -> Self {
Self(s.into())
}
pub fn parse(s: &str) -> Result<Self, DidParseError> {
match s.strip_prefix("did:keri:") {
Some("") => Err(DidParseError::EmptyIdentifier),
Some(_) => Ok(Self(s.to_string())),
None => Err(DidParseError::InvalidIdentityPrefix(s.to_string())),
}
}
pub fn from_prefix(prefix: &str) -> Result<Self, DidParseError> {
if prefix.is_empty() {
return Err(DidParseError::EmptyIdentifier);
}
Ok(Self(format!("did:keri:{}", prefix)))
}
pub fn prefix(&self) -> &str {
self.0.strip_prefix("did:keri:").unwrap_or(&self.0)
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn into_inner(self) -> String {
self.0
}
}
impl fmt::Display for IdentityDID {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl FromStr for IdentityDID {
type Err = DidParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl TryFrom<&str> for IdentityDID {
type Error = DidParseError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::parse(s)
}
}
impl TryFrom<String> for IdentityDID {
type Error = DidParseError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::parse(&s)
}
}
impl From<IdentityDID> for String {
fn from(did: IdentityDID) -> String {
did.0
}
}
impl<'de> serde::Deserialize<'de> for IdentityDID {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::parse(&s).map_err(serde::de::Error::custom)
}
}
impl Deref for IdentityDID {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<str> for IdentityDID {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Borrow<str> for IdentityDID {
fn borrow(&self) -> &str {
&self.0
}
}
impl PartialEq<str> for IdentityDID {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for IdentityDID {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl PartialEq<IdentityDID> for str {
fn eq(&self, other: &IdentityDID) -> bool {
self == other.0
}
}
impl PartialEq<IdentityDID> for &str {
fn eq(&self, other: &IdentityDID) -> bool {
*self == other.0
}
}
impl FromStr for CanonicalDid {
type Err = DidParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
pub fn signer_hex_to_did(hex_key: &str) -> Result<CanonicalDid, DidConversionError> {
signer_hex_to_did_with_curve(hex_key, auths_crypto::CurveType::P256)
}
pub fn signer_hex_to_did_with_curve(
hex_key: &str,
curve: auths_crypto::CurveType,
) -> Result<CanonicalDid, DidConversionError> {
let bytes = hex::decode(hex_key).map_err(|e| DidConversionError::InvalidHex(e.to_string()))?;
let expected = curve.public_key_len();
if bytes.len() != expected {
return Err(DidConversionError::WrongKeyLength(bytes.len()));
}
Ok(CanonicalDid::from_public_key_did_key(&bytes, curve))
}
pub fn validate_did(did_str: &str) -> bool {
if let Some(rest) = did_str.strip_prefix("did:keri:") {
!rest.is_empty()
} else if let Some(rest) = did_str.strip_prefix("did:key:") {
!rest.is_empty()
} else {
false
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum DidConversionError {
#[error("invalid hex: {0}")]
InvalidHex(String),
#[error("expected 32-byte Ed25519 key, got {0} bytes")]
WrongKeyLength(usize),
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum DidParseError {
#[error("did:key: DID must start with 'did:key:z', got: {0}")]
InvalidDevicePrefix(String),
#[error("IdentityDID must start with 'did:keri:', got: {0}")]
InvalidIdentityPrefix(String),
#[error("DID method-specific identifier is empty")]
EmptyIdentifier,
#[error("{0}")]
InvalidFormat(String),
#[error("DID contains control characters")]
ControlCharacters,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[serde(transparent)]
pub struct CanonicalDid(String);
impl CanonicalDid {
pub fn parse(raw: &str) -> Result<Self, DidParseError> {
if raw.chars().any(|c| c.is_control()) {
return Err(DidParseError::ControlCharacters);
}
let trimmed = raw.trim();
if trimmed.is_empty() {
return Err(DidParseError::EmptyIdentifier);
}
let parts: Vec<&str> = trimmed.splitn(3, ':').collect();
if parts.len() < 3 || parts[0] != "did" || parts[1].is_empty() || parts[2].is_empty() {
return Err(DidParseError::InvalidFormat(format!(
"invalid DID format: '{}'",
trimmed
)));
}
let canonical = format!("did:{}:{}", parts[1].to_lowercase(), parts[2]);
Ok(Self(canonical))
}
pub fn new_unchecked<S: Into<String>>(s: S) -> Self {
Self(s.into())
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn method_specific_id(&self) -> &str {
self.0.splitn(3, ':').nth(2).unwrap_or("")
}
pub fn ref_name(&self) -> String {
self.0
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect()
}
pub fn matches_sanitized_ref(&self, ref_name: &str) -> bool {
self.ref_name() == ref_name
}
pub fn from_sanitized<'a>(sanitized: &str, known_dids: &'a [Self]) -> Option<&'a Self> {
known_dids.iter().find(|did| did.ref_name() == sanitized)
}
pub fn from_public_key_did_key(pubkey: &[u8], curve: auths_crypto::CurveType) -> Self {
let varint: &[u8] = match curve {
auths_crypto::CurveType::Ed25519 => &[0xED, 0x01],
auths_crypto::CurveType::P256 => &[0x80, 0x24],
};
let mut prefixed = Vec::with_capacity(varint.len() + pubkey.len());
prefixed.extend_from_slice(varint);
prefixed.extend_from_slice(pubkey);
let encoded = bs58::encode(prefixed).into_string();
Self(format!("did:key:z{}", encoded))
}
pub fn require_keri(self) -> Result<Self, DidParseError> {
let parts: Vec<&str> = self.0.splitn(3, ':').collect();
if parts[1] != "keri" {
return Err(DidParseError::InvalidFormat(format!(
"expected did:keri: DID, got did:{}:",
parts[1]
)));
}
let id = parts[2];
if id.len() < 2 || id.len() > 128 {
return Err(DidParseError::InvalidFormat(
"invalid KERI prefix: length must be 2–128 characters".into(),
));
}
if !id.starts_with(|c: char| c.is_ascii_uppercase()) {
return Err(DidParseError::InvalidFormat(format!(
"invalid KERI prefix: must start with an uppercase derivation code, got '{}'",
&id[..1]
)));
}
Ok(self)
}
pub fn into_inner(self) -> String {
self.0
}
}
impl TryFrom<String> for CanonicalDid {
type Error = DidParseError;
fn try_from(s: String) -> Result<Self, Self::Error> {
Self::parse(&s)
}
}
impl TryFrom<&str> for CanonicalDid {
type Error = DidParseError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
Self::parse(s)
}
}
impl From<CanonicalDid> for String {
fn from(d: CanonicalDid) -> Self {
d.0
}
}
impl fmt::Display for CanonicalDid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl Deref for CanonicalDid {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl AsRef<str> for CanonicalDid {
fn as_ref(&self) -> &str {
&self.0
}
}
impl Borrow<str> for CanonicalDid {
fn borrow(&self) -> &str {
&self.0
}
}
impl<'de> serde::Deserialize<'de> for CanonicalDid {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
Self::parse(&s).map_err(serde::de::Error::custom)
}
}
impl PartialEq<str> for CanonicalDid {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for CanonicalDid {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl From<IdentityDID> for CanonicalDid {
fn from(did: IdentityDID) -> Self {
Self(did.into_inner())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[non_exhaustive]
pub enum AssuranceLevel {
SelfAsserted,
TokenVerified,
Authenticated,
Sovereign,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error(
"invalid assurance level '{0}': expected one of: sovereign, authenticated, token_verified, self_asserted"
)]
pub struct AssuranceLevelParseError(pub String);
impl AssuranceLevel {
pub fn label(&self) -> &'static str {
match self {
Self::SelfAsserted => "Self-Asserted",
Self::TokenVerified => "Token-Verified",
Self::Authenticated => "Authenticated",
Self::Sovereign => "Sovereign",
}
}
pub fn score(&self) -> u8 {
match self {
Self::SelfAsserted => 1,
Self::TokenVerified => 2,
Self::Authenticated => 3,
Self::Sovereign => 4,
}
}
}
impl fmt::Display for AssuranceLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.label())
}
}
impl FromStr for AssuranceLevel {
type Err = AssuranceLevelParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.trim().to_lowercase().as_str() {
"sovereign" => Ok(Self::Sovereign),
"authenticated" => Ok(Self::Authenticated),
"token_verified" => Ok(Self::TokenVerified),
"self_asserted" => Ok(Self::SelfAsserted),
_ => Err(AssuranceLevelParseError(s.to_string())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use auths_keri::Said;
#[test]
fn report_without_witness_quorum_deserializes() {
let json = r#"{
"status": {"type": "Valid"},
"chain": [],
"warnings": []
}"#;
let report: VerificationReport = serde_json::from_str(json).unwrap();
assert!(report.is_valid());
assert!(report.witness_quorum.is_none());
}
#[test]
fn insufficient_witnesses_serializes_correctly() {
let status = VerificationStatus::InsufficientWitnesses {
required: 3,
verified: 1,
};
let json = serde_json::to_string(&status).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(parsed["type"], "InsufficientWitnesses");
assert_eq!(parsed["required"], 3);
assert_eq!(parsed["verified"], 1);
let roundtripped: VerificationStatus = serde_json::from_str(&json).unwrap();
assert_eq!(roundtripped, status);
}
#[test]
fn report_with_witness_quorum_roundtrips() {
use crate::witness::{WitnessQuorum, WitnessReceiptResult};
let report = VerificationReport {
status: VerificationStatus::Valid,
chain: vec![],
warnings: vec![],
witness_quorum: Some(WitnessQuorum {
required: 2,
verified: 2,
receipts: vec![
WitnessReceiptResult {
witness_id: "did:key:w1".into(),
receipt_said: Said::new_unchecked("EReceipt1".into()),
verified: true,
},
WitnessReceiptResult {
witness_id: "did:key:w2".into(),
receipt_said: Said::new_unchecked("EReceipt2".into()),
verified: true,
},
],
}),
anchored: None,
duplicity_warning: None,
};
let json = serde_json::to_string(&report).unwrap();
let parsed: VerificationReport = serde_json::from_str(&json).unwrap();
assert_eq!(report, parsed);
assert!(parsed.witness_quorum.is_some());
assert_eq!(parsed.witness_quorum.unwrap().verified, 2);
}
#[test]
fn report_without_witness_quorum_skips_in_json() {
let report = VerificationReport::valid(vec![]);
let json = serde_json::to_string(&report).unwrap();
assert!(!json.contains("witness_quorum"));
}
#[test]
fn assurance_level_ordering() {
assert!(AssuranceLevel::SelfAsserted < AssuranceLevel::TokenVerified);
assert!(AssuranceLevel::TokenVerified < AssuranceLevel::Authenticated);
assert!(AssuranceLevel::Authenticated < AssuranceLevel::Sovereign);
}
#[test]
fn assurance_level_serde_roundtrip() {
let variants = [
AssuranceLevel::SelfAsserted,
AssuranceLevel::TokenVerified,
AssuranceLevel::Authenticated,
AssuranceLevel::Sovereign,
];
for level in variants {
let json = serde_json::to_string(&level).unwrap();
let parsed: AssuranceLevel = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, level);
}
}
#[test]
fn assurance_level_serde_snake_case() {
assert_eq!(
serde_json::to_string(&AssuranceLevel::SelfAsserted).unwrap(),
"\"self_asserted\""
);
assert_eq!(
serde_json::to_string(&AssuranceLevel::TokenVerified).unwrap(),
"\"token_verified\""
);
assert_eq!(
serde_json::to_string(&AssuranceLevel::Authenticated).unwrap(),
"\"authenticated\""
);
assert_eq!(
serde_json::to_string(&AssuranceLevel::Sovereign).unwrap(),
"\"sovereign\""
);
}
#[test]
fn assurance_level_from_str() {
assert_eq!(
"sovereign".parse::<AssuranceLevel>().unwrap(),
AssuranceLevel::Sovereign
);
assert_eq!(
"authenticated".parse::<AssuranceLevel>().unwrap(),
AssuranceLevel::Authenticated
);
assert_eq!(
"token_verified".parse::<AssuranceLevel>().unwrap(),
AssuranceLevel::TokenVerified
);
assert_eq!(
"self_asserted".parse::<AssuranceLevel>().unwrap(),
AssuranceLevel::SelfAsserted
);
assert!("invalid".parse::<AssuranceLevel>().is_err());
}
#[test]
fn assurance_level_from_str_case_insensitive() {
assert_eq!(
"SOVEREIGN".parse::<AssuranceLevel>().unwrap(),
AssuranceLevel::Sovereign
);
assert_eq!(
"Authenticated".parse::<AssuranceLevel>().unwrap(),
AssuranceLevel::Authenticated
);
}
#[test]
fn assurance_level_score() {
assert_eq!(AssuranceLevel::SelfAsserted.score(), 1);
assert_eq!(AssuranceLevel::TokenVerified.score(), 2);
assert_eq!(AssuranceLevel::Authenticated.score(), 3);
assert_eq!(AssuranceLevel::Sovereign.score(), 4);
}
#[test]
fn assurance_level_display() {
assert_eq!(AssuranceLevel::SelfAsserted.to_string(), "Self-Asserted");
assert_eq!(AssuranceLevel::TokenVerified.to_string(), "Token-Verified");
assert_eq!(AssuranceLevel::Authenticated.to_string(), "Authenticated");
assert_eq!(AssuranceLevel::Sovereign.to_string(), "Sovereign");
}
}