use std::str::FromStr;
use auths_anchor::{AnchorError, WitnessRef, WitnessSet};
use auths_core::storage::keychain::{KeyAlias, KeyRole, extract_public_key_bytes};
use auths_crypto::CurveType;
use auths_id::error::InitError;
use auths_id::keri::delegation::author_root_anchor_ixn;
use auths_id::keri::parse_did_keri;
use auths_keri::{Said, Seal};
use thiserror::Error;
use crate::context::AuthsContext;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum WitnessSetError {
#[error("invalid witness member spec {spec:?}: {reason}")]
InvalidMemberSpec {
spec: String,
reason: String,
},
#[error("witness set rejected: {0}")]
Set(#[source] AnchorError),
#[error("no managed identity to declare a witness set under: {0}")]
NoIdentity(String),
#[error("identity signing key unavailable: {0}")]
Key(#[source] auths_core::AgentError),
#[error("anchoring the witness-set declaration failed: {0}")]
Anchor(#[source] InitError),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DeclaredWitnessSet {
pub set_said: String,
pub ixn_said: String,
pub sequence: u128,
}
pub fn parse_witness_member(spec: &str) -> Result<WitnessRef, WitnessSetError> {
let invalid = |reason: String| WitnessSetError::InvalidMemberSpec {
spec: spec.to_string(),
reason,
};
let (name, key) = spec
.split_once('=')
.ok_or_else(|| invalid("expected `NAME=KEY`".to_string()))?;
let name = name.trim();
let key = key.trim();
if name.is_empty() {
return Err(invalid("empty member name".to_string()));
}
let (curve, public_key) = decode_member_key(key).map_err(invalid)?;
if public_key.len() != curve.public_key_len() {
return Err(invalid(format!(
"a {curve} key is {} bytes, got {}",
curve.public_key_len(),
public_key.len()
)));
}
Ok(WitnessRef {
name: name.to_string(),
curve,
public_key,
operator: None,
})
}
fn decode_member_key(key: &str) -> Result<(CurveType, Vec<u8>), String> {
if let Ok(parsed) = auths_keri::KeriPublicKey::parse(key) {
return Ok((parsed.curve(), parsed.raw_bytes().to_vec()));
}
if key.starts_with("did:key:") {
let decoded = auths_crypto::did_key_decode(key).map_err(|e| e.to_string())?;
return Ok((decoded.curve(), decoded.bytes().to_vec()));
}
if let Some((tag, hex_part)) = key.split_once(':') {
let curve = CurveType::from_str(tag).map_err(|e| e.to_string())?;
let bytes = hex::decode(hex_part).map_err(|e| format!("hex key: {e}"))?;
return Ok((curve, bytes));
}
let bytes = hex::decode(key).map_err(|_| {
"expected a CESR verkey, a did:key, `<curve>:<hex>`, or bare hex".to_string()
})?;
Ok((CurveType::Ed25519, bytes))
}
pub fn build_witness_set(
member_specs: &[String],
threshold: u32,
) -> Result<WitnessSet, WitnessSetError> {
let members = member_specs
.iter()
.map(|spec| parse_witness_member(spec))
.collect::<Result<Vec<_>, _>>()?;
let mut set = WitnessSet {
said: String::new(),
threshold,
members,
};
set.validate().map_err(WitnessSetError::Set)?;
set.said = set.computed_said().map_err(WitnessSetError::Set)?;
Ok(set)
}
pub fn resolve_declaration_alias(
ctx: &AuthsContext,
explicit: Option<String>,
) -> Result<KeyAlias, WitnessSetError> {
if let Some(alias) = explicit {
return Ok(KeyAlias::new_unchecked(alias));
}
let managed = ctx
.identity_storage
.load_identity()
.map_err(|e| WitnessSetError::NoIdentity(e.to_string()))?;
let primaries = ctx
.key_storage
.list_aliases_for_identity_with_role(&managed.controller_did, KeyRole::Primary)
.map_err(WitnessSetError::Key)?;
primaries.into_iter().next().ok_or_else(|| {
WitnessSetError::NoIdentity(format!(
"no primary signing key in the keychain for {}",
managed.controller_did
))
})
}
pub fn declare_witness_set(
ctx: &AuthsContext,
key_alias: &KeyAlias,
set: &WitnessSet,
) -> Result<DeclaredWitnessSet, WitnessSetError> {
set.validate().map_err(WitnessSetError::Set)?;
let set_said = set.computed_said().map_err(WitnessSetError::Set)?;
let managed = ctx
.identity_storage
.load_identity()
.map_err(|e| WitnessSetError::NoIdentity(e.to_string()))?;
let root_prefix = parse_did_keri(managed.controller_did.as_str())
.map_err(|e| WitnessSetError::NoIdentity(e.to_string()))?;
let (_public_key, curve) = extract_public_key_bytes(
ctx.key_storage.as_ref(),
key_alias,
ctx.passphrase_provider.as_ref(),
)
.map_err(WitnessSetError::Key)?;
let ixn = author_root_anchor_ixn(
ctx.registry.as_ref(),
&root_prefix,
key_alias,
curve,
vec![Seal::Digest {
d: Said::new_unchecked(set_said.clone()),
}],
ctx.passphrase_provider.as_ref(),
ctx.key_storage.as_ref(),
)
.map_err(WitnessSetError::Anchor)?;
Ok(DeclaredWitnessSet {
set_said,
ixn_said: ixn.d.as_str().to_string(),
sequence: ixn.s.value(),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn hex_key(byte: u8, len: usize) -> String {
hex::encode(vec![byte; len])
}
#[test]
fn bare_hex_member_defaults_to_the_checkpoint_curve() {
let member = parse_witness_member(&format!("w1={}", hex_key(1, 32))).unwrap();
assert_eq!(member.name, "w1");
assert_eq!(member.curve, CurveType::Ed25519);
assert_eq!(member.public_key, vec![1u8; 32]);
}
#[test]
fn curve_tagged_hex_member_parses() {
let ed = parse_witness_member(&format!("w1=ed25519:{}", hex_key(2, 32))).unwrap();
assert_eq!(ed.curve, CurveType::Ed25519);
let p = parse_witness_member(&format!("w2=p256:{}", hex_key(3, 33))).unwrap();
assert_eq!(p.curve, CurveType::P256);
}
#[test]
fn wrong_length_for_the_tagged_curve_is_refused() {
let err = parse_witness_member(&format!("w1=ed25519:{}", hex_key(2, 33))).unwrap_err();
assert!(matches!(err, WitnessSetError::InvalidMemberSpec { .. }));
let err = parse_witness_member(&format!("w1={}", hex_key(2, 16))).unwrap_err();
assert!(matches!(err, WitnessSetError::InvalidMemberSpec { .. }));
}
#[test]
fn missing_equals_and_empty_name_are_refused() {
assert!(matches!(
parse_witness_member("just-a-name"),
Err(WitnessSetError::InvalidMemberSpec { .. })
));
assert!(matches!(
parse_witness_member(&format!("={}", hex_key(1, 32))),
Err(WitnessSetError::InvalidMemberSpec { .. })
));
}
#[test]
fn unknown_curve_tag_is_refused_not_defaulted() {
assert!(matches!(
parse_witness_member(&format!("w1=ed448:{}", hex_key(1, 57))),
Err(WitnessSetError::InvalidMemberSpec { .. })
));
}
#[test]
fn computed_said_is_stable_across_member_order() {
let a = build_witness_set(
&[
format!("w1={}", hex_key(1, 32)),
format!("w2={}", hex_key(2, 32)),
],
2,
)
.unwrap();
let b = build_witness_set(
&[
format!("w2={}", hex_key(2, 32)),
format!("w1={}", hex_key(1, 32)),
],
2,
)
.unwrap();
assert_eq!(a.said, b.said);
assert!(a.said.starts_with('E'));
}
#[test]
fn different_content_yields_a_different_said() {
let a = build_witness_set(&[format!("w1={}", hex_key(1, 32))], 1).unwrap();
let b = build_witness_set(&[format!("w1={}", hex_key(9, 32))], 1).unwrap();
let c = build_witness_set(
&[
format!("w1={}", hex_key(1, 32)),
format!("w2={}", hex_key(2, 32)),
],
1,
)
.unwrap();
assert_ne!(a.said, b.said);
assert_ne!(a.said, c.said);
}
#[test]
fn structural_violations_are_refused() {
let dup = build_witness_set(
&[
format!("w1={}", hex_key(1, 32)),
format!("w1={}", hex_key(2, 32)),
],
1,
);
assert!(matches!(dup, Err(WitnessSetError::Set(_))));
let over = build_witness_set(&[format!("w1={}", hex_key(1, 32))], 2);
assert!(matches!(over, Err(WitnessSetError::Set(_))));
}
}