anp 0.9.0

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

use super::v2_errors::DirectE2eeV2Error;
use super::v2_models::{
    deserialize_present, V2KeyServiceMetadata, V2OneTimePrekey, V2PrekeyBundle, V2SignedPrekey,
    V2Target, DIRECT_E2EE_PROFILE_V2, MTI_DIRECT_E2EE_SUITE_V2,
    TRANSPORT_PROTECTED_SECURITY_PROFILE,
};
use crate::authentication::{find_eligible_device, PROFILE_DIRECT_E2EE_V2};
use crate::canonical_json::canonicalize_json;
use crate::proof::{generate_object_proof, verify_object_proof};
use crate::PrivateKeyMaterial;

pub fn build_prekey_bundle_v2(
    bundle_id: &str,
    owner_did: &str,
    owner_device_id: &str,
    static_key_agreement_id: &str,
    signed_prekey: V2SignedPrekey,
    signing_private_key: &PrivateKeyMaterial,
    verification_method: &str,
    created: Option<&str>,
) -> Result<V2PrekeyBundle, DirectE2eeV2Error> {
    signed_prekey.validate()?;
    let unsigned = json!({
        "bundle_id": bundle_id,
        "owner_did": owner_did,
        "owner_device_id": owner_device_id,
        "suite": MTI_DIRECT_E2EE_SUITE_V2,
        "static_key_agreement_id": static_key_agreement_id,
        "signed_prekey": signed_prekey,
    });
    let signed = generate_object_proof(
        &unsigned,
        signing_private_key,
        verification_method,
        owner_did,
        created.map(str::to_owned),
    )?;
    let bundle = V2PrekeyBundle {
        bundle_id: bundle_id.to_owned(),
        owner_did: owner_did.to_owned(),
        owner_device_id: owner_device_id.to_owned(),
        suite: MTI_DIRECT_E2EE_SUITE_V2.to_owned(),
        static_key_agreement_id: static_key_agreement_id.to_owned(),
        signed_prekey,
        proof: signed
            .get("proof")
            .cloned()
            .ok_or_else(|| DirectE2eeV2Error::invalid("generated bundle has no proof"))?,
    };
    bundle.validate_structure()?;
    Ok(bundle)
}

pub fn signed_bundle_object_jcs_v2(bundle: &V2PrekeyBundle) -> Result<Vec<u8>, DirectE2eeV2Error> {
    let mut value = serde_json::to_value(bundle)?;
    value
        .as_object_mut()
        .ok_or_else(|| DirectE2eeV2Error::invalid("prekey bundle must be an object"))?
        .remove("proof");
    Ok(canonicalize_json(&value)?)
}

pub fn verify_prekey_bundle_v2(
    bundle: &V2PrekeyBundle,
    did_document: &Value,
    now: DateTime<Utc>,
) -> Result<(), DirectE2eeV2Error> {
    bundle.validate_structure()?;
    if did_document.get("id").and_then(Value::as_str) != Some(bundle.owner_did.as_str()) {
        return Err(DirectE2eeV2Error::invalid(
            "owner_did must match the issuer DID document",
        ));
    }
    let device = find_eligible_device(
        did_document,
        &bundle.owner_device_id,
        PROFILE_DIRECT_E2EE_V2,
    )
    .map_err(|error| DirectE2eeV2Error::invalid(error.to_string()))?
    .ok_or_else(|| DirectE2eeV2Error::invalid("owner device is not P5 v2 eligible"))?;
    if device.e2ee_key_id != bundle.static_key_agreement_id {
        return Err(DirectE2eeV2Error::invalid(
            "static_key_agreement_id must equal the device e2ee_key_id",
        ));
    }
    if bundle
        .proof
        .get("verificationMethod")
        .and_then(Value::as_str)
        != Some(device.signing_key_id.as_str())
    {
        return Err(DirectE2eeV2Error::invalid(
            "proof.verificationMethod must equal the device signing_key_id",
        ));
    }
    let expires_at = DateTime::parse_from_rfc3339(&bundle.signed_prekey.expires_at)
        .map_err(|_| DirectE2eeV2Error::invalid("signed_prekey.expires_at must be RFC3339"))?
        .with_timezone(&Utc);
    if expires_at <= now {
        return Err(DirectE2eeV2Error::invalid("signed prekey is expired"));
    }
    verify_object_proof(
        &serde_json::to_value(bundle)?,
        &bundle.owner_did,
        did_document,
    )?;
    Ok(())
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct V2PublishPrekeyBundleBody {
    pub prekey_bundle: V2PrekeyBundle,
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub one_time_prekeys: Vec<V2OneTimePrekey>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct V2GetPrekeyBundleBody {
    pub target_did: String,
    pub target_device_id: String,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "deserialize_present"
    )]
    pub preferred_suite: Option<String>,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "deserialize_present"
    )]
    pub require_opk: Option<bool>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(deny_unknown_fields)]
