anp 0.8.4

Rust SDK for Agent Network Protocol (ANP)
Documentation
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};

use crate::authentication::{
    create_verification_method, find_verification_method, is_assertion_method_authorized,
    validate_did_document_binding,
};
use crate::proof::{
    compute_signing_input, ProofError, CRYPTOSUITE_EDDSA_JCS_2022, PROOF_TYPE_DATA_INTEGRITY,
};
use crate::{PrivateKeyMaterial, PublicKeyMaterial};

pub const OBJECT_PROOF_PURPOSE: &str = "assertionMethod";
pub const OBJECT_PROOF_SIGNATURE_MULTIBASE_PREFIX: &str = "z";
pub const OBJECT_PROOF_REQUIRED_FIELDS: [&str; 6] = [
    "type",
    "cryptosuite",
    "verificationMethod",
    "proofPurpose",
    "created",
    "proofValue",
];

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct ObjectProofVerificationResult {
    pub issuer_did: String,
    pub verification_method_id: String,
    pub verification_method: Value,
}

pub fn generate_object_proof(
    document: &Value,
    private_key: &PrivateKeyMaterial,
    verification_method: &str,
    issuer_did: &str,
    created: Option<String>,
) -> Result<Value, ProofError> {
    ensure_object(document)?;
    ensure_ed25519_private_key(private_key)?;
    ensure_verification_method_matches_issuer(verification_method, issuer_did)?;
    let created_value = normalize_rfc3339(created)?;

    let mut proof_object = Map::new();
    proof_object.insert(
        "type".to_string(),
        Value::String(PROOF_TYPE_DATA_INTEGRITY.to_string()),
    );
    proof_object.insert(
        "cryptosuite".to_string(),
        Value::String(CRYPTOSUITE_EDDSA_JCS_2022.to_string()),
    );
    proof_object.insert(
        "verificationMethod".to_string(),
        Value::String(verification_method.to_string()),
    );
    proof_object.insert(
        "proofPurpose".to_string(),
        Value::String(OBJECT_PROOF_PURPOSE.to_string()),
    );
    proof_object.insert("created".to_string(), Value::String(created_value));

    let signing_document = document_without_top_level_proof(document)?;
    let signing_input =
        compute_signing_input(&signing_document, &Value::Object(proof_object.clone()))?;
    let signature = private_key
        .sign_message(&signing_input)
        .map_err(|_| ProofError::SigningError)?;
    proof_object.insert(
        "proofValue".to_string(),
        Value::String(encode_signature_multibase(&signature)),
    );

    let mut signed_document = document.clone();
    signed_document
        .as_object_mut()
        .ok_or_else(|| ProofError::InvalidProofField("document must be an object".to_string()))?
        .insert("proof".to_string(), Value::Object(proof_object));
    Ok(signed_document)
}

pub fn verify_object_proof(
    document: &Value,
    issuer_did: &str,
    issuer_document: &Value,
) -> Result<(), ProofError> {
    verify_object_proof_detailed(document, issuer_did, issuer_document).map(|_| ())
}

