use std::collections::HashMap;
use std::path::Path;
use base64::Engine;
use serde_json::Value;
use time::OffsetDateTime;
use uuid::Uuid;
use crate::error::{HaiError, Result};
use crate::types::{
DocSearchResults, DocVerificationResult, MigrateAgentResult, RotationResult, SignedDocument,
SignedPayload, StorageCapabilities, UpdateAgentResult,
};
#[cfg(feature = "jacs-crate")]
pub use jacs::simple::types::{
MediaVerificationResult, MediaVerifyStatus, SignImageOptions, SignTextOptions,
SignTextOutcome, SignedMedia, VerifyImageOptions,
};
#[cfg(feature = "jacs-crate")]
pub use jacs::inline::{
SignatureEntry as TextSignatureEntry, SignatureStatus as TextSignatureStatus,
VerifyOptions as VerifyTextOptions, VerifyTextResult,
};
#[cfg(feature = "jacs-crate")]
pub fn media_verify_status_to_str(s: &MediaVerifyStatus) -> &'static str {
match s {
MediaVerifyStatus::Valid => "valid",
MediaVerifyStatus::InvalidSignature => "invalid_signature",
MediaVerifyStatus::HashMismatch => "hash_mismatch",
MediaVerifyStatus::MissingSignature => "missing_signature",
MediaVerifyStatus::KeyNotFound => "key_not_found",
MediaVerifyStatus::UnsupportedFormat => "unsupported_format",
MediaVerifyStatus::Malformed(_) => "malformed",
}
}
#[cfg(feature = "jacs-crate")]
pub fn text_signature_status_to_str(s: &TextSignatureStatus) -> &'static str {
match s {
TextSignatureStatus::Valid => "valid",
TextSignatureStatus::InvalidSignature => "invalid_signature",
TextSignatureStatus::HashMismatch => "hash_mismatch",
TextSignatureStatus::KeyNotFound => "key_not_found",
TextSignatureStatus::UnsupportedAlgorithm => "unsupported_algorithm",
TextSignatureStatus::Malformed(_) => "malformed",
}
}
#[cfg(feature = "jacs-crate")]
pub fn verify_text_result_to_json(result: &VerifyTextResult) -> Value {
match result {
VerifyTextResult::Signed { signatures } => {
let entries: Vec<Value> = signatures
.iter()
.map(|sig| {
let mut entry = serde_json::json!({
"signer_id": sig.signer_id,
"algorithm": sig.algorithm,
"timestamp": sig.timestamp,
"status": text_signature_status_to_str(&sig.status),
});
if let TextSignatureStatus::Malformed(detail) = &sig.status {
entry["malformed_detail"] = Value::String(detail.clone());
}
entry
})
.collect();
serde_json::json!({
"status": "signed",
"signatures": entries,
})
}
VerifyTextResult::MissingSignature => serde_json::json!({
"status": "missing_signature",
"signatures": [],
}),
VerifyTextResult::Malformed(detail) => serde_json::json!({
"status": "malformed",
"signatures": [],
"malformed_detail": detail,
}),
}
}
#[cfg(feature = "jacs-crate")]
pub fn media_verify_result_to_json(result: &MediaVerificationResult) -> Value {
let mut envelope = serde_json::json!({
"status": media_verify_status_to_str(&result.status),
"signer_id": result.signer_id,
"algorithm": result.algorithm,
"format": result.format,
"embedding_channels": result.embedding_channels,
});
if let MediaVerifyStatus::Malformed(detail) = &result.status {
envelope["malformed_detail"] = Value::String(detail.clone());
}
envelope
}
pub trait JacsProvider: Send + Sync {
fn jacs_id(&self) -> &str;
fn sign_string(&self, message: &str) -> Result<String>;
fn sign_bytes(&self, data: &[u8]) -> Result<Vec<u8>>;
fn key_id(&self) -> &str;
fn algorithm(&self) -> &str;
fn canonical_json(&self, value: &Value) -> Result<String>;
fn sign_envelope(&self, value: &Value) -> Result<String> {
let _ = value;
Err(HaiError::Provider(
"sign_envelope not supported by this provider; use LocalJacsProvider \
or any provider that wraps a real JACS SimpleAgent"
.to_string(),
))
}
fn sign_file_envelope(&self, path: &str, embed: bool) -> Result<SignedDocument> {
let _ = (path, embed);
Err(HaiError::Provider(
"sign_file_envelope not supported by this provider; use LocalJacsProvider \
or any provider that wraps a real JACS SimpleAgent"
.to_string(),
))
}
fn sign_response(&self, payload: &Value) -> Result<SignedPayload>;
fn verify_a2a_artifact(&self, wrapped_json: &str) -> Result<String> {
let wrapped: Value = serde_json::from_str(wrapped_json)?;
let signature = wrapped
.get("jacsSignature")
.and_then(|s| s.get("signature"))
.and_then(|s| s.as_str())
.unwrap_or("");
let signer_id = wrapped
.get("jacsSignature")
.and_then(|s| s.get("agentID"))
.and_then(|s| s.as_str())
.unwrap_or("");
let mut clone = wrapped.clone();
if let Some(obj) = clone.as_object_mut() {
obj.remove("jacsSignature");
}
let canonical = self.canonical_json(&clone)?;
let expected = self.sign_string(&canonical)?;
let valid = signature == expected;
let result = serde_json::json!({
"valid": valid,
"status": if valid { "verified" } else { "invalid" },
"signerId": signer_id,
"artifactType": wrapped.get("jacsType").and_then(|v| v.as_str()).unwrap_or(""),
"timestamp": wrapped.get("jacsVersionDate").and_then(|v| v.as_str()).unwrap_or(""),
"originalArtifact": wrapped.get("a2aArtifact").cloned().unwrap_or(Value::Null),
});
Ok(serde_json::to_string(&result)?)
}
fn sign_email_locally(&self, raw_email: &[u8]) -> Result<Vec<u8>> {
let _ = raw_email;
Err(HaiError::Provider(
"local email signing not supported by this provider; use LocalJacsProvider".to_string(),
))
}
fn rotate(&self) -> Result<RotationResult> {
Err(HaiError::Provider(
"key rotation not supported by this provider; use LocalJacsProvider".to_string(),
))
}
fn export_agent_json(&self) -> Result<String> {
Err(HaiError::Provider(
"export_agent_json not supported by this provider; use LocalJacsProvider".to_string(),
))
}
fn update_agent(&self, new_agent_data: &str) -> Result<UpdateAgentResult> {
let _ = new_agent_data;
Err(HaiError::Provider(
"update_agent not supported by this provider; use LocalJacsProvider".to_string(),
))
}
}
pub trait JacsAgentLifecycle: JacsProvider {
fn lifecycle_rotate(&self) -> Result<RotationResult>;
fn lifecycle_migrate(config_path: Option<&Path>) -> Result<MigrateAgentResult>
where
Self: Sized;
fn lifecycle_update_agent(&self, new_data: &str) -> Result<UpdateAgentResult>;
fn lifecycle_export_agent_json(&self) -> Result<String>;
fn diagnostics(&self) -> Result<Value>;
fn verify_self(&self) -> Result<DocVerificationResult>;
fn quickstart(
name: &str,
domain: &str,
description: Option<&str>,
algorithm: Option<&str>,
config_path: Option<&str>,
) -> Result<Value>
where
Self: Sized;
fn reencrypt_key(&self, old_password: &str, new_password: &str) -> Result<()>;
fn get_setup_instructions(&self, domain: &str, ttl: u32) -> Result<Value>;
}
pub trait JacsDocumentProvider: JacsProvider {
fn sign_document(&self, data: &Value) -> Result<String>;
fn store_document(&self, signed_json: &str) -> Result<String>;
fn sign_and_store(&self, data: &Value) -> Result<SignedDocument>;
fn sign_file(&self, path: &str, embed: bool) -> Result<SignedDocument>;
fn get_document(&self, key: &str) -> Result<String>;
fn list_documents(&self, jacs_type: Option<&str>) -> Result<Vec<String>>;
fn get_document_versions(&self, doc_id: &str) -> Result<Vec<String>>;
fn get_latest_document(&self, doc_id: &str) -> Result<String>;
fn remove_document(&self, key: &str) -> Result<()>;
fn update_document(&self, doc_id: &str, data: &str) -> Result<SignedDocument>;
fn search_documents(
&self,
query: &str,
limit: usize,
offset: usize,
) -> Result<DocSearchResults>;
fn query_by_type(
&self,
doc_type: &str,
limit: usize,
offset: usize,
) -> Result<Vec<String>>;
fn query_by_field(
&self,
field: &str,
value: &str,
limit: usize,
offset: usize,
) -> Result<Vec<String>>;
fn query_by_agent(
&self,
agent_id: &str,
limit: usize,
offset: usize,
) -> Result<Vec<String>>;
fn storage_capabilities(&self) -> Result<StorageCapabilities>;
fn save_memory(&self, _content: Option<&str>) -> Result<String> {
Err(HaiError::Provider(
"save_memory: not implemented for this provider".to_string(),
))
}
fn save_soul(&self, _content: Option<&str>) -> Result<String> {
Err(HaiError::Provider(
"save_soul: not implemented for this provider".to_string(),
))
}
fn get_memory(&self) -> Result<Option<String>> {
Err(HaiError::Provider(
"get_memory: not implemented for this provider".to_string(),
))
}
fn get_soul(&self) -> Result<Option<String>> {
Err(HaiError::Provider(
"get_soul: not implemented for this provider".to_string(),
))
}
fn store_text_file(&self, _path: &str) -> Result<String> {
Err(HaiError::Provider(
"store_text_file: not implemented for this provider".to_string(),
))
}
fn store_image_file(&self, _path: &str) -> Result<String> {
Err(HaiError::Provider(
"store_image_file: not implemented for this provider".to_string(),
))
}
fn get_record_bytes(&self, _key: &str) -> Result<Vec<u8>> {
Err(HaiError::Provider(
"get_record_bytes: not implemented for this provider".to_string(),
))
}
}
pub trait JacsBatchProvider: JacsProvider {
fn sign_messages(&self, messages: &[&Value]) -> Result<Vec<SignedDocument>>;
fn verify_batch(&self, documents: &[&str]) -> Vec<DocVerificationResult>;
}
pub trait JacsVerificationProvider: JacsProvider {
fn verify_document(&self, document: &str) -> Result<DocVerificationResult>;
fn verify_with_key(&self, document: &str, key: Vec<u8>) -> Result<DocVerificationResult>;
fn verify_by_id(&self, doc_id: &str) -> Result<DocVerificationResult>;
fn verify_dns(&self, domain: &str) -> Result<()>;
fn build_auth_header_jacs(&self) -> Result<String>;
fn unwrap_signed_event(
&self,
event: &Value,
server_public_keys: &HashMap<String, Vec<u8>>,
) -> Result<(Value, bool)>;
}
pub trait JacsEmailProvider: JacsProvider {
fn sign_email(&self, raw: &[u8]) -> Result<Vec<u8>>;
fn verify_email(&self, raw: &[u8], key: Vec<u8>) -> Result<Value>;
fn add_jacs_attachment(&self, email: &[u8], doc: &[u8]) -> Result<Vec<u8>>;
fn get_jacs_attachment(&self, email: &[u8]) -> Result<Vec<u8>>;
fn remove_jacs_attachment(&self, email: &[u8]) -> Result<Vec<u8>>;
fn extract_email_parts(&self, raw: &[u8]) -> Result<Value>;
}
#[cfg(feature = "jacs-crate")]
pub trait JacsMediaProvider: JacsProvider {
fn sign_text_file(&self, path: &str, opts: SignTextOptions) -> Result<SignTextOutcome>;
fn verify_text_file(&self, path: &str, opts: VerifyTextOptions) -> Result<VerifyTextResult>;
fn sign_image(
&self,
in_path: &str,
out_path: &str,
opts: SignImageOptions,
) -> Result<SignedMedia>;
fn verify_image(&self, path: &str, opts: VerifyImageOptions) -> Result<MediaVerificationResult>;
fn extract_media_signature(&self, path: &str, raw_payload: bool) -> Result<Option<String>>;
}
#[cfg(feature = "agreements")]
pub trait JacsAgreementProvider: JacsProvider {
fn create_agreement(
&self,
doc: &str,
agent_ids: &[String],
quorum: Option<&str>,
) -> Result<SignedDocument>;
fn sign_agreement(&self, document: &str) -> Result<SignedDocument>;
fn check_agreement(&self, document: &str) -> Result<Value>;
}
#[cfg(feature = "attestation")]
pub trait JacsAttestationProvider: JacsProvider {
fn create_attestation(&self, subject: &Value, claims: &[Value]) -> Result<String>;
fn verify_attestation(&self, doc_key: &str) -> Result<Value>;
}
impl JacsProvider for Box<dyn JacsProvider> {
fn jacs_id(&self) -> &str {
(**self).jacs_id()
}
fn sign_string(&self, message: &str) -> Result<String> {
(**self).sign_string(message)
}
fn sign_bytes(&self, data: &[u8]) -> Result<Vec<u8>> {
(**self).sign_bytes(data)
}
fn key_id(&self) -> &str {
(**self).key_id()
}
fn algorithm(&self) -> &str {
(**self).algorithm()
}
fn canonical_json(&self, value: &Value) -> Result<String> {
(**self).canonical_json(value)
}
fn sign_response(&self, payload: &Value) -> Result<SignedPayload> {
(**self).sign_response(payload)
}
fn verify_a2a_artifact(&self, wrapped_json: &str) -> Result<String> {
(**self).verify_a2a_artifact(wrapped_json)
}
fn sign_email_locally(&self, raw_email: &[u8]) -> Result<Vec<u8>> {
(**self).sign_email_locally(raw_email)
}
fn rotate(&self) -> Result<RotationResult> {
(**self).rotate()
}
fn export_agent_json(&self) -> Result<String> {
(**self).export_agent_json()
}
fn update_agent(&self, new_agent_data: &str) -> Result<UpdateAgentResult> {
(**self).update_agent(new_agent_data)
}
}
#[cfg(feature = "jacs-crate")]
impl JacsProvider for Box<dyn JacsMediaProvider> {
fn jacs_id(&self) -> &str {
(**self).jacs_id()
}
fn sign_string(&self, message: &str) -> Result<String> {
(**self).sign_string(message)
}
fn sign_bytes(&self, data: &[u8]) -> Result<Vec<u8>> {
(**self).sign_bytes(data)
}
fn key_id(&self) -> &str {
(**self).key_id()
}
fn algorithm(&self) -> &str {
(**self).algorithm()
}
fn canonical_json(&self, value: &Value) -> Result<String> {
(**self).canonical_json(value)
}
fn sign_response(&self, payload: &Value) -> Result<SignedPayload> {
(**self).sign_response(payload)
}
fn verify_a2a_artifact(&self, wrapped_json: &str) -> Result<String> {
(**self).verify_a2a_artifact(wrapped_json)
}
fn sign_email_locally(&self, raw_email: &[u8]) -> Result<Vec<u8>> {
(**self).sign_email_locally(raw_email)
}
fn rotate(&self) -> Result<RotationResult> {
(**self).rotate()
}
fn export_agent_json(&self) -> Result<String> {
(**self).export_agent_json()
}
fn update_agent(&self, new_agent_data: &str) -> Result<UpdateAgentResult> {
(**self).update_agent(new_agent_data)
}
}
#[cfg(feature = "jacs-crate")]
impl JacsMediaProvider for Box<dyn JacsMediaProvider> {
fn sign_text_file(&self, path: &str, opts: SignTextOptions) -> Result<SignTextOutcome> {
(**self).sign_text_file(path, opts)
}
fn verify_text_file(&self, path: &str, opts: VerifyTextOptions) -> Result<VerifyTextResult> {
(**self).verify_text_file(path, opts)
}
fn sign_image(
&self,
in_path: &str,
out_path: &str,
opts: SignImageOptions,
) -> Result<SignedMedia> {
(**self).sign_image(in_path, out_path, opts)
}
fn verify_image(&self, path: &str, opts: VerifyImageOptions) -> Result<MediaVerificationResult> {
(**self).verify_image(path, opts)
}
fn extract_media_signature(&self, path: &str, raw_payload: bool) -> Result<Option<String>> {
(**self).extract_media_signature(path, raw_payload)
}
}
#[derive(Debug, Clone)]
pub struct NoopJacsProvider {
jacs_id: String,
}
impl NoopJacsProvider {
pub fn new(jacs_id: impl Into<String>) -> Self {
Self {
jacs_id: jacs_id.into(),
}
}
}
impl JacsProvider for NoopJacsProvider {
fn jacs_id(&self) -> &str {
&self.jacs_id
}
fn sign_string(&self, _message: &str) -> Result<String> {
Err(HaiError::Provider(
"no JACS signer configured; provide a real JacsProvider".to_string(),
))
}
fn sign_bytes(&self, _data: &[u8]) -> Result<Vec<u8>> {
Err(HaiError::Provider(
"no JACS signer configured; provide a real JacsProvider".to_string(),
))
}
fn key_id(&self) -> &str {
&self.jacs_id
}
fn algorithm(&self) -> &str {
"none"
}
fn canonical_json(&self, value: &Value) -> Result<String> {
Ok(canonicalize_json_rfc8785(value))
}
fn sign_response(&self, _payload: &Value) -> Result<SignedPayload> {
Err(HaiError::Provider(
"no JACS response signer configured; provide a real JacsProvider".to_string(),
))
}
}
#[derive(Debug, Clone)]
pub struct StaticJacsProvider {
jacs_id: String,
algorithm: String,
}
impl StaticJacsProvider {
pub fn new(jacs_id: impl Into<String>) -> Self {
Self {
jacs_id: jacs_id.into(),
algorithm: "ed25519".to_string(),
}
}
pub fn with_algorithm(jacs_id: impl Into<String>, algorithm: impl Into<String>) -> Self {
Self {
jacs_id: jacs_id.into(),
algorithm: algorithm.into(),
}
}
}
impl JacsProvider for StaticJacsProvider {
fn jacs_id(&self) -> &str {
&self.jacs_id
}
fn sign_email_locally(&self, raw_email: &[u8]) -> Result<Vec<u8>> {
Ok(raw_email.to_vec())
}
fn sign_string(&self, message: &str) -> Result<String> {
let raw = format!("sig:{}", message);
Ok(base64::engine::general_purpose::STANDARD.encode(raw))
}
fn sign_bytes(&self, data: &[u8]) -> Result<Vec<u8>> {
let mut result = b"sig:".to_vec();
result.extend_from_slice(data);
Ok(result)
}
fn key_id(&self) -> &str {
&self.jacs_id
}
fn algorithm(&self) -> &str {
&self.algorithm
}
fn canonical_json(&self, value: &Value) -> Result<String> {
Ok(canonicalize_json_rfc8785(value))
}
fn sign_envelope(&self, value: &Value) -> Result<String> {
let now = OffsetDateTime::now_utc()
.format(&time::format_description::well_known::Rfc3339)
.map_err(|e| HaiError::Provider(format!("failed to format timestamp: {e}")))?;
let mut envelope = value.clone();
if let Value::Object(map) = &mut envelope {
map.entry("jacsId".to_string())
.or_insert_with(|| Value::String(Uuid::new_v4().to_string()));
map.entry("jacsVersion".to_string())
.or_insert_with(|| Value::String(Uuid::new_v4().to_string()));
map.entry("jacsVersionDate".to_string())
.or_insert_with(|| Value::String(now.clone()));
map.entry("jacsType".to_string())
.or_insert_with(|| Value::String("document".to_string()));
let canonical = canonicalize_json_rfc8785(&Value::Object(map.clone()));
let signature = self.sign_string(&canonical)?;
map.insert(
"jacsSignature".to_string(),
serde_json::json!({
"agentID": self.jacs_id,
"agentVersion": "test-stub",
"date": now,
"signature": signature,
"signingAlgorithm": self.algorithm,
"publicKeyHash": "test-stub-hash",
"fields": [],
}),
);
}
serde_json::to_string(&envelope).map_err(|e| {
HaiError::Provider(format!("StaticJacsProvider::sign_envelope serialise: {e}"))
})
}
fn sign_response(&self, payload: &Value) -> Result<SignedPayload> {
let canonical_payload = canonicalize_json_rfc8785(payload);
let data = serde_json::from_str::<Value>(&canonical_payload)?;
let now = OffsetDateTime::now_utc()
.format(&time::format_description::well_known::Rfc3339)
.map_err(|e| HaiError::Provider(format!("failed to format timestamp: {e}")))?;
let doc = serde_json::json!({
"version": "1.0.0",
"document_type": "job_response",
"data": data,
"metadata": {
"issuer": self.jacs_id,
"document_id": Uuid::new_v4().to_string(),
"created_at": now,
"hash": "",
},
"jacsSignature": {
"agentID": self.jacs_id,
"date": now,
"signature": self.sign_string(&canonical_payload)?,
},
});
Ok(SignedPayload {
signed_document: serde_json::to_string(&doc)?,
agent_jacs_id: self.jacs_id.clone(),
})
}
}
#[cfg(feature = "jacs-crate")]
fn media_op_test_only_error(provider: &str, op: &str) -> HaiError {
HaiError::Provider(format!(
"media operation '{op}' requires a real LocalJacsProvider — current provider is {provider} (test-only)"
))
}
#[cfg(feature = "jacs-crate")]
impl JacsMediaProvider for NoopJacsProvider {
fn sign_text_file(&self, _path: &str, _opts: SignTextOptions) -> Result<SignTextOutcome> {
Err(media_op_test_only_error("NoopJacsProvider", "sign_text_file"))
}
fn verify_text_file(
&self,
_path: &str,
_opts: VerifyTextOptions,
) -> Result<VerifyTextResult> {
Err(media_op_test_only_error("NoopJacsProvider", "verify_text_file"))
}
fn sign_image(
&self,
_in_path: &str,
_out_path: &str,
_opts: SignImageOptions,
) -> Result<SignedMedia> {
Err(media_op_test_only_error("NoopJacsProvider", "sign_image"))
}
fn verify_image(
&self,
_path: &str,
_opts: VerifyImageOptions,
) -> Result<MediaVerificationResult> {
Err(media_op_test_only_error("NoopJacsProvider", "verify_image"))
}
fn extract_media_signature(
&self,
_path: &str,
_raw_payload: bool,
) -> Result<Option<String>> {
Err(media_op_test_only_error(
"NoopJacsProvider",
"extract_media_signature",
))
}
}
#[cfg(feature = "jacs-crate")]
impl JacsMediaProvider for StaticJacsProvider {
fn sign_text_file(&self, _path: &str, _opts: SignTextOptions) -> Result<SignTextOutcome> {
Err(media_op_test_only_error(
"StaticJacsProvider",
"sign_text_file",
))
}
fn verify_text_file(
&self,
_path: &str,
_opts: VerifyTextOptions,
) -> Result<VerifyTextResult> {
Err(media_op_test_only_error(
"StaticJacsProvider",
"verify_text_file",
))
}
fn sign_image(
&self,
_in_path: &str,
_out_path: &str,
_opts: SignImageOptions,
) -> Result<SignedMedia> {
Err(media_op_test_only_error("StaticJacsProvider", "sign_image"))
}
fn verify_image(
&self,
_path: &str,
_opts: VerifyImageOptions,
) -> Result<MediaVerificationResult> {
Err(media_op_test_only_error(
"StaticJacsProvider",
"verify_image",
))
}
fn extract_media_signature(
&self,
_path: &str,
_raw_payload: bool,
) -> Result<Option<String>> {
Err(media_op_test_only_error(
"StaticJacsProvider",
"extract_media_signature",
))
}
}
#[cfg(feature = "jacs-crate")]
pub fn canonicalize_json_rfc8785(value: &Value) -> String {
jacs::protocol::canonicalize_json(value)
}
#[cfg(all(not(feature = "jacs-crate"), feature = "serde_json_canonicalizer"))]
pub fn canonicalize_json_rfc8785(value: &Value) -> String {
serde_json_canonicalizer::to_string(value).unwrap_or_else(|_| "null".to_string())
}
#[cfg(all(not(feature = "jacs-crate"), not(feature = "serde_json_canonicalizer")))]
compile_error!(
"Either `jacs-crate` or `serde_json_canonicalizer` feature must be enabled for JSON canonicalization"
);