jacs 0.9.13

JACS JSON AI Communication Standard
Documentation
use crate::agent::Agent;
use crate::agent::DOCUMENT_AGENT_SIGNATURE_FIELDNAME;
use crate::agent::document::DocumentTraits;
use crate::error::JacsError;
use crate::replay;
use chrono;
use serde_json::Value;
// use crate::agent::{AGENT_REGISTRATION_SIGNATURE_FIELDNAME, AGENT_SIGNATURE_FIELDNAME, Agent};
// use crate::crypt::KeyManager;
// use crate::crypt::hash::hash_string as jacs_hash_string;

/*
Payloads are data that is designed sent and received once.
There should be no versions of a payload

*/

pub trait PayloadTraits {
    fn sign_payload(&mut self, document: Value) -> Result<String, JacsError>;

    fn verify_payload(
        &mut self,
        document_string: String,
        max_replay_time_delta: Option<u64>,
    ) -> Result<Value, JacsError>;

    fn verify_payload_with_agent_id(
        &mut self,
        document_string: String,
        max_replay_time_delta: Option<u64>,
    ) -> Result<(Value, String), JacsError>;
}

impl PayloadTraits for Agent {
    fn sign_payload(&mut self, jacs_payload: Value) -> Result<String, JacsError> {
        let wrapper_value = serde_json::json!({
            "jacs_payload": jacs_payload
        });

        let wrapper_string = serde_json::to_string(&wrapper_value)?;

        let outputfilename: Option<String> = None;
        let attachments: Option<String> = None;
        let no_save = true;
        let docresult = crate::shared::document_create(
            self,
            &wrapper_string,
            None,
            outputfilename,
            no_save,
            attachments.as_deref(),
            Some(false),
        )?;

        Ok(docresult)
    }

    fn verify_payload(
        &mut self,
        document_string: String,
        max_replay_time_delta: Option<u64>,
    ) -> Result<Value, JacsError> {
        let (payload, _) =
            self.verify_payload_with_agent_id(document_string, max_replay_time_delta)?;
        Ok(payload.clone())
    }

    fn verify_payload_with_agent_id(
        &mut self,
        document_string: String,
        max_replay_time_delta_seconds: Option<u64>,
    ) -> Result<(Value, String), JacsError> {
        let doc = self.load_document(&document_string)?;
        let document_key = doc.getkey();
        let value = doc.getvalue();
        self.verify_hash(value)?;
        self.verify_external_document_signature(&document_key)?;

        let payload = value
            .get("jacs_payload")
            .ok_or_else(|| JacsError::Internal {
                message: "'jacs_payload' field not found".to_string(),
            })?;
        let date = self.get_document_signature_date(&document_key)?;
        let agent_id = self.get_document_signature_agent_id(&document_key)?;

        // Default payload freshness window: 5 minutes.
        // Can be overridden per call, or globally with JACS_PAYLOAD_MAX_REPLAY_SECONDS.
        let max_replay_seconds =
            max_replay_time_delta_seconds.unwrap_or_else(replay::payload_replay_window_seconds);
        let current_time = std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map_err(|e| JacsError::Internal {
                message: e.to_string(),
            })?
            .as_secs();

        // Parse ISO date string to timestamp
        let date_timestamp = chrono::DateTime::parse_from_rfc3339(&date)
            .map_err(|e| JacsError::Internal {
                message: e.to_string(),
            })?
            .timestamp() as u64;

        // Check if signature is too old
        if current_time > date_timestamp && current_time - date_timestamp > max_replay_seconds {
            return Err(JacsError::Internal {
                message: format!(
                    "Signature too old: {} seconds (max allowed: {})",
                    current_time - date_timestamp,
                    max_replay_seconds
                ),
            });
        }

        let jti = value
            .get(DOCUMENT_AGENT_SIGNATURE_FIELDNAME)
            .and_then(|sig| sig.get("jti"))
            .and_then(Value::as_str)
            .map(str::trim)
            .filter(|nonce| !nonce.is_empty())
            .ok_or_else(|| JacsError::Internal {
                message: "Missing or invalid 'jacsSignature.jti' in payload document".to_string(),
            })?;
        replay::check_and_store_nonce(&agent_id, jti)?;

        Ok((payload.clone(), agent_id))
    }
}