pub struct V2PublishPrekeyBundleResult {
    pub published: bool,
    pub owner_did: String,
    pub owner_device_id: String,
    pub bundle_id: String,
    pub published_at: String,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "deserialize_present"
    )]
    pub published_opk_count: Option<u64>,
}

impl V2PublishPrekeyBundleResult {
    pub fn validate(&self) -> Result<(), DirectE2eeV2Error> {
        if !self.published {
            return Err(DirectE2eeV2Error::invalid(
                "published must be true in a successful result",
            ));
        }
        for (field, value) in [
            ("owner_did", self.owner_did.as_str()),
            ("owner_device_id", self.owner_device_id.as_str()),
            ("bundle_id", self.bundle_id.as_str()),
        ] {
            if value.is_empty() {
                return Err(DirectE2eeV2Error::invalid(format!(
                    "{field} must be a non-empty string"
                )));
            }
        }
        DateTime::parse_from_rfc3339(&self.published_at)
            .map_err(|_| DirectE2eeV2Error::invalid("published_at must be RFC3339"))?;
        Ok(())
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct V2GetPrekeyBundleResult {
    pub target_did: String,
    pub target_device_id: String,
    pub prekey_bundle: V2PrekeyBundle,
    #[serde(
        default,
        skip_serializing_if = "Option::is_none",
        deserialize_with = "deserialize_present"
    )]
    pub one_time_prekey: Option<V2OneTimePrekey>,
}

impl V2GetPrekeyBundleResult {
    pub fn validate(&self) -> Result<(), DirectE2eeV2Error> {
        if self.target_did.is_empty() || self.target_device_id.is_empty() {
            return Err(DirectE2eeV2Error::invalid(
                "target_did and target_device_id must be non-empty",
            ));
        }
        self.prekey_bundle.validate_structure()?;
        if self.target_did != self.prekey_bundle.owner_did
            || self.target_device_id != self.prekey_bundle.owner_device_id
        {
            return Err(DirectE2eeV2Error::invalid(
                "get result target must equal the returned bundle owner",
            ));
        }
        if let Some(one_time_prekey) = &self.one_time_prekey {
            one_time_prekey.validate()?;
        }
        Ok(())
    }
}

pub fn key_service_metadata_v2(
    sender_did: &str,
    sender_device_id: &str,
    service_did: &str,
    operation_id: &str,
) -> V2KeyServiceMetadata {
    V2KeyServiceMetadata {
        anp_version: None,
        profile: DIRECT_E2EE_PROFILE_V2.to_owned(),
        security_profile: TRANSPORT_PROTECTED_SECURITY_PROFILE.to_owned(),
        sender_did: sender_did.to_owned(),
        sender_device_id: sender_device_id.to_owned(),
        target: V2Target {
            kind: "service".to_owned(),
            did: service_did.to_owned(),
        },
        operation_id: operation_id.to_owned(),
        created_at: None,
    }
}