pub fn verify_object_proof_detailed(
    document: &Value,
    issuer_did: &str,
    issuer_document: &Value,
) -> Result<ObjectProofVerificationResult, ProofError> {
    ensure_object(document)?;
    ensure_object(issuer_document)?;
    if issuer_document.get("id").and_then(Value::as_str) != Some(issuer_did) {
        return Err(ProofError::IssuerDidMismatch);
    }
    if issuer_did.starts_with("did:wba:") && !validate_did_document_binding(issuer_document, false)
    {
        return Err(ProofError::InvalidIssuerDidDocument);
    }

    let proof = document
        .get("proof")
        .and_then(Value::as_object)
        .ok_or(ProofError::MissingProof)?;
    for field in OBJECT_PROOF_REQUIRED_FIELDS {
        let value = proof.get(field).and_then(Value::as_str).unwrap_or_default();
        if value.is_empty() {
            return Err(ProofError::MissingProofField(field.to_string()));
        }
    }
    if proof.get("type").and_then(Value::as_str) != Some(PROOF_TYPE_DATA_INTEGRITY) {
        return Err(ProofError::InvalidProofField("type".to_string()));
    }
    if proof.get("cryptosuite").and_then(Value::as_str) != Some(CRYPTOSUITE_EDDSA_JCS_2022) {
        return Err(ProofError::InvalidProofField("cryptosuite".to_string()));
    }
    if proof.get("proofPurpose").and_then(Value::as_str) != Some(OBJECT_PROOF_PURPOSE) {
        return Err(ProofError::InvalidProofField("proofPurpose".to_string()));
    }

    let verification_method_id = proof
        .get("verificationMethod")
        .and_then(Value::as_str)
        .ok_or_else(|| ProofError::MissingProofField("verificationMethod".to_string()))?;
    ensure_verification_method_matches_issuer(verification_method_id, issuer_did)?;
    parse_rfc3339(
        proof
            .get("created")
            .and_then(Value::as_str)
            .ok_or_else(|| ProofError::MissingProofField("created".to_string()))?,
    )?;

    if !is_assertion_method_authorized(issuer_document, verification_method_id) {
        return Err(ProofError::UnauthorizedVerificationMethod);
    }
    let method = find_verification_method(issuer_document, verification_method_id)
        .ok_or_else(|| ProofError::InvalidProofField("verificationMethod".to_string()))?;
    let verification_method =
        create_verification_method(&method).map_err(|_| ProofError::InvalidPublicKey)?;
    let public_key = match &verification_method.public_key {
        PublicKeyMaterial::Ed25519(key) => PublicKeyMaterial::Ed25519(*key),
        _ => return Err(ProofError::InvalidPublicKey),
    };

    let proof_value = proof
        .get("proofValue")
        .and_then(Value::as_str)
        .ok_or_else(|| ProofError::MissingProofField("proofValue".to_string()))?;
    let signature = decode_signature_multibase(proof_value)?;
    let mut proof_options = proof.clone();
    proof_options.remove("proofValue");
    let signing_document = document_without_top_level_proof(document)?;
    let signing_input = compute_signing_input(&signing_document, &Value::Object(proof_options))?;
    public_key
        .verify_message(&signing_input, &signature)
        .map_err(|_| ProofError::VerificationFailed)?;

    Ok(ObjectProofVerificationResult {
        issuer_did: issuer_did.to_string(),
        verification_method_id: verification_method_id.to_string(),
        verification_method: method,
    })
}

fn ensure_object(value: &Value) -> Result<(), ProofError> {
    if value.is_object() {
        Ok(())
    } else {
        Err(ProofError::InvalidProofField(
            "document must be an object".to_string(),
        ))
    }
}

fn ensure_ed25519_private_key(private_key: &PrivateKeyMaterial) -> Result<(), ProofError> {
    match private_key {
        PrivateKeyMaterial::Ed25519(_) => Ok(()),
        _ => Err(ProofError::InvalidPublicKey),
    }
}

fn ensure_verification_method_matches_issuer(
    verification_method: &str,
    issuer_did: &str,
) -> Result<(), ProofError> {
    if did_from_verification_method(verification_method)? == issuer_did {
        Ok(())
    } else {
        Err(ProofError::IssuerDidMismatch)
    }
}

fn did_from_verification_method(verification_method: &str) -> Result<&str, ProofError> {
    let (did, fragment) = verification_method
        .split_once('#')
        .ok_or_else(|| ProofError::InvalidProofField("verificationMethod".to_string()))?;
    if did.starts_with("did:") && !fragment.is_empty() {
        Ok(did)
    } else {
        Err(ProofError::InvalidProofField(
            "verificationMethod".to_string(),
        ))
    }
}

fn normalize_rfc3339(value: Option<String>) -> Result<String, ProofError> {
    match value {
        Some(value) => {
            parse_rfc3339(&value)?;
            Ok(value)
        }
        None => Ok(Utc::now().format("%Y-%m-%dT%H:%M:%SZ").to_string()),
    }
}

pub(crate) fn parse_rfc3339(value: &str) -> Result<DateTime<Utc>, ProofError> {
    DateTime::parse_from_rfc3339(value)
        .map(|parsed| parsed.with_timezone(&Utc))
        .map_err(|_| ProofError::InvalidTimestamp(value.to_string()))
}

fn encode_signature_multibase(signature: &[u8]) -> String {
    format!(
        "{}{}",
        OBJECT_PROOF_SIGNATURE_MULTIBASE_PREFIX,
        bs58::encode(signature).into_string()
    )
}

fn decode_signature_multibase(value: &str) -> Result<Vec<u8>, ProofError> {
    if !value.starts_with(OBJECT_PROOF_SIGNATURE_MULTIBASE_PREFIX) {
        return Err(ProofError::InvalidProofValue);
    }
    let signature = bs58::decode(value.trim_start_matches(OBJECT_PROOF_SIGNATURE_MULTIBASE_PREFIX))
        .into_vec()
        .map_err(|_| ProofError::InvalidProofValue)?;
    if signature.len() != 64 {
        return Err(ProofError::InvalidProofValue);
    }
    Ok(signature)
}

pub(crate) fn document_without_top_level_proof(document: &Value) -> Result<Value, ProofError> {
    let mut output = document.clone();
    output
        .as_object_mut()
        .ok_or_else(|| ProofError::InvalidProofField("document must be an object".to_string()))?
        .remove("proof");
    Ok(output)
}