use serde::Serialize;
use crate::{
DataIntegrityError, DataIntegrityProof, SignOptions, VerificationMethodResolver, VerifyOptions,
signer::Signer,
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum VerifyPolicy {
RequireAll,
RequireAny,
RequireThreshold(usize),
}
#[derive(Debug)]
#[non_exhaustive]
pub struct MultiVerifyResult<'a> {
pub passed: Vec<&'a DataIntegrityProof>,
pub failed: Vec<(&'a DataIntegrityProof, DataIntegrityError)>,
pub policy_satisfied: bool,
}
impl MultiVerifyResult<'_> {
pub fn into_result(self) -> Result<(), DataIntegrityError> {
if self.policy_satisfied {
Ok(())
} else {
Err(DataIntegrityError::Conformance(format!(
"multi-proof policy not satisfied ({} passed, {} failed)",
self.passed.len(),
self.failed.len()
)))
}
}
}
impl DataIntegrityProof {
pub async fn sign_multi<S>(
data_doc: &S,
signers: &[&dyn Signer],
options: SignOptions,
) -> Result<Vec<DataIntegrityProof>, DataIntegrityError>
where
S: Serialize,
{
if signers.is_empty() {
return Err(DataIntegrityError::MalformedProof(
"sign_multi called with no signers".to_string(),
));
}
let mut options = options;
if options.created.is_none() {
options.created = Some(chrono::Utc::now());
}
let mut proofs = Vec::with_capacity(signers.len());
for signer in signers {
let proof = DataIntegrityProof::sign(data_doc, *signer, options.clone()).await?;
proofs.push(proof);
}
Ok(proofs)
}
}
pub async fn verify_multi<'a, S, R>(
proofs: &'a [DataIntegrityProof],
data_doc: &S,
resolver: &R,
options: VerifyOptions,
policy: VerifyPolicy,
) -> MultiVerifyResult<'a>
where
S: Serialize + Sync,
R: VerificationMethodResolver + ?Sized,
{
let mut passed = Vec::new();
let mut failed = Vec::new();
for proof in proofs {
match proof.verify(data_doc, resolver, options.clone()).await {
Ok(()) => passed.push(proof),
Err(e) => failed.push((proof, e)),
}
}
let policy_satisfied = match policy {
VerifyPolicy::RequireAll => !proofs.is_empty() && failed.is_empty(),
VerifyPolicy::RequireAny => !passed.is_empty(),
VerifyPolicy::RequireThreshold(n) => {
if n == 0 {
!proofs.is_empty() && failed.is_empty()
} else {
passed.len() >= n
}
}
};
MultiVerifyResult {
passed,
failed,
policy_satisfied,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::DidKeyResolver;
use affinidi_secrets_resolver::secrets::Secret;
use serde_json::json;
fn make_signer(kind: &str, seed: u8) -> Secret {
let secret = match kind {
"ed25519" => Secret::generate_ed25519(None, Some(&[seed; 32])),
#[cfg(feature = "ml-dsa")]
"ml-dsa-44" => Secret::generate_ml_dsa_44(None, Some(&[seed; 32])),
_ => panic!("unknown kind {kind}"),
};
let pk_mb = secret.get_public_keymultibase().unwrap();
let mut s = secret.clone();
s.id = format!("did:key:{pk_mb}#{pk_mb}");
s
}
#[tokio::test]
async fn sign_multi_pins_created_across_batch() {
let a = make_signer("ed25519", 10);
let b = make_signer("ed25519", 11);
let signers: Vec<&dyn Signer> = vec![&a, &b];
let doc = json!({"pin": "created"});
let proofs = DataIntegrityProof::sign_multi(&doc, &signers, SignOptions::new())
.await
.unwrap();
assert_eq!(proofs.len(), 2);
assert_eq!(proofs[0].created, proofs[1].created);
}
#[tokio::test]
async fn sign_multi_emits_one_proof_per_signer() {
let a = make_signer("ed25519", 1);
let b = make_signer("ed25519", 2);
let signers: Vec<&dyn Signer> = vec![&a, &b];
let doc = json!({"multi": true});
let proofs = DataIntegrityProof::sign_multi(&doc, &signers, SignOptions::new())
.await
.unwrap();
assert_eq!(proofs.len(), 2);
}
#[cfg(feature = "ml-dsa")]
#[tokio::test]
async fn sign_multi_hybrid_classical_and_pqc() {
let classical = make_signer("ed25519", 9);
let pqc = make_signer("ml-dsa-44", 9);
let signers: Vec<&dyn Signer> = vec![&classical, &pqc];
let doc = json!({"hybrid": "yes"});
let proofs = DataIntegrityProof::sign_multi(&doc, &signers, SignOptions::new())
.await
.unwrap();
assert_eq!(proofs.len(), 2);
assert_eq!(
proofs[0].cryptosuite,
crate::crypto_suites::CryptoSuite::EddsaJcs2022
);
assert_eq!(
proofs[1].cryptosuite,
crate::crypto_suites::CryptoSuite::MlDsa44Jcs2024
);
let result = verify_multi(
&proofs,
&doc,
&DidKeyResolver,
VerifyOptions::new(),
VerifyPolicy::RequireAll,
)
.await;
assert!(result.policy_satisfied);
assert_eq!(result.passed.len(), 2);
assert!(result.failed.is_empty());
}
#[tokio::test]
async fn verify_multi_require_any_tolerates_one_bad_proof() {
let good = make_signer("ed25519", 3);
let signers: Vec<&dyn Signer> = vec![&good];
let doc = json!({"x": 1});
let mut proofs = DataIntegrityProof::sign_multi(&doc, &signers, SignOptions::new())
.await
.unwrap();
let mut bad = proofs[0].clone();
let pv = bad.proof_value.take().unwrap();
let mut raw = multibase::decode(&pv).unwrap().1;
raw[0] ^= 0xff;
bad.proof_value = Some(multibase::encode(multibase::Base::Base58Btc, raw));
proofs.push(bad);
let result = verify_multi(
&proofs,
&doc,
&DidKeyResolver,
VerifyOptions::new(),
VerifyPolicy::RequireAny,
)
.await;
assert!(result.policy_satisfied);
assert_eq!(result.passed.len(), 1);
assert_eq!(result.failed.len(), 1);
let result = verify_multi(
&proofs,
&doc,
&DidKeyResolver,
VerifyOptions::new(),
VerifyPolicy::RequireAll,
)
.await;
assert!(!result.policy_satisfied);
}
#[tokio::test]
async fn verify_multi_threshold() {
let a = make_signer("ed25519", 1);
let b = make_signer("ed25519", 2);
let c = make_signer("ed25519", 3);
let signers: Vec<&dyn Signer> = vec![&a, &b, &c];
let doc = json!({"witnesses": 3});
let proofs = DataIntegrityProof::sign_multi(&doc, &signers, SignOptions::new())
.await
.unwrap();
let result = verify_multi(
&proofs,
&doc,
&DidKeyResolver,
VerifyOptions::new(),
VerifyPolicy::RequireThreshold(2),
)
.await;
assert!(result.policy_satisfied);
assert_eq!(result.passed.len(), 3);
}
#[tokio::test]
async fn verify_multi_threshold_zero_equals_require_all() {
let a = make_signer("ed25519", 1);
let signers: Vec<&dyn Signer> = vec![&a];
let doc = json!({"t": 0});
let proofs = DataIntegrityProof::sign_multi(&doc, &signers, SignOptions::new())
.await
.unwrap();
let require_all = verify_multi(
&proofs,
&doc,
&DidKeyResolver,
VerifyOptions::new(),
VerifyPolicy::RequireAll,
)
.await;
let threshold_zero = verify_multi(
&proofs,
&doc,
&DidKeyResolver,
VerifyOptions::new(),
VerifyPolicy::RequireThreshold(0),
)
.await;
assert_eq!(
require_all.policy_satisfied,
threshold_zero.policy_satisfied
);
assert_eq!(require_all.passed.len(), threshold_zero.passed.len());
let empty: Vec<DataIntegrityProof> = vec![];
let r = verify_multi(
&empty,
&doc,
&DidKeyResolver,
VerifyOptions::new(),
VerifyPolicy::RequireThreshold(0),
)
.await;
assert!(!r.policy_satisfied);
}
#[tokio::test]
async fn sign_multi_empty_signer_list_is_error() {
let doc = json!({});
let err = DataIntegrityProof::sign_multi(&doc, &[], SignOptions::new())
.await
.unwrap_err();
assert!(matches!(err, DataIntegrityError::MalformedProof(_)));
}
}