1use base64::{Engine as _, engine::general_purpose};
10use jacs::agent::agreement::Agreement;
11use jacs::agent::boilerplate::BoilerPlate;
12use jacs::agent::document::{DocumentTraits, JACSDocument};
13use jacs::agent::payloads::PayloadTraits;
14use jacs::agent::{
15 AGENT_AGREEMENT_FIELDNAME, AGENT_REGISTRATION_SIGNATURE_FIELDNAME, AGENT_SIGNATURE_FIELDNAME,
16 Agent,
17};
18use jacs::config::Config;
19use jacs::crypt::KeyManager;
20use jacs::crypt::hash::hash_string as jacs_hash_string;
21use reqwest::blocking::Client as BlockingClient;
22use reqwest::header::{ACCEPT, CONTENT_TYPE};
23use reqwest::{StatusCode, Url};
24use serde_json::{Value, json};
25use std::collections::HashMap;
26use std::fs;
27use std::fs::OpenOptions;
28use std::io::Write;
29#[cfg(unix)]
30use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
31use std::path::{Path, PathBuf};
32use std::str::FromStr;
33use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
34
35#[cfg(feature = "agreements")]
36pub mod agreement_v2;
37pub mod conversion;
38pub mod doc_wrapper;
39pub mod simple_wrapper;
40
41pub use doc_wrapper::DocumentServiceWrapper;
42pub use simple_wrapper::SimpleAgentWrapper;
43pub use simple_wrapper::sign_message_json;
44pub use simple_wrapper::verify_json;
45
46#[derive(Debug)]
51pub struct BindingCoreError {
52 pub message: String,
53 pub kind: ErrorKind,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
58pub enum ErrorKind {
59 LockFailed,
61 AgentLoad,
63 Validation,
65 SigningFailed,
67 VerificationFailed,
69 DocumentFailed,
71 AgreementFailed,
73 SerializationFailed,
75 InvalidArgument,
77 TrustFailed,
79 NetworkFailed,
81 KeyNotFound,
83 MissingSignature,
86 Generic,
88}
89
90impl BindingCoreError {
91 pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
92 Self {
93 message: message.into(),
94 kind,
95 }
96 }
97
98 pub fn lock_failed(message: impl Into<String>) -> Self {
99 Self::new(ErrorKind::LockFailed, message)
100 }
101
102 pub fn agent_load(message: impl Into<String>) -> Self {
103 Self::new(ErrorKind::AgentLoad, message)
104 }
105
106 pub fn validation(message: impl Into<String>) -> Self {
107 Self::new(ErrorKind::Validation, message)
108 }
109
110 pub fn signing_failed(message: impl Into<String>) -> Self {
111 Self::new(ErrorKind::SigningFailed, message)
112 }
113
114 pub fn verification_failed(message: impl Into<String>) -> Self {
115 Self::new(ErrorKind::VerificationFailed, message)
116 }
117
118 pub fn document_failed(message: impl Into<String>) -> Self {
119 Self::new(ErrorKind::DocumentFailed, message)
120 }
121
122 pub fn agreement_failed(message: impl Into<String>) -> Self {
123 Self::new(ErrorKind::AgreementFailed, message)
124 }
125
126 pub fn serialization_failed(message: impl Into<String>) -> Self {
127 Self::new(ErrorKind::SerializationFailed, message)
128 }
129
130 pub fn invalid_argument(message: impl Into<String>) -> Self {
131 Self::new(ErrorKind::InvalidArgument, message)
132 }
133
134 pub fn trust_failed(message: impl Into<String>) -> Self {
135 Self::new(ErrorKind::TrustFailed, message)
136 }
137
138 pub fn network_failed(message: impl Into<String>) -> Self {
139 Self::new(ErrorKind::NetworkFailed, message)
140 }
141
142 pub fn key_not_found(message: impl Into<String>) -> Self {
143 Self::new(ErrorKind::KeyNotFound, message)
144 }
145
146 pub fn missing_signature(message: impl Into<String>) -> Self {
151 Self::new(ErrorKind::MissingSignature, message)
152 }
153
154 pub fn generic(message: impl Into<String>) -> Self {
155 Self::new(ErrorKind::Generic, message)
156 }
157}
158
159impl std::fmt::Display for BindingCoreError {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 write!(f, "{}", self.message)
162 }
163}
164
165impl std::error::Error for BindingCoreError {}
166
167impl<T> From<PoisonError<T>> for BindingCoreError {
168 fn from(e: PoisonError<T>) -> Self {
169 Self::lock_failed(format!("Failed to acquire lock: {}", e))
170 }
171}
172
173pub type BindingResult<T> = Result<T, BindingCoreError>;
175
176fn serialize_agent_info(info: &jacs::simple::AgentInfo) -> BindingResult<String> {
177 serde_json::to_string(info).map_err(|e| {
178 BindingCoreError::serialization_failed(format!("Failed to serialize AgentInfo: {}", e))
179 })
180}
181
182fn resolve_existing_config_path(config_path: &str) -> BindingResult<String> {
183 let requested = Path::new(config_path);
184 let resolved = if requested.is_absolute() {
185 requested.to_path_buf()
186 } else {
187 std::env::current_dir()
188 .map_err(|e| {
189 BindingCoreError::agent_load(format!(
190 "Failed to determine current working directory: {}",
191 e
192 ))
193 })?
194 .join(requested)
195 };
196
197 if !resolved.exists() {
198 return Err(BindingCoreError::agent_load(format!(
199 "Config file not found: {}",
200 resolved.display()
201 )));
202 }
203
204 Ok(normalize_path(&resolved).to_string_lossy().into_owned())
205}
206
207fn normalize_path(path: &Path) -> PathBuf {
208 let mut normalized = PathBuf::new();
209 for component in path.components() {
210 match component {
211 std::path::Component::CurDir => {}
212 std::path::Component::ParentDir => {
213 normalized.pop();
214 }
215 other => normalized.push(other.as_os_str()),
216 }
217 }
218 normalized
219}
220
221fn resolve_path_from_cwd(path: &str) -> BindingResult<PathBuf> {
222 let requested = Path::new(path);
223 if requested.is_absolute() {
224 return Ok(normalize_path(requested));
225 }
226
227 Ok(normalize_path(
228 &std::env::current_dir()
229 .map_err(|e| {
230 BindingCoreError::agent_load(format!(
231 "Failed to determine current working directory: {}",
232 e
233 ))
234 })?
235 .join(requested),
236 ))
237}
238
239fn resolve_relative_to_config(config_path: &Path, candidate: &str) -> PathBuf {
240 let candidate_path = Path::new(candidate);
241 if candidate_path.is_absolute() {
242 return normalize_path(candidate_path);
243 }
244
245 let base_dir = config_path.parent().unwrap_or_else(|| Path::new("."));
246 normalize_path(&base_dir.join(candidate_path))
247}
248
249fn read_password_file(path: &Path) -> BindingResult<Option<String>> {
250 if !path.exists() {
251 return Ok(None);
252 }
253
254 #[cfg(unix)]
255 {
256 let metadata = fs::metadata(path).map_err(|e| {
257 BindingCoreError::generic(format!(
258 "Failed to inspect password file {}: {}",
259 path.display(),
260 e
261 ))
262 })?;
263 let mode = metadata.permissions().mode() & 0o777;
264 if mode & 0o077 != 0 {
265 return Err(BindingCoreError::generic(format!(
266 "Password file {} has insecure permissions (mode {:04o}). \
267 File must not be group-readable or world-readable.",
268 path.display(),
269 mode
270 )));
271 }
272 if !metadata.is_file() {
273 return Err(BindingCoreError::generic(format!(
274 "Password file {} is not a regular file.",
275 path.display()
276 )));
277 }
278 }
279
280 let contents = fs::read_to_string(path).map_err(|e| {
281 BindingCoreError::generic(format!(
282 "Failed to read password file {}: {}",
283 path.display(),
284 e
285 ))
286 })?;
287 let password = contents.trim_end_matches(['\n', '\r']).trim();
288 if password.is_empty() {
289 return Ok(None);
290 }
291 Ok(Some(password.to_string()))
292}
293
294fn missing_password_message(error: &str) -> bool {
295 error.contains("No private key password available")
296}
297
298fn truthy_env_var(name: &str) -> bool {
299 std::env::var(name)
300 .ok()
301 .map(|value| {
302 let value = value.trim();
303 value.eq_ignore_ascii_case("true") || value == "1"
304 })
305 .unwrap_or(false)
306}
307
308const DEFAULT_NETWORK_TIMEOUT_MS: u64 = 10_000;
309const DEFAULT_KEYS_BASE_URL: &str = "https://hai.ai";
310
311#[cfg(feature = "a2a")]
312fn a2a_default_skills() -> Vec<jacs::a2a::AgentSkill> {
313 vec![jacs::a2a::AgentSkill {
314 id: "verify-signature".to_string(),
315 name: "verify_signature".to_string(),
316 description: "Verify JACS document signatures".to_string(),
317 tags: vec![
318 "jacs".to_string(),
319 "verification".to_string(),
320 "cryptography".to_string(),
321 ],
322 examples: Some(vec![
323 "Verify a signed JACS document".to_string(),
324 "Check document signature integrity".to_string(),
325 ]),
326 input_modes: Some(vec!["application/json".to_string()]),
327 output_modes: Some(vec!["application/json".to_string()]),
328 security: None,
329 }]
330}
331
332#[cfg(feature = "a2a")]
333fn slugify_skill_name(name: &str) -> String {
334 name.to_lowercase()
335 .replace([' ', '_'], "-")
336 .chars()
337 .filter(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || *c == '-')
338 .collect()
339}
340
341#[cfg(feature = "a2a")]
342fn string_array_field(skill: &Value, field: &str) -> Option<Vec<String>> {
343 skill
344 .get(field)
345 .and_then(|v| v.as_array())
346 .map(|values| {
347 values
348 .iter()
349 .filter_map(|v| v.as_str().map(ToOwned::to_owned))
350 .collect::<Vec<_>>()
351 })
352 .filter(|values| !values.is_empty())
353}
354
355#[cfg(feature = "a2a")]
356fn explicit_a2a_skills(agent_value: &Value) -> Vec<jacs::a2a::AgentSkill> {
357 let raw_skills = agent_value
358 .get("skills")
359 .or_else(|| agent_value.get("a2aSkills"))
360 .and_then(|v| v.as_array());
361
362 let Some(raw_skills) = raw_skills else {
363 return a2a_default_skills();
364 };
365
366 let skills = raw_skills
367 .iter()
368 .map(|raw| {
369 let name = raw
370 .get("name")
371 .or_else(|| raw.get("id"))
372 .and_then(|v| v.as_str())
373 .unwrap_or("unnamed");
374 jacs::a2a::AgentSkill {
375 id: raw
376 .get("id")
377 .and_then(|v| v.as_str())
378 .map(ToOwned::to_owned)
379 .unwrap_or_else(|| slugify_skill_name(name)),
380 name: name.to_string(),
381 description: raw
382 .get("description")
383 .and_then(|v| v.as_str())
384 .unwrap_or_default()
385 .to_string(),
386 tags: string_array_field(raw, "tags").unwrap_or_else(|| vec!["jacs".to_string()]),
387 examples: string_array_field(raw, "examples"),
388 input_modes: string_array_field(raw, "inputModes")
389 .or_else(|| string_array_field(raw, "input_modes")),
390 output_modes: string_array_field(raw, "outputModes")
391 .or_else(|| string_array_field(raw, "output_modes")),
392 security: raw
393 .get("security")
394 .and_then(|v| v.as_array())
395 .map(|values| values.to_vec()),
396 }
397 })
398 .collect::<Vec<_>>();
399
400 if skills.is_empty() {
401 a2a_default_skills()
402 } else {
403 skills
404 }
405}
406
407fn build_blocking_json_client(timeout_ms: u64) -> BindingResult<BlockingClient> {
408 BlockingClient::builder()
409 .timeout(std::time::Duration::from_millis(timeout_ms.max(1)))
410 .build()
411 .map_err(|e| {
412 BindingCoreError::network_failed(format!("Failed to build HTTP client: {}", e))
413 })
414}
415
416fn is_loopback_host(host: &str) -> bool {
417 matches!(
418 host.trim().trim_matches(['[', ']']),
419 "localhost" | "127.0.0.1" | "::1"
420 )
421}
422
423fn validate_network_url(url: &Url, description: &str) -> BindingResult<()> {
424 match url.scheme() {
425 "https" => Ok(()),
426 "http" if url.host_str().is_some_and(is_loopback_host) => Ok(()),
427 "http" => Err(BindingCoreError::network_failed(format!(
428 "{} must use HTTPS (got '{}'). Only localhost URLs are allowed over HTTP for testing.",
429 description, url
430 ))),
431 other => Err(BindingCoreError::invalid_argument(format!(
432 "{} must use http or https (got scheme '{}')",
433 description, other
434 ))),
435 }
436}
437
438fn content_type_header(response: &reqwest::blocking::Response) -> String {
439 response
440 .headers()
441 .get(CONTENT_TYPE)
442 .and_then(|value| value.to_str().ok())
443 .unwrap_or("")
444 .to_string()
445}
446
447fn parse_json_object_body(
448 body: &str,
449 invalid_json_message: String,
450 non_object_message: String,
451) -> BindingResult<String> {
452 let value: Value = serde_json::from_str(body)
453 .map_err(|e| BindingCoreError::validation(format!("{}: {}", invalid_json_message, e)))?;
454 if !value.is_object() {
455 return Err(BindingCoreError::validation(non_object_message));
456 }
457 serde_json::to_string(&value).map_err(|e| {
458 BindingCoreError::serialization_failed(format!("Failed to serialize JSON response: {}", e))
459 })
460}
461
462fn resolve_keys_base_url(override_base_url: Option<&str>) -> String {
463 if let Some(value) = override_base_url {
464 let trimmed = value.trim();
465 if !trimmed.is_empty() {
466 return trimmed.trim_end_matches('/').to_string();
467 }
468 }
469
470 if let Ok(value) = std::env::var("JACS_KEYS_BASE_URL") {
471 let trimmed = value.trim();
472 if !trimmed.is_empty() {
473 return trimmed.trim_end_matches('/').to_string();
474 }
475 }
476
477 if let Ok(value) = std::env::var("HAI_KEYS_BASE_URL") {
478 let trimmed = value.trim();
479 if !trimmed.is_empty() {
480 return trimmed.trim_end_matches('/').to_string();
481 }
482 }
483 DEFAULT_KEYS_BASE_URL.to_string()
484}
485
486fn normalize_public_key_hash(public_key_hash: &str) -> BindingResult<String> {
487 let trimmed = public_key_hash.trim();
488 if trimmed.is_empty() {
489 return Err(BindingCoreError::invalid_argument(
490 "public_key_hash cannot be empty",
491 ));
492 }
493 if trimmed.starts_with("sha256:") {
494 Ok(trimmed.to_string())
495 } else {
496 Ok(format!("sha256:{}", trimmed))
497 }
498}
499
500fn decode_public_key_base64(public_key_b64: &str) -> BindingResult<Vec<u8>> {
501 for engine in [
502 &general_purpose::STANDARD,
503 &general_purpose::STANDARD_NO_PAD,
504 &general_purpose::URL_SAFE,
505 &general_purpose::URL_SAFE_NO_PAD,
506 ] {
507 if let Ok(decoded) = engine.decode(public_key_b64) {
508 return Ok(decoded);
509 }
510 }
511
512 Err(BindingCoreError::invalid_argument(
513 "Public key must be valid base64 or base64url text.",
514 ))
515}
516
517fn build_jwk_set_from_public_key_bytes(
518 public_key: &[u8],
519 key_algorithm: &str,
520 key_id: &str,
521) -> BindingResult<Value> {
522 let normalized_algorithm = key_algorithm.trim().to_ascii_lowercase();
523
524 if normalized_algorithm.contains("ed25519")
525 || (normalized_algorithm.is_empty() && public_key.len() == 32)
526 {
527 if public_key.len() != 32 {
528 return Err(BindingCoreError::invalid_argument(format!(
529 "Ed25519 public key must be 32 bytes, got {} bytes.",
530 public_key.len()
531 )));
532 }
533
534 return Ok(json!({
535 "keys": [{
536 "kty": "OKP",
537 "crv": "Ed25519",
538 "x": general_purpose::URL_SAFE_NO_PAD.encode(public_key),
539 "kid": key_id,
540 "use": "sig",
541 "alg": "EdDSA",
542 }]
543 }));
544 }
545
546 Ok(json!({ "keys": [] }))
547}
548
549fn resolve_password_context(
550 config_path: Option<&str>,
551 key_directory: Option<&str>,
552) -> BindingResult<(PathBuf, Option<String>)> {
553 let mut agent_id = None;
554
555 if let Some(config_path) = config_path {
556 let resolved_config_path = resolve_path_from_cwd(config_path)?;
557 if resolved_config_path.exists() {
558 let config = Config::from_file(resolved_config_path.to_string_lossy().as_ref())
559 .map_err(|e| {
560 BindingCoreError::agent_load(format!(
561 "Failed to load config from {}: {}",
562 resolved_config_path.display(),
563 e
564 ))
565 })?;
566 let configured_key_dir = config
567 .jacs_key_directory()
568 .as_deref()
569 .unwrap_or("./jacs_keys");
570 let resolved_key_dir =
571 resolve_relative_to_config(&resolved_config_path, configured_key_dir);
572 agent_id = config.jacs_agent_id_and_version().clone();
573
574 if let Some(key_directory) = key_directory {
575 return Ok((resolve_path_from_cwd(key_directory)?, agent_id));
576 }
577 return Ok((resolved_key_dir, agent_id));
578 }
579
580 if let Some(key_directory) = key_directory {
581 return Ok((resolve_path_from_cwd(key_directory)?, None));
582 }
583
584 return Ok((
585 resolve_relative_to_config(&resolved_config_path, "./jacs_keys"),
586 None,
587 ));
588 }
589
590 if let Some(key_directory) = key_directory {
591 return Ok((resolve_path_from_cwd(key_directory)?, None));
592 }
593
594 Ok((resolve_path_from_cwd("./jacs_keys")?, agent_id))
595}
596
597fn generate_private_key_password_value() -> String {
598 use rand::Rng;
599
600 const UPPER: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
601 const LOWER: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
602 const DIGITS: &[u8] = b"0123456789";
603 const SPECIAL: &[u8] = b"!@#$%^&*()-_=+";
604 const ALL: &[u8] =
605 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+";
606
607 let mut rng = rand::rng();
608 let mut password = String::with_capacity(32);
609
610 password.push(UPPER[rng.random_range(0..UPPER.len())] as char);
611 password.push(LOWER[rng.random_range(0..LOWER.len())] as char);
612 password.push(DIGITS[rng.random_range(0..DIGITS.len())] as char);
613 password.push(SPECIAL[rng.random_range(0..SPECIAL.len())] as char);
614
615 for _ in 4..32 {
616 password.push(ALL[rng.random_range(0..ALL.len())] as char);
617 }
618
619 password
620}
621
622fn persist_password_file(key_directory: &Path, password: &str) -> BindingResult<()> {
623 fs::create_dir_all(key_directory).map_err(|e| {
624 BindingCoreError::generic(format!(
625 "Failed to create key directory {}: {}",
626 key_directory.display(),
627 e
628 ))
629 })?;
630
631 let password_path = key_directory.join(".jacs_password");
632 let mut options = OpenOptions::new();
633 options.write(true).create_new(true);
634
635 #[cfg(unix)]
636 {
637 options.mode(0o600);
638 }
639
640 let mut file = options.open(&password_path).map_err(|e| {
641 BindingCoreError::generic(format!(
642 "Failed to create password file {} securely: {}",
643 password_path.display(),
644 e
645 ))
646 })?;
647 file.write_all(password.as_bytes()).map_err(|e| {
648 BindingCoreError::generic(format!(
649 "Failed to write password file {}: {}",
650 password_path.display(),
651 e
652 ))
653 })?;
654 file.sync_all().map_err(|e| {
655 BindingCoreError::generic(format!(
656 "Failed to flush password file {}: {}",
657 password_path.display(),
658 e
659 ))
660 })?;
661
662 #[cfg(unix)]
663 {
664 let permissions = std::fs::Permissions::from_mode(0o600);
665 fs::set_permissions(&password_path, permissions).map_err(|e| {
666 BindingCoreError::generic(format!(
667 "Failed to set password file permissions on {}: {}",
668 password_path.display(),
669 e
670 ))
671 })?;
672 }
673
674 Ok(())
675}
676
677fn is_editable_level(level: &str) -> bool {
678 matches!(level, "artifact" | "config")
679}
680
681fn normalize_agent_id_for_compare(agent_id: &str) -> &str {
682 agent_id.split(':').next().unwrap_or(agent_id)
683}
684
685fn extract_agreement_payload(value: &Value) -> Value {
686 if let Some(payload) = value.get("jacsDocument") {
687 return payload.clone();
688 }
689 if let Some(payload) = value.get("content") {
690 return payload.clone();
691 }
692 if let Some(obj) = value.as_object() {
693 let mut filtered = serde_json::Map::new();
694 for (k, v) in obj {
695 if !k.starts_with("jacs") && k != "$schema" {
696 filtered.insert(k.clone(), v.clone());
697 }
698 }
699 if !filtered.is_empty() {
700 return Value::Object(filtered);
701 }
702 }
703 Value::Null
704}
705
706fn create_editable_agreement_document(
707 agent: &mut Agent,
708 payload: Value,
709) -> BindingResult<JACSDocument> {
710 let wrapped = json!({
711 "jacsType": "artifact",
712 "jacsLevel": "artifact",
713 "content": payload
714 });
715 agent
716 .create_document_and_load(&wrapped.to_string(), None, None)
717 .map_err(|e| {
718 BindingCoreError::document_failed(format!(
719 "Failed to create editable agreement document: {}",
720 e
721 ))
722 })
723}
724
725fn ensure_editable_agreement_document(
726 agent: &mut Agent,
727 document_string: &str,
728) -> BindingResult<JACSDocument> {
729 match agent.load_document(document_string) {
730 Ok(doc) => {
731 let level = doc
732 .value
733 .get("jacsLevel")
734 .and_then(|v| v.as_str())
735 .unwrap_or("");
736 if is_editable_level(level) {
737 Ok(doc)
738 } else {
739 let payload = extract_agreement_payload(doc.getvalue());
740 create_editable_agreement_document(agent, payload)
741 }
742 }
743 Err(load_err) => {
744 if let Ok(parsed) = serde_json::from_str::<Value>(document_string)
745 && (parsed.get("jacsId").is_some() || parsed.get("jacsVersion").is_some())
746 {
747 return Err(BindingCoreError::document_failed(format!(
748 "Failed to load document: {}",
749 load_err
750 )));
751 }
752 let payload = serde_json::from_str::<Value>(document_string)
753 .unwrap_or_else(|_| Value::String(document_string.to_string()));
754 create_editable_agreement_document(agent, payload)
755 }
756 }
757}
758
759#[derive(Clone)]
768pub struct AgentWrapper {
769 inner: Arc<Mutex<Agent>>,
770 private_key_password: Arc<Mutex<Option<String>>>,
771}
772
773struct AgentWrapperInlineResolver<'a> {
774 wrapper: &'a AgentWrapper,
775}
776
777impl<'a> jacs::inline::KeyResolver for AgentWrapperInlineResolver<'a> {
778 fn resolve(&self, signer_id: &str) -> Option<jacs::inline::ResolvedKey> {
779 let agent = self.wrapper.inner.lock().ok()?;
780 let my_id = agent
781 .get_value()
782 .and_then(|value| value.get("jacsId"))
783 .and_then(|value| value.as_str())?;
784 if my_id != signer_id {
785 return None;
786 }
787
788 let public_key = agent.get_public_key().ok()?;
789 let algorithm = jacs::crypt::detect_algorithm_from_public_key(&public_key)
790 .ok()
791 .map(binding_inline_algorithm_tag)
792 .unwrap_or_else(|| "ed25519".to_string());
793 Some(jacs::inline::ResolvedKey {
794 public_key_pem: public_key,
795 algorithm,
796 })
797 }
798}
799
800fn binding_inline_algorithm_tag<T: std::fmt::Display>(algo: T) -> String {
801 let s = algo.to_string();
802 match s.as_str() {
803 "ring-Ed25519" | "ed25519" | "Ed25519" => "ed25519".to_string(),
804 "pq2025" | "ML-DSA-87" | "ml-dsa-87" => "pq2025".to_string(),
805 _ => s.to_lowercase(),
806 }
807}
808
809fn binding_should_auto_dispatch_inline_text(content: &str) -> bool {
810 let trimmed = content.trim_start();
811 content.contains(jacs::inline::BEGIN_MARKER)
812 && !trimmed.starts_with('{')
813 && !trimmed.starts_with('[')
814}
815
816impl Default for AgentWrapper {
821 fn default() -> Self {
822 Self::new()
823 }
824}
825
826impl AgentWrapper {
827 pub fn new() -> Self {
829 Self {
830 inner: Arc::new(Mutex::new(jacs::get_empty_agent())),
831 private_key_password: Arc::new(Mutex::new(None)),
832 }
833 }
834
835 pub fn from_inner(inner: Arc<Mutex<Agent>>) -> Self {
840 Self {
841 inner,
842 private_key_password: Arc::new(Mutex::new(None)),
843 }
844 }
845
846 pub fn inner_arc(&self) -> Arc<Mutex<Agent>> {
851 Arc::clone(&self.inner)
852 }
853
854 fn lock(&self) -> BindingResult<MutexGuard<'_, Agent>> {
856 self.inner.lock().map_err(BindingCoreError::from)
857 }
858
859 fn verify_inline_document(&self, framed: &str) -> BindingResult<bool> {
860 let resolver = AgentWrapperInlineResolver { wrapper: self };
861 let result = jacs::inline::verify_inline(
862 framed,
863 &resolver,
864 jacs::inline::VerifyOptions {
865 strict: false,
866 key_dir: None,
867 },
868 )
869 .map_err(|e| {
870 BindingCoreError::document_failed(format!("Inline text verify failed: {e}"))
871 })?;
872
873 match result {
874 jacs::inline::VerifyTextResult::Signed { signatures } => {
875 if signatures
876 .iter()
877 .all(|entry| entry.status == jacs::inline::SignatureStatus::Valid)
878 {
879 Ok(true)
880 } else {
881 let errors: Vec<String> = signatures
882 .iter()
883 .filter_map(|entry| {
884 if entry.status == jacs::inline::SignatureStatus::Valid {
885 None
886 } else {
887 Some(format!("{}: {:?}", entry.signer_id, entry.status))
888 }
889 })
890 .collect();
891 Err(BindingCoreError::verification_failed(format!(
892 "Inline text verification failed: {}",
893 errors.join("; ")
894 )))
895 }
896 }
897 jacs::inline::VerifyTextResult::MissingSignature => Err(
898 BindingCoreError::document_failed("No inline JACS signature found".to_string()),
899 ),
900 jacs::inline::VerifyTextResult::Malformed(detail) => Err(
901 BindingCoreError::document_failed(format!("Malformed inline signature: {detail}")),
902 ),
903 }
904 }
905
906 fn configured_private_key_password(&self) -> BindingResult<Option<String>> {
907 self.private_key_password
908 .lock()
909 .map_err(BindingCoreError::from)
910 .map(|password| password.clone())
911 }
912
913 fn with_private_key_password<T>(
914 &self,
915 operation: impl FnOnce() -> BindingResult<T>,
916 ) -> BindingResult<T> {
917 {
922 let password = self.configured_private_key_password()?;
923 let mut agent = self.lock()?;
924 agent.set_password(password);
925 }
926 operation()
927 }
928
929 pub fn set_private_key_password(&self, password: Option<String>) -> BindingResult<()> {
935 let mut slot = self
936 .private_key_password
937 .lock()
938 .map_err(BindingCoreError::from)?;
939 *slot = password.and_then(|value| if value.is_empty() { None } else { Some(value) });
940 Ok(())
941 }
942
943 pub fn load(&self, config_path: String) -> BindingResult<String> {
948 let password = self.configured_private_key_password()?;
949 let new_agent = self.load_agent_from_config(&config_path, true, password.as_deref())?;
950 *self.lock()? = new_agent;
951 Ok("Agent loaded".to_string())
952 }
953
954 pub fn load_file_only(&self, config_path: String) -> BindingResult<String> {
959 let new_agent = self.load_agent_from_config(&config_path, false, None)?;
960 *self.lock()? = new_agent;
961 Ok("Agent loaded (file-only)".to_string())
962 }
963
964 pub fn load_with_info(&self, config_path: String) -> BindingResult<String> {
966 let resolved_config_path = resolve_existing_config_path(&config_path)?;
967 let password = self.configured_private_key_password()?;
968 let new_agent =
969 self.load_agent_from_config(&resolved_config_path, true, password.as_deref())?;
970 let info = jacs::simple::build_loaded_agent_info(&new_agent, &resolved_config_path)
971 .map_err(|e| BindingCoreError::agent_load(format!("Failed to load agent: {}", e)))?;
972 *self.lock()? = new_agent;
973 serialize_agent_info(&info)
974 }
975
976 fn load_agent_from_config(
981 &self,
982 config_path: &str,
983 apply_env: bool,
984 password: Option<&str>,
985 ) -> BindingResult<Agent> {
986 let mut config = Config::from_file(config_path)
987 .map_err(|e| BindingCoreError::agent_load(format!("Failed to load config: {}", e)))?;
988 if apply_env {
989 config.apply_env_overrides();
990 }
991 Agent::from_config(config, password)
992 .map_err(|e| BindingCoreError::agent_load(format!("Failed to load agent: {}", e)))
993 }
994
995 pub fn set_storage_root(&self, root: std::path::PathBuf) -> BindingResult<()> {
1002 let mut agent = self.lock()?;
1003 agent
1004 .set_storage_root(root)
1005 .map_err(|e| BindingCoreError::generic(format!("Failed to set storage root: {}", e)))?;
1006 Ok(())
1007 }
1008
1009 pub fn sign_agent(
1011 &self,
1012 agent_string: &str,
1013 public_key: Vec<u8>,
1014 public_key_enc_type: String,
1015 ) -> BindingResult<String> {
1016 self.with_private_key_password(|| {
1017 let mut agent = self.lock()?;
1018
1019 let mut external_agent: Value = agent.validate_agent(agent_string).map_err(|e| {
1020 BindingCoreError::validation(format!("Agent validation failed: {}", e))
1021 })?;
1022
1023 agent
1024 .signature_verification_procedure(
1025 &external_agent,
1026 None,
1027 AGENT_SIGNATURE_FIELDNAME,
1028 public_key,
1029 Some(public_key_enc_type),
1030 None,
1031 None,
1032 )
1033 .map_err(|e| {
1034 BindingCoreError::verification_failed(format!(
1035 "Signature verification failed: {}",
1036 e
1037 ))
1038 })?;
1039
1040 let registration_signature = agent
1041 .signing_procedure(
1042 &external_agent,
1043 None,
1044 AGENT_REGISTRATION_SIGNATURE_FIELDNAME,
1045 )
1046 .map_err(|e| {
1047 BindingCoreError::signing_failed(format!("Signing procedure failed: {}", e))
1048 })?;
1049
1050 external_agent[AGENT_REGISTRATION_SIGNATURE_FIELDNAME] = registration_signature;
1051 Ok(external_agent.to_string())
1052 })
1053 }
1054
1055 pub fn verify_string(
1057 &self,
1058 data: &str,
1059 signature_base64: &str,
1060 public_key: Vec<u8>,
1061 public_key_enc_type: String,
1062 ) -> BindingResult<bool> {
1063 let agent = self.lock()?;
1064
1065 if data.is_empty()
1066 || signature_base64.is_empty()
1067 || public_key.is_empty()
1068 || public_key_enc_type.is_empty()
1069 {
1070 return Err(BindingCoreError::invalid_argument(format!(
1071 "One parameter is empty: data={}, signature_base64={}, public_key_enc_type={}",
1072 data.is_empty(),
1073 signature_base64.is_empty(),
1074 public_key_enc_type
1075 )));
1076 }
1077
1078 agent
1079 .verify_string(
1080 data,
1081 signature_base64,
1082 public_key,
1083 Some(public_key_enc_type),
1084 )
1085 .map_err(|e| {
1086 BindingCoreError::verification_failed(format!(
1087 "Signature verification failed: {}",
1088 e
1089 ))
1090 })?;
1091
1092 Ok(true)
1093 }
1094
1095 pub fn sign_string(&self, data: &str) -> BindingResult<String> {
1097 self.with_private_key_password(|| {
1098 let mut agent = self.lock()?;
1099 agent.sign_string(data).map_err(|e| {
1100 BindingCoreError::signing_failed(format!("Failed to sign string: {}", e))
1101 })
1102 })
1103 }
1104
1105 pub fn sign_batch(&self, messages: Vec<String>) -> BindingResult<Vec<String>> {
1107 self.with_private_key_password(|| {
1108 let mut agent = self.lock()?;
1109 let refs: Vec<&str> = messages.iter().map(|s| s.as_str()).collect();
1110 agent
1111 .sign_batch(&refs)
1112 .map_err(|e| BindingCoreError::signing_failed(format!("Batch sign failed: {}", e)))
1113 })
1114 }
1115
1116 pub fn verify_agent(&self, agentfile: Option<String>) -> BindingResult<bool> {
1118 self.with_private_key_password(|| {
1119 let mut agent = self.lock()?;
1120
1121 if let Some(file) = agentfile {
1122 let loaded_agent = jacs::load_agent(Some(file)).map_err(|e| {
1123 BindingCoreError::agent_load(format!("Failed to load agent: {}", e))
1124 })?;
1125 *agent = loaded_agent;
1126 }
1127
1128 agent.verify_self_signature().map_err(|e| {
1129 BindingCoreError::verification_failed(format!(
1130 "Failed to verify agent signature: {}",
1131 e
1132 ))
1133 })?;
1134
1135 agent.verify_self_hash().map_err(|e| {
1136 BindingCoreError::verification_failed(format!("Failed to verify agent hash: {}", e))
1137 })?;
1138
1139 Ok(true)
1140 })
1141 }
1142
1143 pub fn update_agent(&self, new_agent_string: &str) -> BindingResult<String> {
1145 self.with_private_key_password(|| {
1146 let mut agent = self.lock()?;
1147 agent
1148 .update_self(new_agent_string)
1149 .map_err(|e| BindingCoreError::agent_load(format!("Failed to update agent: {}", e)))
1150 })
1151 }
1152
1153 pub fn verify_document(&self, document_string: &str) -> BindingResult<bool> {
1155 if binding_should_auto_dispatch_inline_text(document_string) {
1156 return self.verify_inline_document(document_string);
1157 }
1158
1159 let mut agent = self.lock()?;
1160
1161 let doc = agent.load_document(document_string).map_err(|e| {
1162 BindingCoreError::document_failed(format!("Failed to load document: {}", e))
1163 })?;
1164
1165 let document_key = doc.getkey();
1166 let value = doc.getvalue();
1167
1168 agent.verify_hash(value).map_err(|e| {
1169 BindingCoreError::verification_failed(format!("Failed to verify document hash: {}", e))
1170 })?;
1171
1172 if agent
1176 .verify_document_signature(&document_key, None, None, None, None)
1177 .is_err()
1178 {
1179 agent
1180 .verify_external_document_signature(&document_key)
1181 .map_err(|e| {
1182 BindingCoreError::verification_failed(format!(
1183 "Failed to verify document signature: {}",
1184 e
1185 ))
1186 })?;
1187 }
1188
1189 Ok(true)
1190 }
1191
1192 pub fn update_document(
1194 &self,
1195 document_key: &str,
1196 new_document_string: &str,
1197 attachments: Option<Vec<String>>,
1198 embed: Option<bool>,
1199 ) -> BindingResult<String> {
1200 self.with_private_key_password(|| {
1201 let mut agent = self.lock()?;
1202
1203 let doc = agent
1204 .update_document(document_key, new_document_string, attachments, embed)
1205 .map_err(|e| {
1206 BindingCoreError::document_failed(format!("Failed to update document: {}", e))
1207 })?;
1208
1209 Ok(doc.to_string())
1210 })
1211 }
1212
1213 pub fn verify_signature(
1215 &self,
1216 document_string: &str,
1217 signature_field: Option<String>,
1218 ) -> BindingResult<bool> {
1219 if signature_field.is_none() && binding_should_auto_dispatch_inline_text(document_string) {
1220 return self.verify_inline_document(document_string);
1221 }
1222
1223 let mut agent = self.lock()?;
1224
1225 let doc = agent.load_document(document_string).map_err(|e| {
1226 BindingCoreError::document_failed(format!("Failed to load document: {}", e))
1227 })?;
1228
1229 let document_key = doc.getkey();
1230 let sig_field_ref = signature_field.as_ref();
1231
1232 agent
1233 .verify_document_signature(
1234 &document_key,
1235 sig_field_ref.map(|s| s.as_str()),
1236 None,
1237 None,
1238 None,
1239 )
1240 .map_err(|e| {
1241 BindingCoreError::verification_failed(format!("Failed to verify signature: {}", e))
1242 })?;
1243
1244 Ok(true)
1245 }
1246
1247 pub fn create_agreement(
1249 &self,
1250 document_string: &str,
1251 agentids: Vec<String>,
1252 question: Option<String>,
1253 context: Option<String>,
1254 agreement_fieldname: Option<String>,
1255 ) -> BindingResult<String> {
1256 self.create_agreement_with_options(
1257 document_string,
1258 agentids,
1259 question,
1260 context,
1261 agreement_fieldname,
1262 None,
1263 None,
1264 None,
1265 None,
1266 )
1267 }
1268
1269 #[allow(clippy::too_many_arguments)]
1277 pub fn create_agreement_with_options(
1278 &self,
1279 document_string: &str,
1280 agentids: Vec<String>,
1281 question: Option<String>,
1282 context: Option<String>,
1283 agreement_fieldname: Option<String>,
1284 timeout: Option<String>,
1285 quorum: Option<u32>,
1286 required_algorithms: Option<Vec<String>>,
1287 minimum_strength: Option<String>,
1288 ) -> BindingResult<String> {
1289 use jacs::agent::agreement::{Agreement, AgreementOptions};
1290
1291 self.with_private_key_password(|| {
1292 let mut agent = self.lock()?;
1293 let base_doc = ensure_editable_agreement_document(&mut agent, document_string)?;
1294 let document_key = base_doc.getkey();
1295
1296 let options = AgreementOptions {
1297 timeout,
1298 quorum,
1299 required_algorithms,
1300 minimum_strength,
1301 };
1302
1303 let agreement_doc = agent
1304 .create_agreement_with_options(
1305 &document_key,
1306 agentids.as_slice(),
1307 question.as_deref(),
1308 context.as_deref(),
1309 agreement_fieldname,
1310 &options,
1311 )
1312 .map_err(|e| {
1313 BindingCoreError::agreement_failed(format!("Failed to create agreement: {}", e))
1314 })?;
1315
1316 Ok(agreement_doc.value.to_string())
1317 })
1318 }
1319
1320 pub fn sign_agreement(
1322 &self,
1323 document_string: &str,
1324 agreement_fieldname: Option<String>,
1325 ) -> BindingResult<String> {
1326 self.with_private_key_password(|| {
1327 let mut agent = self.lock()?;
1328 let doc = agent.load_document(document_string).map_err(|e| {
1329 BindingCoreError::document_failed(format!("Failed to load document: {}", e))
1330 })?;
1331 let document_key = doc.getkey();
1332 let signed_doc = agent
1333 .sign_agreement(&document_key, agreement_fieldname)
1334 .map_err(|e| {
1335 BindingCoreError::agreement_failed(format!("Failed to sign agreement: {}", e))
1336 })?;
1337
1338 Ok(signed_doc.value.to_string())
1339 })
1340 }
1341
1342 #[cfg(feature = "agreements")]
1344 pub fn create_agreement_v2_json(&self, input_json: &str) -> BindingResult<String> {
1345 self.with_private_key_password(|| {
1346 let mut agent = self.lock()?;
1347 agreement_v2::create_agreement_v2_json(&mut agent, input_json)
1348 })
1349 }
1350
1351 #[cfg(feature = "agreements")]
1353 pub fn apply_agreement_v2_json(
1354 &self,
1355 document_json: &str,
1356 mutation_json: &str,
1357 ) -> BindingResult<String> {
1358 self.with_private_key_password(|| {
1359 let mut agent = self.lock()?;
1360 agreement_v2::apply_agreement_v2_json(&mut agent, document_json, mutation_json)
1361 })
1362 }
1363
1364 #[cfg(feature = "agreements")]
1366 pub fn sign_agreement_v2_json(&self, document_json: &str, role: &str) -> BindingResult<String> {
1367 self.with_private_key_password(|| {
1368 let mut agent = self.lock()?;
1369 agreement_v2::sign_agreement_v2_json(&mut agent, document_json, role)
1370 })
1371 }
1372
1373 #[cfg(feature = "agreements")]
1375 pub fn verify_agreement_v2_json(&self, document_json: &str) -> BindingResult<String> {
1376 self.with_private_key_password(|| {
1377 let mut agent = self.lock()?;
1378 agreement_v2::verify_agreement_v2_json(&mut agent, document_json)
1379 })
1380 }
1381
1382 #[cfg(feature = "agreements")]
1384 pub fn detect_agreement_v2_branch_conflict_json(
1385 &self,
1386 base_document_json: &str,
1387 left_document_json: &str,
1388 right_document_json: &str,
1389 ) -> BindingResult<String> {
1390 agreement_v2::detect_agreement_v2_branch_conflict_json(
1391 base_document_json,
1392 left_document_json,
1393 right_document_json,
1394 )
1395 }
1396
1397 #[cfg(feature = "agreements")]
1399 pub fn merge_agreement_v2_transcript_branches_json(
1400 &self,
1401 base_document_json: &str,
1402 left_document_json: &str,
1403 right_document_json: &str,
1404 ) -> BindingResult<String> {
1405 self.with_private_key_password(|| {
1406 let mut agent = self.lock()?;
1407 agreement_v2::merge_agreement_v2_transcript_branches_json(
1408 &mut agent,
1409 base_document_json,
1410 left_document_json,
1411 right_document_json,
1412 )
1413 })
1414 }
1415
1416 #[cfg(feature = "agreements")]
1418 pub fn resolve_agreement_v2_branch_conflict_json(
1419 &self,
1420 base_document_json: &str,
1421 previous_document_json: &str,
1422 side_branch_document_json: &str,
1423 mutation_json: &str,
1424 ) -> BindingResult<String> {
1425 self.with_private_key_password(|| {
1426 let mut agent = self.lock()?;
1427 agreement_v2::resolve_agreement_v2_branch_conflict_json(
1428 &mut agent,
1429 base_document_json,
1430 previous_document_json,
1431 side_branch_document_json,
1432 mutation_json,
1433 )
1434 })
1435 }
1436
1437 pub fn create_document(
1439 &self,
1440 document_string: &str,
1441 custom_schema: Option<String>,
1442 outputfilename: Option<String>,
1443 no_save: bool,
1444 attachments: Option<&str>,
1445 embed: Option<bool>,
1446 ) -> BindingResult<String> {
1447 self.with_private_key_password(|| {
1448 let mut agent = self.lock()?;
1449
1450 jacs::shared::document_create(
1451 &mut agent,
1452 document_string,
1453 custom_schema,
1454 outputfilename,
1455 no_save,
1456 attachments,
1457 embed,
1458 )
1459 .map_err(|e| {
1460 BindingCoreError::document_failed(format!("Failed to create document: {}", e))
1461 })
1462 })
1463 }
1464
1465 pub fn save_signed_document(
1471 &self,
1472 document_string: &str,
1473 outputfilename: Option<String>,
1474 export_embedded: Option<bool>,
1475 extract_only: Option<bool>,
1476 ) -> BindingResult<String> {
1477 let mut agent = self.lock()?;
1478 let doc = agent.load_document(document_string).map_err(|e| {
1479 BindingCoreError::document_failed(format!("Failed to load signed document: {}", e))
1480 })?;
1481 let document_key = doc.getkey();
1482 agent
1483 .save_document(&document_key, outputfilename, export_embedded, extract_only)
1484 .map_err(|e| {
1485 BindingCoreError::document_failed(format!(
1486 "Failed to persist signed document '{}': {}",
1487 document_key, e
1488 ))
1489 })?;
1490
1491 Ok(document_key)
1492 }
1493
1494 pub fn list_document_keys(&self) -> BindingResult<Vec<String>> {
1496 let mut agent = self.lock()?;
1497 Ok(agent.get_document_keys())
1498 }
1499
1500 pub fn check_agreement(
1502 &self,
1503 document_string: &str,
1504 agreement_fieldname: Option<String>,
1505 ) -> BindingResult<String> {
1506 let mut agent = self.lock()?;
1507 let doc = agent.load_document(document_string).map_err(|e| {
1508 BindingCoreError::document_failed(format!("Failed to load document: {}", e))
1509 })?;
1510 let document_key = doc.getkey();
1511 let agreement_fieldname_key = agreement_fieldname
1512 .clone()
1513 .unwrap_or_else(|| AGENT_AGREEMENT_FIELDNAME.to_string());
1514
1515 agent
1516 .check_agreement(&document_key, Some(agreement_fieldname_key.clone()))
1517 .map_err(|e| {
1518 BindingCoreError::agreement_failed(format!("Failed to check agreement: {}", e))
1519 })?;
1520
1521 let requested = doc
1522 .agreement_requested_agents(Some(agreement_fieldname_key.clone()))
1523 .map_err(|e| {
1524 BindingCoreError::agreement_failed(format!(
1525 "Failed to read requested signers: {}",
1526 e
1527 ))
1528 })?;
1529
1530 let pending = doc
1531 .agreement_unsigned_agents(Some(agreement_fieldname_key.clone()))
1532 .map_err(|e| {
1533 BindingCoreError::agreement_failed(format!("Failed to read pending signers: {}", e))
1534 })?;
1535
1536 let signatures = doc
1537 .value
1538 .get(&agreement_fieldname_key)
1539 .and_then(|agreement| agreement.get("signatures"))
1540 .and_then(|v| v.as_array())
1541 .cloned()
1542 .unwrap_or_default();
1543
1544 let mut signed_at_by_agent: HashMap<String, String> = HashMap::new();
1545 for signature in signatures {
1546 if let Some(agent_id) = signature.get("agentID").and_then(|v| v.as_str()) {
1547 let normalized = normalize_agent_id_for_compare(agent_id).to_string();
1548 let signed_at = signature
1549 .get("date")
1550 .and_then(|v| v.as_str())
1551 .unwrap_or("")
1552 .to_string();
1553 signed_at_by_agent.insert(normalized, signed_at);
1554 }
1555 }
1556
1557 let signers = requested
1558 .iter()
1559 .map(|agent_id| {
1560 let normalized = normalize_agent_id_for_compare(agent_id).to_string();
1561 let signed_at = signed_at_by_agent
1562 .get(&normalized)
1563 .filter(|ts| !ts.is_empty())
1564 .cloned();
1565 let signed = signed_at.is_some();
1566 let mut signer = json!({
1567 "agentId": agent_id,
1568 "agent_id": agent_id,
1569 "signed": signed
1570 });
1571 if let Some(ts) = signed_at {
1572 signer["signedAt"] = json!(ts.clone());
1573 signer["signed_at"] = json!(ts);
1574 }
1575 signer
1576 })
1577 .collect::<Vec<Value>>();
1578
1579 let result = json!({
1580 "complete": pending.is_empty(),
1581 "signers": signers,
1582 "pending": pending
1583 });
1584
1585 Ok(result.to_string())
1586 }
1587
1588 pub fn sign_request(&self, payload_value: Value) -> BindingResult<String> {
1590 self.with_private_key_password(|| {
1591 let mut agent = self.lock()?;
1592
1593 let wrapper_value = serde_json::json!({
1594 "jacs_payload": payload_value
1595 });
1596
1597 let wrapper_string = serde_json::to_string(&wrapper_value).map_err(|e| {
1598 BindingCoreError::serialization_failed(format!(
1599 "Failed to serialize wrapper JSON: {}",
1600 e
1601 ))
1602 })?;
1603
1604 jacs::shared::document_create(
1605 &mut agent,
1606 &wrapper_string,
1607 None,
1608 None,
1609 true, None,
1611 Some(false),
1612 )
1613 .map_err(|e| {
1614 BindingCoreError::document_failed(format!("Failed to create document: {}", e))
1615 })
1616 })
1617 }
1618
1619 pub fn verify_response(&self, document_string: String) -> BindingResult<Value> {
1621 let mut agent = self.lock()?;
1622
1623 agent
1624 .verify_payload(document_string, None)
1625 .map_err(|e| BindingCoreError::verification_failed(e.to_string()))
1626 }
1627
1628 pub fn verify_response_with_agent_id(
1630 &self,
1631 document_string: String,
1632 ) -> BindingResult<(Value, String)> {
1633 let mut agent = self.lock()?;
1634
1635 agent
1636 .verify_payload_with_agent_id(document_string, None)
1637 .map_err(|e| BindingCoreError::verification_failed(e.to_string()))
1638 }
1639
1640 pub fn verify_document_by_id(&self, document_id: &str) -> BindingResult<bool> {
1645 use jacs::storage::StorageDocumentTraits;
1646
1647 if !document_id.contains(':') {
1649 return Err(BindingCoreError::invalid_argument(format!(
1650 "Document ID must be in 'uuid:version' format, got '{}'. \
1651 Use verify_document() with the full JSON string instead.",
1652 document_id
1653 )));
1654 }
1655
1656 let storage = jacs::storage::MultiStorage::default_new().map_err(|e| {
1657 BindingCoreError::generic(format!("Failed to initialize storage: {}", e))
1658 })?;
1659
1660 let doc = storage.get_document(document_id).map_err(|e| {
1661 BindingCoreError::document_failed(format!(
1662 "Failed to load document '{}' from storage: {}",
1663 document_id, e
1664 ))
1665 })?;
1666
1667 let doc_str = serde_json::to_string(&doc.value).map_err(|e| {
1668 BindingCoreError::serialization_failed(format!(
1669 "Failed to serialize document '{}': {}",
1670 document_id, e
1671 ))
1672 })?;
1673
1674 self.verify_document(&doc_str)
1675 }
1676
1677 pub fn get_document_by_id(&self, document_id: &str) -> BindingResult<String> {
1681 if !document_id.contains(':') {
1682 return Err(BindingCoreError::invalid_argument(format!(
1683 "Document ID must be in 'uuid:version' format, got '{}'.",
1684 document_id
1685 )));
1686 }
1687
1688 let agent = self.lock()?;
1689 let doc = agent.get_document(document_id).map_err(|e| {
1690 BindingCoreError::document_failed(format!(
1691 "Failed to load document '{}' from storage: {}",
1692 document_id, e
1693 ))
1694 })?;
1695
1696 serde_json::to_string(&doc.value).map_err(|e| {
1697 BindingCoreError::serialization_failed(format!(
1698 "Failed to serialize document '{}': {}",
1699 document_id, e
1700 ))
1701 })
1702 }
1703
1704 pub fn get_agent_id(&self) -> BindingResult<String> {
1706 let agent = self.lock()?;
1707 let value = agent
1708 .get_value()
1709 .ok_or_else(|| BindingCoreError::agent_load("Agent not loaded. Call load() first."))?;
1710 value
1711 .get("jacsId")
1712 .and_then(|v| v.as_str())
1713 .map(str::to_string)
1714 .filter(|id| !id.is_empty())
1715 .ok_or_else(|| {
1716 BindingCoreError::agent_load(
1717 "Agent not loaded or has no jacsId. Call load() first.",
1718 )
1719 })
1720 }
1721
1722 pub fn reencrypt_key(&self, old_password: &str, new_password: &str) -> BindingResult<()> {
1727 let agent = self.lock()?;
1729 let (key_dir, key_file) = if let Some(config) = &agent.config {
1730 (
1731 config
1732 .jacs_key_directory()
1733 .as_deref()
1734 .unwrap_or("./jacs_keys")
1735 .to_string(),
1736 config
1737 .jacs_agent_private_key_filename()
1738 .as_deref()
1739 .unwrap_or("jacs.private.pem.enc")
1740 .to_string(),
1741 )
1742 } else {
1743 (
1744 "./jacs_keys".to_string(),
1745 "jacs.private.pem.enc".to_string(),
1746 )
1747 };
1748 drop(agent);
1749
1750 jacs::validation::require_relative_path_safe(&key_file).map_err(|e| {
1758 BindingCoreError::generic(format!(
1759 "Invalid private key filename '{}': {}",
1760 key_file, e
1761 ))
1762 })?;
1763 let key_path = format!("{}/{}", key_dir, key_file);
1764
1765 jacs::crypt::aes_encrypt::reencrypt_private_key_file(&key_path, old_password, new_password)
1766 .map_err(|e| BindingCoreError::generic(format!("Re-encryption failed: {}", e)))?;
1767
1768 Ok(())
1769 }
1770
1771 pub fn rotate_keys(&self, algorithm: Option<&str>) -> BindingResult<String> {
1776 let config_path = {
1778 let agent = self.lock()?;
1779 agent
1780 .config
1781 .as_ref()
1782 .and_then(|c| c.config_dir())
1783 .map(|dir| dir.join("jacs.config.json").display().to_string())
1784 };
1785
1786 let result = jacs::simple::advanced::rotate_with_mutex(
1787 &self.inner,
1788 config_path.as_deref(),
1789 algorithm,
1790 )
1791 .map_err(|e| BindingCoreError::generic(format!("Key rotation failed: {}", e)))?;
1792
1793 serde_json::to_string(&result).map_err(|e| {
1794 BindingCoreError::serialization_failed(format!(
1795 "Failed to serialize rotation result: {}",
1796 e
1797 ))
1798 })
1799 }
1800
1801 pub fn ephemeral(&self, algorithm: Option<&str>) -> BindingResult<String> {
1807 let algo = match algorithm.unwrap_or("pq2025") {
1809 "ed25519" => "ring-Ed25519",
1810 "pq2025" => "pq2025",
1811 other => other,
1812 };
1813
1814 let mut agent = Agent::ephemeral(algo).map_err(|e| {
1815 BindingCoreError::agent_load(format!("Failed to create ephemeral agent: {}", e))
1816 })?;
1817
1818 let template = jacs::create_minimal_blank_agent("ai".to_string(), None, None, None)
1819 .map_err(|e| {
1820 BindingCoreError::agent_load(format!(
1821 "Failed to create minimal agent template: {}",
1822 e
1823 ))
1824 })?;
1825 let mut agent_json: Value = serde_json::from_str(&template).map_err(|e| {
1826 BindingCoreError::serialization_failed(format!(
1827 "Failed to parse agent template JSON: {}",
1828 e
1829 ))
1830 })?;
1831 if let Some(obj) = agent_json.as_object_mut() {
1832 obj.insert("name".to_string(), json!("ephemeral"));
1833 obj.insert("description".to_string(), json!("Ephemeral JACS agent"));
1834 }
1835
1836 let instance = agent
1837 .create_agent_and_load(&agent_json.to_string(), true, Some(algo))
1838 .map_err(|e| {
1839 BindingCoreError::agent_load(format!("Failed to initialize ephemeral agent: {}", e))
1840 })?;
1841
1842 let agent_id = instance["jacsId"].as_str().unwrap_or("").to_string();
1843 let version = instance["jacsVersion"].as_str().unwrap_or("").to_string();
1844
1845 let mut inner = self.lock()?;
1847 *inner = agent;
1848
1849 let info = json!({
1850 "agent_id": agent_id,
1851 "name": "ephemeral",
1852 "version": version,
1853 "algorithm": algo,
1854 });
1855
1856 serde_json::to_string_pretty(&info).map_err(|e| {
1857 BindingCoreError::serialization_failed(format!(
1858 "Failed to serialize ephemeral agent info: {}",
1859 e
1860 ))
1861 })
1862 }
1863
1864 pub fn diagnostics(&self) -> String {
1866 let mut info = jacs::simple::diagnostics();
1867
1868 if let Ok(agent) = self.inner.lock() {
1869 if agent.ready() {
1870 info["agent_loaded"] = json!(true);
1871 if let Some(value) = agent.get_value() {
1872 info["agent_id"] = json!(value.get("jacsId").and_then(|v| v.as_str()));
1873 info["agent_version"] =
1874 json!(value.get("jacsVersion").and_then(|v| v.as_str()));
1875 }
1876 }
1877 if let Some(config) = &agent.config {
1878 if let Some(dir) = config.jacs_data_directory().as_ref() {
1879 info["data_directory"] = json!(dir);
1880 }
1881 if let Some(dir) = config.jacs_key_directory().as_ref() {
1882 info["key_directory"] = json!(dir);
1883 }
1884 if let Some(storage) = config.jacs_default_storage().as_ref() {
1885 info["default_storage"] = json!(storage);
1886 }
1887 if let Some(algo) = config.jacs_agent_key_algorithm().as_ref() {
1888 info["key_algorithm"] = json!(algo);
1889 }
1890 }
1891 }
1892
1893 serde_json::to_string_pretty(&info).unwrap_or_default()
1894 }
1895
1896 pub fn get_setup_instructions(&self, domain: &str, ttl: u32) -> BindingResult<String> {
1900 use jacs::agent::boilerplate::BoilerPlate;
1901 use jacs::dns::bootstrap::{
1902 DigestEncoding, build_dns_record, dnssec_guidance, emit_azure_cli,
1903 emit_cloudflare_curl, emit_gcloud_dns, emit_plain_bind, emit_route53_change_batch,
1904 tld_requirement_text,
1905 };
1906
1907 let agent = self.lock()?;
1908 let agent_value = agent.get_value().cloned().unwrap_or(json!({}));
1909 let agent_id = agent_value
1910 .get("jacsId")
1911 .and_then(|v| v.as_str())
1912 .unwrap_or("");
1913 if agent_id.is_empty() {
1914 return Err(BindingCoreError::agent_load(
1915 "Agent not loaded or has no jacsId. Call load() first.",
1916 ));
1917 }
1918
1919 let pk = agent
1920 .get_public_key()
1921 .map_err(|e| BindingCoreError::generic(format!("Failed to get public key: {}", e)))?;
1922 let digest = jacs::dns::bootstrap::pubkey_digest_b64(&pk);
1923 let rr = build_dns_record(domain, ttl, agent_id, &digest, DigestEncoding::Base64);
1924
1925 let dns_record_bind = emit_plain_bind(&rr);
1926 let dns_owner = rr.owner.clone();
1927 let dns_record_value = rr.txt.clone();
1928
1929 let mut provider_commands = std::collections::HashMap::new();
1930 provider_commands.insert("bind".to_string(), dns_record_bind.clone());
1931 provider_commands.insert("route53".to_string(), emit_route53_change_batch(&rr));
1932 provider_commands.insert("gcloud".to_string(), emit_gcloud_dns(&rr, "YOUR_ZONE_NAME"));
1933 provider_commands.insert(
1934 "azure".to_string(),
1935 emit_azure_cli(&rr, "YOUR_RG", domain, "_v1.agent.jacs"),
1936 );
1937 provider_commands.insert(
1938 "cloudflare".to_string(),
1939 emit_cloudflare_curl(&rr, "YOUR_ZONE_ID"),
1940 );
1941
1942 let mut dnssec_instructions = std::collections::HashMap::new();
1943 for name in &["aws", "cloudflare", "azure", "gcloud"] {
1944 dnssec_instructions.insert(name.to_string(), dnssec_guidance(name).to_string());
1945 }
1946
1947 let tld_requirement = tld_requirement_text().to_string();
1948
1949 let well_known = json!({
1950 "jacs_agent_id": agent_id,
1951 "jacs_public_key_hash": digest,
1952 "jacs_dns_record": dns_owner,
1953 });
1954 let well_known_json = serde_json::to_string_pretty(&well_known).unwrap_or_default();
1955
1956 let summary = format!(
1957 "Setup instructions for agent {agent_id} on domain {domain}:\n\
1958 \n\
1959 1. DNS: Publish the following TXT record:\n\
1960 {bind}\n\
1961 \n\
1962 2. DNSSEC: {dnssec}\n\
1963 \n\
1964 3. Domain requirement: {tld}\n\
1965 \n\
1966 4. .well-known: Serve the well-known JSON at /.well-known/jacs-agent.json",
1967 agent_id = agent_id,
1968 domain = domain,
1969 bind = dns_record_bind,
1970 dnssec = dnssec_guidance("aws"),
1971 tld = tld_requirement,
1972 );
1973
1974 let result = json!({
1975 "dns_record_bind": dns_record_bind,
1976 "dns_record_value": dns_record_value,
1977 "dns_owner": dns_owner,
1978 "provider_commands": provider_commands,
1979 "dnssec_instructions": dnssec_instructions,
1980 "tld_requirement": tld_requirement,
1981 "well_known_json": well_known_json,
1982 "summary": summary,
1983 });
1984
1985 serde_json::to_string_pretty(&result).map_err(|e| {
1986 BindingCoreError::serialization_failed(format!(
1987 "Failed to serialize setup instructions: {}",
1988 e
1989 ))
1990 })
1991 }
1992
1993 pub fn export_agent(&self) -> BindingResult<String> {
1995 let agent = self.lock()?;
1996 let value = agent
1997 .get_value()
1998 .cloned()
1999 .ok_or_else(|| BindingCoreError::agent_load("Agent not loaded. Call load() first."))?;
2000 serde_json::to_string_pretty(&value).map_err(|e| {
2001 BindingCoreError::serialization_failed(format!(
2002 "Failed to serialize agent document: {}",
2003 e
2004 ))
2005 })
2006 }
2007
2008 pub fn get_public_key_pem(&self) -> BindingResult<String> {
2010 let agent = self.lock()?;
2011 let public_key = BoilerPlate::get_public_key(&*agent)
2012 .map_err(|e| BindingCoreError::generic(format!("Failed to get public key: {}", e)))?;
2013 Ok(jacs::crypt::normalize_public_key_pem(&public_key))
2014 }
2015
2016 pub fn get_agent_json(&self) -> BindingResult<String> {
2020 self.export_agent()
2021 }
2022}
2023
2024#[cfg(feature = "a2a")]
2025impl AgentWrapper {
2026 pub fn export_agent_card(&self) -> BindingResult<String> {
2034 let agent = self.lock()?;
2035 let mut card = jacs::a2a::agent_card::export_agent_card(&agent).map_err(|e| {
2036 BindingCoreError::generic(format!("Failed to export agent card: {}", e))
2037 })?;
2038 if let Some(agent_value) = agent.get_value() {
2039 card.skills = explicit_a2a_skills(agent_value);
2040 }
2041 serde_json::to_string_pretty(&card).map_err(|e| {
2042 BindingCoreError::serialization_failed(format!("Failed to serialize agent card: {}", e))
2043 })
2044 }
2045
2046 pub fn generate_well_known_documents(
2050 &self,
2051 a2a_algorithm: Option<&str>,
2052 ) -> BindingResult<String> {
2053 let agent = self.lock()?;
2054 let mut card = jacs::a2a::agent_card::export_agent_card(&agent).map_err(|e| {
2055 BindingCoreError::generic(format!("Failed to export agent card: {}", e))
2056 })?;
2057 if let Some(agent_value) = agent.get_value() {
2058 card.skills = explicit_a2a_skills(agent_value);
2059 }
2060
2061 let a2a_alg = a2a_algorithm.unwrap_or("ring-Ed25519");
2062 let dual_keys = jacs::a2a::keys::create_jwk_keys(None, Some(a2a_alg)).map_err(|e| {
2063 BindingCoreError::generic(format!("Failed to generate A2A keys: {}", e))
2064 })?;
2065
2066 let agent_id = agent
2067 .get_id()
2068 .map_err(|e| BindingCoreError::generic(format!("Failed to get agent ID: {}", e)))?;
2069
2070 let jws = jacs::a2a::extension::sign_agent_card_jws(
2071 &card,
2072 &dual_keys.a2a_private_key,
2073 &dual_keys.a2a_algorithm,
2074 &agent_id,
2075 )
2076 .map_err(|e| BindingCoreError::generic(format!("Failed to sign Agent Card: {}", e)))?;
2077
2078 let documents = jacs::a2a::extension::generate_well_known_documents(
2079 &agent,
2080 &card,
2081 &dual_keys.a2a_public_key,
2082 &dual_keys.a2a_algorithm,
2083 &jws,
2084 )
2085 .map_err(|e| {
2086 BindingCoreError::generic(format!("Failed to generate well-known documents: {}", e))
2087 })?;
2088
2089 let pairs: Vec<Value> = documents
2091 .into_iter()
2092 .map(|(path, doc)| serde_json::json!({ "path": path, "document": doc }))
2093 .collect();
2094 serde_json::to_string_pretty(&pairs).map_err(|e| {
2095 BindingCoreError::serialization_failed(format!(
2096 "Failed to serialize well-known documents: {}",
2097 e
2098 ))
2099 })
2100 }
2101
2102 #[deprecated(since = "0.9.0", note = "Use sign_artifact() instead")]
2106 pub fn wrap_a2a_artifact(
2107 &self,
2108 artifact_json: &str,
2109 artifact_type: &str,
2110 parent_signatures_json: Option<&str>,
2111 ) -> BindingResult<String> {
2112 if std::env::var("JACS_SHOW_DEPRECATIONS").is_ok() {
2113 tracing::warn!("wrap_a2a_artifact is deprecated, use sign_artifact instead");
2114 }
2115
2116 let artifact: Value = serde_json::from_str(artifact_json).map_err(|e| {
2117 BindingCoreError::invalid_argument(format!("Invalid artifact JSON: {}", e))
2118 })?;
2119
2120 let parent_signatures: Option<Vec<Value>> = match parent_signatures_json {
2121 Some(json_str) => {
2122 let parsed: Vec<Value> = serde_json::from_str(json_str).map_err(|e| {
2123 BindingCoreError::invalid_argument(format!(
2124 "Invalid parent signatures JSON array: {}",
2125 e
2126 ))
2127 })?;
2128 Some(parsed)
2129 }
2130 None => None,
2131 };
2132
2133 let mut agent = self.lock()?;
2134 let wrapped = jacs::a2a::provenance::wrap_artifact_with_provenance(
2135 &mut agent,
2136 artifact,
2137 artifact_type,
2138 parent_signatures,
2139 )
2140 .map_err(|e| BindingCoreError::signing_failed(format!("Failed to wrap artifact: {}", e)))?;
2141
2142 serde_json::to_string_pretty(&wrapped).map_err(|e| {
2143 BindingCoreError::serialization_failed(format!(
2144 "Failed to serialize wrapped artifact: {}",
2145 e
2146 ))
2147 })
2148 }
2149
2150 pub fn sign_artifact(
2155 &self,
2156 artifact_json: &str,
2157 artifact_type: &str,
2158 parent_signatures_json: Option<&str>,
2159 ) -> BindingResult<String> {
2160 #[allow(deprecated)]
2161 self.wrap_a2a_artifact(artifact_json, artifact_type, parent_signatures_json)
2162 }
2163
2164 pub fn verify_a2a_artifact(&self, wrapped_json: &str) -> BindingResult<String> {
2168 let wrapped: Value = serde_json::from_str(wrapped_json).map_err(|e| {
2169 BindingCoreError::invalid_argument(format!("Invalid wrapped artifact JSON: {}", e))
2170 })?;
2171
2172 let agent = self.lock()?;
2173 let result =
2174 jacs::a2a::provenance::verify_wrapped_artifact(&agent, &wrapped).map_err(|e| {
2175 BindingCoreError::verification_failed(format!(
2176 "A2A artifact verification error: {}",
2177 e
2178 ))
2179 })?;
2180
2181 serde_json::to_string_pretty(&result).map_err(|e| {
2182 BindingCoreError::serialization_failed(format!(
2183 "Failed to serialize verification result: {}",
2184 e
2185 ))
2186 })
2187 }
2188 pub fn assess_a2a_agent(&self, agent_card_json: &str, policy: &str) -> BindingResult<String> {
2192 use jacs::a2a::AgentCard;
2193 use jacs::a2a::trust::{A2ATrustPolicy, assess_a2a_agent};
2194
2195 let card: AgentCard = serde_json::from_str(agent_card_json).map_err(|e| {
2196 BindingCoreError::invalid_argument(format!("Invalid Agent Card JSON: {}", e))
2197 })?;
2198
2199 let trust_policy = A2ATrustPolicy::from_str_loose(policy).map_err(|e| {
2200 BindingCoreError::invalid_argument(format!("Invalid trust policy '{}': {}", policy, e))
2201 })?;
2202
2203 let agent = self.lock()?;
2204 let assessment = assess_a2a_agent(&agent, &card, trust_policy);
2205
2206 serde_json::to_string_pretty(&assessment).map_err(|e| {
2207 BindingCoreError::serialization_failed(format!(
2208 "Failed to serialize trust assessment: {}",
2209 e
2210 ))
2211 })
2212 }
2213
2214 pub fn verify_a2a_artifact_with_policy(
2230 &self,
2231 wrapped_json: &str,
2232 agent_card_json: &str,
2233 policy: &str,
2234 ) -> BindingResult<String> {
2235 use jacs::a2a::AgentCard;
2236 use jacs::a2a::trust::A2ATrustPolicy;
2237
2238 let wrapped: Value = serde_json::from_str(wrapped_json).map_err(|e| {
2239 BindingCoreError::invalid_argument(format!("Invalid wrapped artifact JSON: {}", e))
2240 })?;
2241
2242 let card: AgentCard = serde_json::from_str(agent_card_json).map_err(|e| {
2243 BindingCoreError::invalid_argument(format!("Invalid Agent Card JSON: {}", e))
2244 })?;
2245
2246 let trust_policy = A2ATrustPolicy::from_str_loose(policy).map_err(|e| {
2247 BindingCoreError::invalid_argument(format!("Invalid trust policy '{}': {}", policy, e))
2248 })?;
2249
2250 let agent = self.lock()?;
2251 let result = jacs::a2a::provenance::verify_wrapped_artifact_with_policy(
2252 &agent,
2253 &wrapped,
2254 &card,
2255 trust_policy,
2256 )
2257 .map_err(|e| {
2258 BindingCoreError::verification_failed(format!(
2259 "A2A artifact verification with policy error: {}",
2260 e
2261 ))
2262 })?;
2263
2264 serde_json::to_string_pretty(&result).map_err(|e| {
2265 BindingCoreError::serialization_failed(format!(
2266 "Failed to serialize verification result: {}",
2267 e
2268 ))
2269 })
2270 }
2271}
2272
2273impl AgentWrapper {
2274 #[cfg(feature = "attestation")]
2289 pub fn create_attestation(&self, params_json: &str) -> BindingResult<String> {
2290 use jacs::attestation::AttestationTraits;
2291 use jacs::attestation::types::*;
2292
2293 let params: Value = serde_json::from_str(params_json).map_err(|e| {
2294 BindingCoreError::serialization_failed(format!(
2295 "Failed to parse attestation params JSON: {}. \
2296 Provide a valid JSON object with 'subject' and 'claims' fields.",
2297 e
2298 ))
2299 })?;
2300
2301 let subject: AttestationSubject =
2303 serde_json::from_value(params.get("subject").cloned().ok_or_else(|| {
2304 BindingCoreError::validation(
2305 "Missing required 'subject' field in attestation params",
2306 )
2307 })?)
2308 .map_err(|e| BindingCoreError::validation(format!("Invalid 'subject' field: {}", e)))?;
2309
2310 let claims: Vec<Claim> =
2312 serde_json::from_value(params.get("claims").cloned().ok_or_else(|| {
2313 BindingCoreError::validation(
2314 "Missing required 'claims' field in attestation params",
2315 )
2316 })?)
2317 .map_err(|e| BindingCoreError::validation(format!("Invalid 'claims' field: {}", e)))?;
2318
2319 let evidence: Vec<EvidenceRef> = if let Some(ev) = params.get("evidence") {
2321 serde_json::from_value(ev.clone()).map_err(|e| {
2322 BindingCoreError::validation(format!("Invalid 'evidence' field: {}", e))
2323 })?
2324 } else {
2325 vec![]
2326 };
2327
2328 let derivation: Option<Derivation> = if let Some(d) = params.get("derivation") {
2330 Some(serde_json::from_value(d.clone()).map_err(|e| {
2331 BindingCoreError::validation(format!("Invalid 'derivation' field: {}", e))
2332 })?)
2333 } else {
2334 None
2335 };
2336
2337 let policy_context: Option<PolicyContext> = if let Some(p) = params.get("policyContext") {
2339 Some(serde_json::from_value(p.clone()).map_err(|e| {
2340 BindingCoreError::validation(format!("Invalid 'policyContext' field: {}", e))
2341 })?)
2342 } else {
2343 None
2344 };
2345
2346 let mut agent = self.lock()?;
2347 let jacs_doc = agent
2348 .create_attestation(
2349 &subject,
2350 &claims,
2351 &evidence,
2352 derivation.as_ref(),
2353 policy_context.as_ref(),
2354 )
2355 .map_err(|e| {
2356 BindingCoreError::document_failed(format!("Failed to create attestation: {}", e))
2357 })?;
2358
2359 serde_json::to_string_pretty(&jacs_doc.value).map_err(|e| {
2360 BindingCoreError::serialization_failed(format!(
2361 "Failed to serialize attestation: {}",
2362 e
2363 ))
2364 })
2365 }
2366
2367 #[cfg(feature = "attestation")]
2372 pub fn verify_attestation(&self, document_key: &str) -> BindingResult<String> {
2373 let agent = self.lock()?;
2374 let result = agent
2375 .verify_attestation_local_impl(document_key)
2376 .map_err(|e| {
2377 BindingCoreError::verification_failed(format!(
2378 "Attestation local verification failed: {}",
2379 e
2380 ))
2381 })?;
2382
2383 serde_json::to_string_pretty(&result).map_err(|e| {
2384 BindingCoreError::serialization_failed(format!(
2385 "Failed to serialize verification result: {}",
2386 e
2387 ))
2388 })
2389 }
2390
2391 #[cfg(feature = "attestation")]
2396 pub fn verify_attestation_full(&self, document_key: &str) -> BindingResult<String> {
2397 let agent = self.lock()?;
2398 let result = agent
2399 .verify_attestation_full_impl(document_key)
2400 .map_err(|e| {
2401 BindingCoreError::verification_failed(format!(
2402 "Attestation full verification failed: {}",
2403 e
2404 ))
2405 })?;
2406
2407 serde_json::to_string_pretty(&result).map_err(|e| {
2408 BindingCoreError::serialization_failed(format!(
2409 "Failed to serialize verification result: {}",
2410 e
2411 ))
2412 })
2413 }
2414
2415 #[cfg(feature = "attestation")]
2420 pub fn lift_to_attestation(
2421 &self,
2422 signed_doc_json: &str,
2423 claims_json: &str,
2424 ) -> BindingResult<String> {
2425 use jacs::attestation::types::Claim;
2426
2427 let claims: Vec<Claim> = serde_json::from_str(claims_json).map_err(|e| {
2428 BindingCoreError::serialization_failed(format!(
2429 "Failed to parse claims JSON: {}. \
2430 Provide a valid JSON array of claim objects.",
2431 e
2432 ))
2433 })?;
2434
2435 let mut agent = self.lock()?;
2436 let jacs_doc =
2437 jacs::attestation::migration::lift_to_attestation(&mut agent, signed_doc_json, &claims)
2438 .map_err(|e| {
2439 BindingCoreError::document_failed(format!(
2440 "Failed to lift document to attestation: {}",
2441 e
2442 ))
2443 })?;
2444
2445 serde_json::to_string_pretty(&jacs_doc.value).map_err(|e| {
2446 BindingCoreError::serialization_failed(format!(
2447 "Failed to serialize attestation: {}",
2448 e
2449 ))
2450 })
2451 }
2452
2453 #[cfg(feature = "attestation")]
2457 pub fn export_attestation_dsse(&self, attestation_json: &str) -> BindingResult<String> {
2458 let att_value: Value = serde_json::from_str(attestation_json).map_err(|e| {
2459 BindingCoreError::serialization_failed(format!(
2460 "Failed to parse attestation JSON: {}",
2461 e
2462 ))
2463 })?;
2464
2465 let envelope = jacs::attestation::dsse::export_dsse(&att_value).map_err(|e| {
2466 BindingCoreError::document_failed(format!("Failed to export DSSE envelope: {}", e))
2467 })?;
2468
2469 serde_json::to_string_pretty(&envelope).map_err(|e| {
2470 BindingCoreError::serialization_failed(format!(
2471 "Failed to serialize DSSE envelope: {}",
2472 e
2473 ))
2474 })
2475 }
2476
2477 pub fn build_auth_header(&self) -> BindingResult<String> {
2486 let mut agent = self.lock()?;
2487 jacs::protocol::build_auth_header(&mut agent).map_err(|e| {
2488 BindingCoreError::signing_failed(format!("Failed to build auth header: {}", e))
2489 })
2490 }
2491
2492 pub fn canonicalize_json(&self, json_string: &str) -> BindingResult<String> {
2496 let value: Value = serde_json::from_str(json_string).map_err(|e| {
2497 BindingCoreError::serialization_failed(format!(
2498 "Failed to parse JSON for canonicalization: {}",
2499 e
2500 ))
2501 })?;
2502 Ok(jacs::protocol::canonicalize_json(&value))
2503 }
2504
2505 pub fn sign_response(&self, payload_json: &str) -> BindingResult<String> {
2511 let mut agent = self.lock()?;
2512 let payload: Value = serde_json::from_str(payload_json).map_err(|e| {
2513 BindingCoreError::serialization_failed(format!(
2514 "Failed to parse payload JSON for sign_response: {}",
2515 e
2516 ))
2517 })?;
2518 let result = jacs::protocol::sign_response(&mut agent, &payload).map_err(|e| {
2519 BindingCoreError::signing_failed(format!("Failed to sign response: {}", e))
2520 })?;
2521 serde_json::to_string(&result).map_err(|e| {
2522 BindingCoreError::serialization_failed(format!(
2523 "Failed to serialize signed response: {}",
2524 e
2525 ))
2526 })
2527 }
2528
2529 pub fn encode_verify_payload(&self, document: &str) -> BindingResult<String> {
2534 Ok(jacs::protocol::encode_verify_payload(document))
2535 }
2536
2537 pub fn decode_verify_payload(&self, encoded: &str) -> BindingResult<String> {
2540 jacs::protocol::decode_verify_payload(encoded).map_err(|e| {
2541 BindingCoreError::serialization_failed(format!(
2542 "Failed to decode verify payload: {}",
2543 e
2544 ))
2545 })
2546 }
2547
2548 pub fn extract_document_id(&self, document: &str) -> BindingResult<String> {
2553 jacs::protocol::extract_document_id(document)
2554 .map_err(|e| BindingCoreError::generic(format!("Failed to extract document ID: {}", e)))
2555 }
2556
2557 pub fn unwrap_signed_event(
2566 &self,
2567 event_json: &str,
2568 server_keys_json: &str,
2569 ) -> BindingResult<String> {
2570 let agent = self.lock()?;
2571 let event: Value = serde_json::from_str(event_json).map_err(|e| {
2572 BindingCoreError::serialization_failed(format!(
2573 "Failed to parse event JSON for unwrap_signed_event: {}",
2574 e
2575 ))
2576 })?;
2577 let keys_map: HashMap<String, String> =
2578 serde_json::from_str(server_keys_json).map_err(|e| {
2579 BindingCoreError::serialization_failed(format!(
2580 "Failed to parse server keys JSON for unwrap_signed_event: {}",
2581 e
2582 ))
2583 })?;
2584 let keys: HashMap<String, Vec<u8>> = keys_map
2585 .into_iter()
2586 .map(|(k, v)| {
2587 let bytes = base64::engine::general_purpose::STANDARD
2588 .decode(&v)
2589 .unwrap_or_else(|_| v.into_bytes());
2590 (k, bytes)
2591 })
2592 .collect();
2593 let (data, verified) =
2594 jacs::protocol::unwrap_signed_event(&agent, &event, &keys).map_err(|e| {
2595 BindingCoreError::verification_failed(format!(
2596 "Failed to unwrap signed event: {}",
2597 e
2598 ))
2599 })?;
2600 let result = json!({"data": data, "verified": verified});
2601 serde_json::to_string(&result).map_err(|e| {
2602 BindingCoreError::serialization_failed(format!(
2603 "Failed to serialize unwrap_signed_event result: {}",
2604 e
2605 ))
2606 })
2607 }
2608}
2609
2610pub fn diagnostics_standalone() -> String {
2617 serde_json::to_string_pretty(&jacs::simple::diagnostics()).unwrap_or_default()
2618}
2619
2620#[derive(Debug, Clone)]
2626pub struct VerificationResult {
2627 pub valid: bool,
2629 pub signer_id: String,
2631 pub timestamp: String,
2633 pub agent_version: String,
2635}
2636
2637pub fn verify_document_standalone(
2656 signed_document: &str,
2657 key_resolution: Option<&str>,
2658 data_directory: Option<&str>,
2659 key_directory: Option<&str>,
2660) -> BindingResult<VerificationResult> {
2661 use std::collections::HashSet;
2662 use std::path::{Path, PathBuf};
2663 use std::sync::{Mutex, OnceLock};
2664
2665 fn absolutize_dir(raw: &str) -> String {
2666 let p = PathBuf::from(raw);
2667 if p.is_absolute() {
2668 p.to_string_lossy().to_string()
2669 } else {
2670 std::env::current_dir()
2671 .unwrap_or_else(|_| PathBuf::from("."))
2672 .join(p)
2673 .to_string_lossy()
2674 .to_string()
2675 }
2676 }
2677
2678 fn sig_field(doc: &str, field: &str) -> String {
2679 serde_json::from_str::<Value>(doc)
2680 .ok()
2681 .and_then(|v| {
2682 v.get("jacsSignature")
2683 .and_then(|s| s.get(field))
2684 .and_then(|f| f.as_str())
2685 .map(String::from)
2686 })
2687 .unwrap_or_default()
2688 }
2689
2690 fn has_local_key_cache(root: &Path, key_hash: &str) -> bool {
2691 if key_hash.is_empty() {
2692 return false;
2693 }
2694 root.join("public_keys")
2695 .join(format!("{}.pem", key_hash))
2696 .exists()
2697 && root
2698 .join("public_keys")
2699 .join(format!("{}.enc_type", key_hash))
2700 .exists()
2701 }
2702
2703 fn build_fixture_key_cache(cache_root: &Path, source_dirs: &[PathBuf]) -> usize {
2704 let public_keys_dir = cache_root.join("public_keys");
2705 if std::fs::create_dir_all(&public_keys_dir).is_err() {
2706 return 0;
2707 }
2708
2709 let mut written: HashSet<String> = HashSet::new();
2710 for dir in source_dirs {
2711 let entries = match std::fs::read_dir(dir) {
2712 Ok(v) => v,
2713 Err(_) => continue,
2714 };
2715
2716 for entry in entries.flatten() {
2717 let path = entry.path();
2718 if !path.is_file() {
2719 continue;
2720 }
2721 let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
2722 continue;
2723 };
2724 let Some(prefix) = name.strip_suffix("_metadata.json") else {
2725 continue;
2726 };
2727
2728 let metadata = match std::fs::read_to_string(&path)
2729 .ok()
2730 .and_then(|s| serde_json::from_str::<Value>(&s).ok())
2731 {
2732 Some(v) => v,
2733 None => continue,
2734 };
2735 let key_hash = metadata
2736 .get("public_key_hash")
2737 .and_then(|v| v.as_str())
2738 .unwrap_or("")
2739 .trim();
2740 let signing_algorithm = metadata
2741 .get("signing_algorithm")
2742 .and_then(|v| v.as_str())
2743 .unwrap_or("")
2744 .trim();
2745 if key_hash.is_empty() || signing_algorithm.is_empty() {
2746 continue;
2747 }
2748 if written.contains(key_hash) {
2749 continue;
2750 }
2751
2752 let key_path = dir.join(format!("{}_public_key.pem", prefix));
2753 let key_bytes = match std::fs::read(&key_path) {
2754 Ok(v) => v,
2755 Err(_) => continue,
2756 };
2757
2758 if std::fs::write(public_keys_dir.join(format!("{}.pem", key_hash)), key_bytes)
2759 .is_err()
2760 {
2761 continue;
2762 }
2763 if std::fs::write(
2764 public_keys_dir.join(format!("{}.enc_type", key_hash)),
2765 signing_algorithm.as_bytes(),
2766 )
2767 .is_err()
2768 {
2769 continue;
2770 }
2771
2772 written.insert(key_hash.to_string());
2773 }
2774 }
2775
2776 written.len()
2777 }
2778
2779 fn standalone_verify_lock() -> &'static Mutex<()> {
2780 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
2781 LOCK.get_or_init(|| Mutex::new(()))
2782 }
2783
2784 let _lock = standalone_verify_lock()
2785 .lock()
2786 .map_err(|e| BindingCoreError::generic(format!("Failed to lock standalone verify: {e}")))?;
2787
2788 let signer_id = sig_field(signed_document, "agentID");
2789 let timestamp = sig_field(signed_document, "date");
2790 let agent_version = sig_field(signed_document, "agentVersion");
2791 let signer_public_key_hash = sig_field(signed_document, "publicKeyHash");
2792
2793 let temp_dir = std::env::temp_dir().to_string_lossy().to_string();
2796 let raw_data_dir = data_directory
2797 .map(String::from)
2798 .unwrap_or_else(|| temp_dir.clone());
2799 let raw_key_dir = key_directory
2800 .map(String::from)
2801 .unwrap_or_else(|| raw_data_dir.clone());
2802
2803 let absolute_data_dir = absolutize_dir(&raw_data_dir);
2804 let absolute_key_dir = absolutize_dir(&raw_key_dir);
2805
2806 let mut effective_storage_root = if data_directory.is_some() {
2809 absolute_data_dir.clone()
2810 } else if key_directory.is_some() {
2811 absolute_key_dir.clone()
2812 } else {
2813 absolute_data_dir.clone()
2814 };
2815 let mut temp_cache_root: Option<PathBuf> = None;
2816
2817 let local_requested = key_resolution.is_none_or(|kr| {
2822 kr.split(',')
2823 .any(|part| part.trim().eq_ignore_ascii_case("local"))
2824 });
2825 if local_requested && !signer_public_key_hash.is_empty() {
2826 let current_root = PathBuf::from(&effective_storage_root);
2827 if !has_local_key_cache(¤t_root, &signer_public_key_hash) {
2828 let mut source_dirs = Vec::new();
2829 let data_path = PathBuf::from(&absolute_data_dir);
2830 let key_path = PathBuf::from(&absolute_key_dir);
2831 if data_path.exists() {
2832 source_dirs.push(data_path);
2833 }
2834 if key_path.exists() && !source_dirs.iter().any(|p| p == &key_path) {
2835 source_dirs.push(key_path);
2836 }
2837
2838 if !source_dirs.is_empty() {
2839 let nonce = std::time::SystemTime::now()
2840 .duration_since(std::time::UNIX_EPOCH)
2841 .map(|d| d.as_nanos())
2842 .unwrap_or(0);
2843 let temp_root = std::env::temp_dir()
2844 .canonicalize()
2845 .unwrap_or_else(|_| std::env::temp_dir());
2846 let cache_root = temp_root.join(format!(
2847 "jacs_standalone_keycache_{}_{}",
2848 std::process::id(),
2849 nonce
2850 ));
2851 let _ = build_fixture_key_cache(&cache_root, &source_dirs);
2852 if has_local_key_cache(&cache_root, &signer_public_key_hash) {
2853 effective_storage_root = cache_root.to_string_lossy().to_string();
2854 temp_cache_root = Some(cache_root);
2855 } else {
2856 let _ = std::fs::remove_dir_all(&cache_root);
2857 }
2858 }
2859 }
2860 }
2861 let explicit_local_key_available = local_requested
2862 && !signer_public_key_hash.is_empty()
2863 && has_local_key_cache(
2864 &PathBuf::from(&effective_storage_root),
2865 &signer_public_key_hash,
2866 );
2867
2868 let data_dir = String::new();
2871 let key_dir = String::new();
2872
2873 let config = Config::new(
2874 Some("false".to_string()),
2875 Some(data_dir.clone()),
2876 Some(key_dir.clone()),
2877 Some("jacs.private.pem.enc".to_string()),
2878 Some("jacs.public.pem".to_string()),
2879 Some("pq2025".to_string()),
2880 None,
2881 Some("".to_string()),
2882 Some("fs".to_string()),
2883 );
2884 let config_json = serde_json::to_string_pretty(&config).map_err(|e| {
2885 BindingCoreError::serialization_failed(format!("Failed to serialize config: {}", e))
2886 })?;
2887
2888 let thread_id = format!("{:?}", std::thread::current().id())
2889 .chars()
2890 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
2891 .collect::<String>();
2892 let nonce = std::time::SystemTime::now()
2893 .duration_since(std::time::UNIX_EPOCH)
2894 .map(|d| d.as_nanos())
2895 .unwrap_or(0);
2896 let temp_root = std::env::temp_dir()
2897 .canonicalize()
2898 .unwrap_or_else(|_| std::env::temp_dir());
2899 let config_path = temp_root.join(format!(
2900 "jacs_standalone_verify_config_{}_{}_{}.json",
2901 std::process::id(),
2902 thread_id,
2903 nonce
2904 ));
2905 std::fs::write(&config_path, &config_json)
2906 .map_err(|e| BindingCoreError::generic(format!("Failed to write temp config: {}", e)))?;
2907
2908 use jacs::storage::jenv;
2915
2916 struct KeyResolutionGuard {
2918 had_override: bool,
2919 prev_value: Option<String>,
2920 }
2921 impl Drop for KeyResolutionGuard {
2922 fn drop(&mut self) {
2923 if self.had_override {
2924 if let Some(ref val) = self.prev_value {
2925 let _ = jacs::storage::jenv::set_env_var("JACS_KEY_RESOLUTION", val);
2926 } else {
2927 let _ = jacs::storage::jenv::clear_env_var("JACS_KEY_RESOLUTION");
2928 }
2929 } else {
2930 let _ = jacs::storage::jenv::clear_env_var("JACS_KEY_RESOLUTION");
2931 }
2932 }
2933 }
2934 let kr_had_override = jenv::has_jenv_override("JACS_KEY_RESOLUTION");
2935 let kr_prev = if kr_had_override {
2936 jenv::get_env_var("JACS_KEY_RESOLUTION", false)
2937 .ok()
2938 .flatten()
2939 } else {
2940 None
2941 };
2942 if let Some(kr) = key_resolution {
2943 let _ = jenv::set_env_var("JACS_KEY_RESOLUTION", kr);
2944 } else {
2945 let _ = jenv::clear_env_var("JACS_KEY_RESOLUTION");
2946 }
2947 let _kr_guard = KeyResolutionGuard {
2948 had_override: kr_had_override,
2949 prev_value: kr_prev,
2950 };
2951
2952 let result: BindingResult<VerificationResult> = (|| {
2953 let wrapper = AgentWrapper::new();
2954 wrapper.load_file_only(config_path.to_string_lossy().to_string())?;
2955 let _ = wrapper.set_storage_root(PathBuf::from(&effective_storage_root));
2956
2957 if explicit_local_key_available {
2958 let key_base = PathBuf::from(&effective_storage_root)
2959 .join("public_keys")
2960 .join(&signer_public_key_hash);
2961 let public_key = std::fs::read(key_base.with_extension("pem")).map_err(|e| {
2962 BindingCoreError::verification_failed(format!(
2963 "Failed to load local public key for hash '{}': {}",
2964 signer_public_key_hash, e
2965 ))
2966 })?;
2967 let enc_type = std::fs::read_to_string(key_base.with_extension("enc_type"))
2968 .map_err(|e| {
2969 BindingCoreError::verification_failed(format!(
2970 "Failed to load local public key type for hash '{}': {}",
2971 signer_public_key_hash, e
2972 ))
2973 })?
2974 .trim()
2975 .to_string();
2976
2977 let mut agent = wrapper.lock()?;
2978 let doc = agent.load_document(signed_document).map_err(|e| {
2979 BindingCoreError::document_failed(format!("Failed to load document: {}", e))
2980 })?;
2981 let document_key = doc.getkey();
2982 let value = doc.getvalue();
2983 agent.verify_hash(value).map_err(|e| {
2984 BindingCoreError::verification_failed(format!(
2985 "Failed to verify document hash: {}",
2986 e
2987 ))
2988 })?;
2989 agent
2990 .verify_document_signature(
2991 &document_key,
2992 None,
2993 None,
2994 Some(public_key),
2995 Some(enc_type.clone()),
2996 )
2997 .map_err(|e| {
2998 BindingCoreError::verification_failed(format!(
2999 "Failed to verify document signature (enc_type={}): {}",
3000 enc_type, e
3001 ))
3002 })?;
3003
3004 return Ok(VerificationResult {
3005 valid: true,
3006 signer_id: signer_id.clone(),
3007 timestamp: timestamp.clone(),
3008 agent_version: agent_version.clone(),
3009 });
3010 }
3011
3012 let valid = wrapper.verify_document(signed_document)?;
3013 Ok(VerificationResult {
3014 valid,
3015 signer_id: signer_id.clone(),
3016 timestamp: timestamp.clone(),
3017 agent_version: agent_version.clone(),
3018 })
3019 })();
3020
3021 let _ = std::fs::remove_file(&config_path);
3023 if let Some(cache_root) = temp_cache_root {
3024 let _ = std::fs::remove_dir_all(cache_root);
3025 }
3026
3027 match result {
3028 Ok(r) => Ok(r),
3029 Err(e) => {
3030 if e.kind == ErrorKind::VerificationFailed
3031 || e.kind == ErrorKind::DocumentFailed
3032 || e.kind == ErrorKind::InvalidArgument
3033 {
3034 Ok(VerificationResult {
3035 valid: false,
3036 signer_id,
3037 timestamp,
3038 agent_version,
3039 })
3040 } else {
3041 Err(e)
3042 }
3043 }
3044 }
3045}
3046
3047pub fn hash_string(data: &str) -> String {
3053 jacs_hash_string(data)
3054}
3055
3056pub fn hash_public_key_base64(public_key_b64: &str) -> BindingResult<String> {
3058 let public_key = decode_public_key_base64(public_key_b64)?;
3059 Ok(jacs::crypt::hash::hash_public_key(&public_key))
3060}
3061
3062pub fn build_jwk_set_from_public_key(
3064 public_key_b64: &str,
3065 key_algorithm: &str,
3066 key_id: &str,
3067) -> BindingResult<String> {
3068 let public_key = decode_public_key_base64(public_key_b64)?;
3069 let jwk_set = build_jwk_set_from_public_key_bytes(&public_key, key_algorithm, key_id)?;
3070 serde_json::to_string(&jwk_set).map_err(|e| {
3071 BindingCoreError::serialization_failed(format!("Failed to serialize JWK set: {}", e))
3072 })
3073}
3074
3075pub fn ensure_network_access(capability: &str) -> BindingResult<()> {
3077 let capability = jacs::config::NetworkCapability::from_str(capability)
3078 .map_err(BindingCoreError::invalid_argument)?;
3079 jacs::config::ensure_network_access(capability)
3080 .map_err(|e| BindingCoreError::network_failed(e.to_string()))
3081}
3082
3083pub fn fetch_agent_card(base_url: &str, timeout_ms: Option<u64>) -> BindingResult<String> {
3085 let trimmed = base_url.trim();
3086 if trimmed.is_empty() {
3087 return Err(BindingCoreError::invalid_argument(
3088 "Agent base URL cannot be empty",
3089 ));
3090 }
3091
3092 let card_url = format!(
3093 "{}/.well-known/agent-card.json",
3094 trimmed.trim_end_matches('/')
3095 );
3096 let parsed_url = Url::parse(&card_url).map_err(|e| {
3097 BindingCoreError::invalid_argument(format!("Invalid agent URL '{}': {}", base_url, e))
3098 })?;
3099 validate_network_url(&parsed_url, "Agent Card URL")?;
3100 jacs::config::ensure_network_access(jacs::config::NetworkCapability::AgentCardFetch)
3101 .map_err(|e| BindingCoreError::network_failed(e.to_string()))?;
3102
3103 let client = build_blocking_json_client(timeout_ms.unwrap_or(DEFAULT_NETWORK_TIMEOUT_MS))?;
3104 let response = client
3105 .get(parsed_url.clone())
3106 .header(ACCEPT, "application/json")
3107 .send()
3108 .map_err(|e| {
3109 if e.is_timeout() {
3110 BindingCoreError::network_failed(format!(
3111 "Agent discovery timed out: {}",
3112 parsed_url
3113 ))
3114 } else {
3115 BindingCoreError::network_failed(format!(
3116 "Agent unreachable: {} ({})",
3117 parsed_url, e
3118 ))
3119 }
3120 })?;
3121
3122 if response.status() == StatusCode::NOT_FOUND {
3123 return Err(BindingCoreError::network_failed(format!(
3124 "Agent card not found (404): {}",
3125 parsed_url
3126 )));
3127 }
3128
3129 if !response.status().is_success() {
3130 return Err(BindingCoreError::network_failed(format!(
3131 "Agent card request failed (HTTP {}): {}",
3132 response.status(),
3133 parsed_url
3134 )));
3135 }
3136
3137 let content_type = content_type_header(&response);
3138 if !content_type.is_empty() && !content_type.to_ascii_lowercase().contains("json") {
3139 return Err(BindingCoreError::validation(format!(
3140 "Agent card response is not JSON (content-type: {}): {}",
3141 content_type, parsed_url
3142 )));
3143 }
3144
3145 let body = response.text().map_err(|e| {
3146 BindingCoreError::network_failed(format!(
3147 "Failed to read Agent Card response from {}: {}",
3148 parsed_url, e
3149 ))
3150 })?;
3151
3152 parse_json_object_body(
3153 &body,
3154 format!("Agent card is not valid JSON: {}", parsed_url),
3155 format!("Agent card at {} is not a JSON object", parsed_url),
3156 )
3157}
3158
3159pub fn fetch_remote_key_lookup(
3161 base_url: Option<&str>,
3162 jacs_id: Option<&str>,
3163 version: Option<&str>,
3164 public_key_hash: Option<&str>,
3165 timeout_ms: Option<u64>,
3166) -> BindingResult<String> {
3167 let resolved_base_url = resolve_keys_base_url(base_url);
3168 let mut parsed_url = Url::parse(&resolved_base_url).map_err(|e| {
3169 BindingCoreError::invalid_argument(format!(
3170 "Invalid JACS key base URL '{}': {}",
3171 resolved_base_url, e
3172 ))
3173 })?;
3174 validate_network_url(&parsed_url, "JACS key lookup base URL")?;
3175
3176 {
3177 let mut segments = parsed_url.path_segments_mut().map_err(|_| {
3178 BindingCoreError::invalid_argument(format!(
3179 "Invalid JACS key base URL '{}': cannot append path segments",
3180 resolved_base_url
3181 ))
3182 })?;
3183
3184 if let Some(hash) = public_key_hash
3185 .map(str::trim)
3186 .filter(|value| !value.is_empty())
3187 {
3188 let normalized_hash = normalize_public_key_hash(hash)?;
3189 segments.extend(["jacs", "v1", "keys", "by-hash"]);
3190 segments.push(&normalized_hash);
3191 } else {
3192 let agent_id = jacs_id
3193 .map(str::trim)
3194 .filter(|value| !value.is_empty())
3195 .ok_or_else(|| {
3196 BindingCoreError::invalid_argument(
3197 "fetch_remote_key_lookup requires jacs_id or public_key_hash",
3198 )
3199 })?;
3200 let resolved_version = version
3201 .map(str::trim)
3202 .filter(|value| !value.is_empty())
3203 .unwrap_or("latest");
3204 segments.extend(["jacs", "v1", "agents"]);
3205 segments.push(agent_id);
3206 segments.push("keys");
3207 segments.push(resolved_version);
3208 }
3209 }
3210
3211 jacs::config::ensure_network_access(jacs::config::NetworkCapability::RemoteKeyFetch)
3212 .map_err(|e| BindingCoreError::network_failed(e.to_string()))?;
3213
3214 let client = build_blocking_json_client(timeout_ms.unwrap_or(DEFAULT_NETWORK_TIMEOUT_MS))?;
3215 let response = client
3216 .get(parsed_url.clone())
3217 .header(ACCEPT, "application/json")
3218 .send()
3219 .map_err(|e| {
3220 if e.is_timeout() {
3221 BindingCoreError::network_failed(format!(
3222 "Remote key lookup timed out: {}",
3223 parsed_url
3224 ))
3225 } else {
3226 BindingCoreError::network_failed(format!(
3227 "Failed to reach key lookup endpoint {}: {}",
3228 parsed_url, e
3229 ))
3230 }
3231 })?;
3232
3233 let status = response.status();
3234 let content_type = content_type_header(&response);
3235 let body = response.text().map_err(|e| {
3236 BindingCoreError::network_failed(format!(
3237 "Failed to read key lookup response from {}: {}",
3238 parsed_url, e
3239 ))
3240 })?;
3241
3242 if !status.is_success() {
3243 let detail = if body.trim().is_empty() {
3244 status.canonical_reason().unwrap_or("unknown error")
3245 } else {
3246 body.trim()
3247 };
3248 return Err(BindingCoreError::network_failed(format!(
3249 "HTTP {} from key lookup endpoint: {}",
3250 status.as_u16(),
3251 detail
3252 )));
3253 }
3254
3255 if !content_type.is_empty() && !content_type.to_ascii_lowercase().contains("json") {
3256 return Err(BindingCoreError::validation(format!(
3257 "Key lookup endpoint returned non-JSON response: {}",
3258 parsed_url
3259 )));
3260 }
3261
3262 parse_json_object_body(
3263 &body,
3264 format!(
3265 "Key lookup endpoint returned non-JSON response: {}",
3266 parsed_url
3267 ),
3268 format!(
3269 "Key lookup endpoint returned a non-object response: {}",
3270 parsed_url
3271 ),
3272 )
3273}
3274
3275pub fn resolve_private_key_password(
3279 config_path: Option<&str>,
3280 key_directory: Option<&str>,
3281 explicit_password: Option<&str>,
3282) -> BindingResult<String> {
3283 if let Some(password) = explicit_password {
3284 if password.trim().is_empty() {
3285 return Err(BindingCoreError::invalid_argument(
3286 "Explicit password provided but empty or whitespace-only.",
3287 ));
3288 }
3289 return Ok(password.to_string());
3290 }
3291
3292 if let Ok(password) = std::env::var("JACS_PRIVATE_KEY_PASSWORD") {
3293 if password.trim().is_empty() {
3294 return Err(BindingCoreError::invalid_argument(
3295 "JACS_PRIVATE_KEY_PASSWORD is set but empty or whitespace-only.",
3296 ));
3297 }
3298 return Ok(password);
3299 }
3300
3301 let (resolved_key_directory, agent_id) = resolve_password_context(config_path, key_directory)?;
3302
3303 match jacs::crypt::aes_encrypt::resolve_private_key_password(None, agent_id.as_deref()) {
3304 Ok(password) => Ok(password),
3305 Err(e) if missing_password_message(&e.to_string()) => Ok(read_password_file(
3306 &resolved_key_directory.join(".jacs_password"),
3307 )?
3308 .unwrap_or_default()),
3309 Err(e) => Err(BindingCoreError::generic(format!(
3310 "Failed to resolve private key password: {}",
3311 e
3312 ))),
3313 }
3314}
3315
3316pub fn quickstart_private_key_password(
3318 config_path: Option<&str>,
3319 key_directory: Option<&str>,
3320) -> BindingResult<String> {
3321 let existing = resolve_private_key_password(config_path, key_directory, None)?;
3322 if !existing.is_empty() {
3323 return Ok(existing);
3324 }
3325
3326 let password = generate_private_key_password_value();
3327 if truthy_env_var("JACS_SAVE_PASSWORD_FILE") {
3328 let (resolved_key_directory, _agent_id) =
3329 resolve_password_context(config_path, key_directory)?;
3330 persist_password_file(&resolved_key_directory, &password)?;
3331 }
3332
3333 Ok(password)
3334}
3335
3336#[allow(clippy::too_many_arguments)]
3338pub fn create_config(
3339 jacs_use_security: Option<String>,
3340 jacs_data_directory: Option<String>,
3341 jacs_key_directory: Option<String>,
3342 jacs_agent_private_key_filename: Option<String>,
3343 jacs_agent_public_key_filename: Option<String>,
3344 jacs_agent_key_algorithm: Option<String>,
3345 jacs_private_key_password: Option<String>,
3346 jacs_agent_id_and_version: Option<String>,
3347 jacs_default_storage: Option<String>,
3348) -> BindingResult<String> {
3349 let config = Config::new(
3350 jacs_use_security,
3351 jacs_data_directory,
3352 jacs_key_directory,
3353 jacs_agent_private_key_filename,
3354 jacs_agent_public_key_filename,
3355 jacs_agent_key_algorithm,
3356 jacs_private_key_password,
3357 jacs_agent_id_and_version,
3358 jacs_default_storage,
3359 );
3360
3361 serde_json::to_string_pretty(&config).map_err(|e| {
3362 BindingCoreError::serialization_failed(format!("Failed to serialize config: {}", e))
3363 })
3364}
3365
3366pub fn trust_agent(agent_json: &str) -> BindingResult<String> {
3372 jacs::trust::trust_agent(agent_json)
3373 .map_err(|e| BindingCoreError::trust_failed(format!("Failed to trust agent: {}", e)))
3374}
3375
3376pub fn trust_agent_with_key(agent_json: &str, public_key_pem: &str) -> BindingResult<String> {
3380 if public_key_pem.trim().is_empty() {
3381 return Err(BindingCoreError::invalid_argument(
3382 "public_key_pem cannot be empty",
3383 ));
3384 }
3385 jacs::trust::trust_agent_with_key(agent_json, Some(public_key_pem)).map_err(|e| {
3386 BindingCoreError::trust_failed(format!("Failed to trust agent with explicit key: {}", e))
3387 })
3388}
3389
3390pub fn list_trusted_agents() -> BindingResult<Vec<String>> {
3392 jacs::trust::list_trusted_agents().map_err(|e| {
3393 BindingCoreError::trust_failed(format!("Failed to list trusted agents: {}", e))
3394 })
3395}
3396
3397pub fn untrust_agent(agent_id: &str) -> BindingResult<()> {
3399 jacs::trust::untrust_agent(agent_id)
3400 .map_err(|e| BindingCoreError::trust_failed(format!("Failed to untrust agent: {}", e)))
3401}
3402
3403pub fn is_trusted(agent_id: &str) -> bool {
3405 jacs::trust::is_trusted(agent_id)
3406}
3407
3408pub fn get_trusted_agent(agent_id: &str) -> BindingResult<String> {
3410 jacs::trust::get_trusted_agent(agent_id)
3411 .map_err(|e| BindingCoreError::trust_failed(format!("Failed to get trusted agent: {}", e)))
3412}
3413
3414pub fn audit(config_path: Option<&str>, recent_n: Option<u32>) -> BindingResult<String> {
3423 use jacs::audit::{AuditOptions, audit as jacs_audit};
3424
3425 let opts = AuditOptions {
3426 config_path: config_path.map(String::from),
3427 recent_verify_count: recent_n.or(AuditOptions::default().recent_verify_count),
3428 ..Default::default()
3429 };
3430 let result =
3431 jacs_audit(opts).map_err(|e| BindingCoreError::generic(format!("Audit failed: {}", e)))?;
3432 serde_json::to_string_pretty(&result).map_err(|e| {
3433 BindingCoreError::serialization_failed(format!("Failed to serialize audit result: {}", e))
3434 })
3435}
3436
3437#[allow(clippy::too_many_arguments)]
3445pub fn create_agent_programmatic(
3446 name: &str,
3447 password: &str,
3448 algorithm: Option<&str>,
3449 data_directory: Option<&str>,
3450 key_directory: Option<&str>,
3451 config_path: Option<&str>,
3452 agent_type: Option<&str>,
3453 description: Option<&str>,
3454 domain: Option<&str>,
3455 default_storage: Option<&str>,
3456) -> BindingResult<String> {
3457 use jacs::simple::{CreateAgentParams, SimpleAgent};
3458
3459 let params = CreateAgentParams {
3460 name: name.to_string(),
3461 password: password.to_string(),
3462 algorithm: algorithm.unwrap_or("pq2025").to_string(),
3463 data_directory: data_directory.unwrap_or("./jacs_data").to_string(),
3464 key_directory: key_directory.unwrap_or("./jacs_keys").to_string(),
3465 config_path: config_path.unwrap_or("./jacs.config.json").to_string(),
3466 agent_type: agent_type.unwrap_or("ai").to_string(),
3467 description: description.unwrap_or("").to_string(),
3468 domain: domain.unwrap_or("").to_string(),
3469 default_storage: default_storage.unwrap_or("fs").to_string(),
3470 storage: None,
3471 };
3472
3473 let (_agent, info) = SimpleAgent::create_with_params(params)
3474 .map_err(|e| BindingCoreError::agent_load(format!("Failed to create agent: {}", e)))?;
3475
3476 serde_json::to_string_pretty(&info).map_err(|e| {
3477 BindingCoreError::serialization_failed(format!("Failed to serialize agent info: {}", e))
3478 })
3479}
3480
3481pub fn handle_agent_create(filename: Option<&String>, create_keys: bool) -> BindingResult<()> {
3483 jacs::cli_utils::create::handle_agent_create(filename, create_keys)
3484 .map_err(|e| BindingCoreError::generic(e.to_string()))
3485}
3486
3487pub fn handle_agent_create_auto(
3490 filename: Option<&String>,
3491 create_keys: bool,
3492 auto_update_config: bool,
3493) -> BindingResult<()> {
3494 jacs::cli_utils::create::handle_agent_create_auto(filename, create_keys, auto_update_config)
3495 .map_err(|e| BindingCoreError::generic(e.to_string()))
3496}
3497
3498pub fn handle_config_create() -> BindingResult<()> {
3500 jacs::cli_utils::create::handle_config_create()
3501 .map_err(|e| BindingCoreError::generic(e.to_string()))
3502}
3503
3504pub use jacs::dns::bootstrap::DnsVerificationResult;
3510
3511pub fn verify_agent_dns(agent_json: &str, domain: &str) -> BindingResult<DnsVerificationResult> {
3516 jacs::dns::bootstrap::verify_agent_dns(agent_json, domain).map_err(|e| {
3517 BindingCoreError::invalid_argument(format!("DNS verification setup failed: {}", e))
3518 })
3519}
3520
3521pub use jacs;
3526
3527#[cfg(test)]
3532mod tests {
3533 use super::*;
3534 use std::path::PathBuf;
3535 use tempfile::tempdir;
3536
3537 fn cross_language_fixtures_dir() -> Option<PathBuf> {
3538 let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3539 .parent()?
3540 .to_path_buf();
3541 let dir = workspace.join("jacs/tests/fixtures/cross-language");
3542 if dir.exists() { Some(dir) } else { None }
3543 }
3544
3545 #[test]
3546 fn verify_standalone_invalid_json_returns_valid_false() {
3547 let result = verify_document_standalone("not json", Some("local"), None, None).unwrap();
3548 assert!(!result.valid);
3549 assert_eq!(result.signer_id, "");
3550 }
3551
3552 #[test]
3553 fn verify_standalone_tampered_document_returns_valid_false_with_signer_id() {
3554 let tampered = r#"{"jacsSignature":{"agentID":"golden-test-agent","agentVersion":"v1"},"jacsSha256":"x"}"#;
3555 let result = verify_document_standalone(tampered, Some("local"), None, None).unwrap();
3556 assert!(!result.valid);
3557 assert_eq!(result.signer_id, "golden-test-agent");
3558 }
3559
3560 #[test]
3561 fn verify_standalone_golden_invalid_signature_returns_valid_false() {
3562 let invalid_sig =
3563 std::fs::read_to_string("../jacs/tests/fixtures/golden/invalid_signature.json")
3564 .unwrap_or_else(|_| {
3565 r#"{"jacsSignature":{"agentID":"golden-test-agent"},"jacsSha256":"x"}"#
3566 .to_string()
3567 });
3568 let result = verify_document_standalone(
3569 &invalid_sig,
3570 Some("local"),
3571 Some("../jacs/tests/fixtures"),
3572 Some("../jacs/tests/fixtures/keys"),
3573 )
3574 .unwrap();
3575 assert!(!result.valid);
3576 assert_eq!(result.signer_id, "golden-test-agent");
3577 }
3578
3579 #[test]
3580 fn verify_standalone_nonexistent_key_directory_returns_valid_false() {
3581 let doc = r#"{"jacsSignature":{"agentID":"some-agent"},"jacsSha256":"x"}"#;
3582 let result = verify_document_standalone(
3583 doc,
3584 Some("local"),
3585 Some("/nonexistent_data"),
3586 Some("/nonexistent_keys"),
3587 )
3588 .unwrap();
3589 assert!(!result.valid);
3590 assert_eq!(result.signer_id, "some-agent");
3591 }
3592
3593 #[cfg(unix)]
3594 #[test]
3595 fn read_password_file_rejects_insecure_permissions() {
3596 let tmp = tempdir().expect("tempdir");
3597 let password_path = tmp.path().join(".jacs_password");
3598 fs::write(&password_path, "TopSecret!123").expect("write password file");
3599 fs::set_permissions(&password_path, std::fs::Permissions::from_mode(0o644))
3600 .expect("set insecure permissions");
3601
3602 let result = read_password_file(&password_path);
3603 assert!(result.is_err(), "insecure password file should be rejected");
3604 assert!(
3605 result.unwrap_err().message.contains("insecure permissions"),
3606 "error should mention permissions"
3607 );
3608 }
3609
3610 #[cfg(unix)]
3611 #[test]
3612 fn persist_password_file_creates_owner_only_password_file() {
3613 let tmp = tempdir().expect("tempdir");
3614 persist_password_file(tmp.path(), "TopSecret!123").expect("persist password file");
3615
3616 let password_path = tmp.path().join(".jacs_password");
3617 let metadata = fs::metadata(&password_path).expect("stat password file");
3618 let mode = metadata.permissions().mode() & 0o777;
3619
3620 assert_eq!(mode, 0o600, "password file should be created with 0600");
3621 let saved = fs::read_to_string(&password_path).expect("read password file");
3622 assert_eq!(saved, "TopSecret!123");
3623 }
3624
3625 #[test]
3626 #[ignore = "pre-existing: cross-language fixture verification fails with relative parent paths"]
3627 fn verify_standalone_accepts_relative_parent_paths_from_subdir() {
3628 let Some(fixtures_dir) = cross_language_fixtures_dir() else {
3629 eprintln!("Skipping: cross-language fixtures directory not found");
3630 return;
3631 };
3632 let signed_path = fixtures_dir.join("python_ed25519_signed.json");
3633 if !signed_path.exists() {
3634 eprintln!(
3635 "Skipping: fixture '{}' not found",
3636 signed_path.to_string_lossy()
3637 );
3638 return;
3639 }
3640 let signed = std::fs::read_to_string(&signed_path).expect("read python fixture");
3641
3642 let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3643 .parent()
3644 .expect("workspace root")
3645 .to_path_buf();
3646 let jacsnpm_dir = workspace.join("jacsnpm");
3647 if !jacsnpm_dir.exists() {
3648 eprintln!("Skipping: jacsnpm directory not found");
3649 return;
3650 }
3651
3652 struct CwdGuard(PathBuf);
3653 impl Drop for CwdGuard {
3654 fn drop(&mut self) {
3655 let _ = std::env::set_current_dir(&self.0);
3656 }
3657 }
3658
3659 let original_cwd = std::env::current_dir().expect("current dir");
3660 std::env::set_current_dir(&jacsnpm_dir).expect("chdir to jacsnpm");
3661 let _guard = CwdGuard(original_cwd);
3662
3663 let rel = "../jacs/tests/fixtures/cross-language";
3664 let result = verify_document_standalone(&signed, Some("local"), Some(rel), Some(rel))
3665 .expect("standalone verify should not error");
3666 assert!(result.valid, "relative parent-path fixture should verify");
3667 }
3668
3669 #[test]
3670 fn verify_standalone_accepts_absolute_fixture_paths() {
3671 let Some(fixtures_dir) = cross_language_fixtures_dir() else {
3672 eprintln!("Skipping: cross-language fixtures directory not found");
3673 return;
3674 };
3675 let signed_path = fixtures_dir.join("python_ed25519_signed.json");
3676 if !signed_path.exists() {
3677 eprintln!(
3678 "Skipping: fixture '{}' not found",
3679 signed_path.to_string_lossy()
3680 );
3681 return;
3682 }
3683 let signed = std::fs::read_to_string(&signed_path).expect("read python fixture");
3684 let fixtures_abs = fixtures_dir
3685 .canonicalize()
3686 .unwrap_or_else(|_| fixtures_dir.clone());
3687 let fixtures_abs_str = fixtures_abs.to_string_lossy().to_string();
3688
3689 let result = verify_document_standalone(
3690 &signed,
3691 Some("local"),
3692 Some(&fixtures_abs_str),
3693 Some(&fixtures_abs_str),
3694 )
3695 .expect("standalone verify should not error");
3696 assert!(result.valid, "absolute-path fixture should verify");
3697 }
3698
3699 #[test]
3700 fn verify_standalone_uses_key_directory_when_data_directory_missing() {
3701 let Some(fixtures_dir) = cross_language_fixtures_dir() else {
3702 eprintln!("Skipping: cross-language fixtures directory not found");
3703 return;
3704 };
3705 let signed_path = fixtures_dir.join("python_ed25519_signed.json");
3706 if !signed_path.exists() {
3707 eprintln!(
3708 "Skipping: fixture '{}' not found",
3709 signed_path.to_string_lossy()
3710 );
3711 return;
3712 }
3713 let signed = std::fs::read_to_string(&signed_path).expect("read python fixture");
3714 let fixtures_abs = fixtures_dir
3715 .canonicalize()
3716 .unwrap_or_else(|_| fixtures_dir.clone());
3717 let fixtures_abs_str = fixtures_abs.to_string_lossy().to_string();
3718
3719 let result =
3720 verify_document_standalone(&signed, Some("local"), None, Some(&fixtures_abs_str))
3721 .expect("standalone verify should not error");
3722 assert!(
3723 result.valid,
3724 "key_directory should be usable as standalone storage root when data_directory is omitted"
3725 );
3726 }
3727
3728 #[test]
3729 fn verify_standalone_ignores_polluting_env_overrides() {
3730 let Some(fixtures_dir) = cross_language_fixtures_dir() else {
3731 eprintln!("Skipping: cross-language fixtures directory not found");
3732 return;
3733 };
3734 let signed_path = fixtures_dir.join("python_ed25519_signed.json");
3735 if !signed_path.exists() {
3736 eprintln!(
3737 "Skipping: fixture '{}' not found",
3738 signed_path.to_string_lossy()
3739 );
3740 return;
3741 }
3742 let signed = std::fs::read_to_string(&signed_path).expect("read python fixture");
3743 let fixtures_abs = fixtures_dir
3744 .canonicalize()
3745 .unwrap_or_else(|_| fixtures_dir.clone());
3746 let fixtures_abs_str = fixtures_abs.to_string_lossy().to_string();
3747
3748 struct EnvRestore(Vec<(&'static str, Option<std::ffi::OsString>)>);
3749 impl Drop for EnvRestore {
3750 fn drop(&mut self) {
3751 for (k, v) in &self.0 {
3752 if let Some(val) = v {
3753 unsafe { std::env::set_var(k, val) }
3755 } else {
3756 unsafe { std::env::remove_var(k) }
3758 }
3759 }
3760 }
3761 }
3762
3763 let keys = [
3764 "JACS_DATA_DIRECTORY",
3765 "JACS_KEY_DIRECTORY",
3766 "JACS_DEFAULT_STORAGE",
3767 "JACS_KEY_RESOLUTION",
3768 ];
3769 let mut prev = Vec::new();
3770 for k in keys {
3771 prev.push((k, std::env::var_os(k)));
3772 }
3773 let _restore = EnvRestore(prev);
3774
3775 unsafe {
3778 std::env::set_var("JACS_DATA_DIRECTORY", "/tmp/does-not-exist");
3779 std::env::set_var("JACS_KEY_DIRECTORY", "/tmp/does-not-exist");
3780 std::env::set_var("JACS_DEFAULT_STORAGE", "memory");
3781 std::env::set_var("JACS_KEY_RESOLUTION", "remote");
3782 }
3783
3784 let result = verify_document_standalone(
3785 &signed,
3786 Some("local"),
3787 Some(&fixtures_abs_str),
3788 Some(&fixtures_abs_str),
3789 )
3790 .expect("standalone verify should not error");
3791
3792 assert!(
3793 result.valid,
3794 "verification should ignore ambient JACS_* env pollution"
3795 );
3796 }
3797
3798 #[test]
3799 fn audit_default_returns_ok_json_has_risks_and_health_checks() {
3800 let json = audit(None, None).unwrap();
3801 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3802 assert!(v.get("risks").is_some(), "audit JSON should have risks");
3803 assert!(
3804 v.get("health_checks").is_some(),
3805 "audit JSON should have health_checks"
3806 );
3807 }
3808
3809 #[cfg(feature = "a2a")]
3815 fn ephemeral_wrapper() -> AgentWrapper {
3816 let wrapper = AgentWrapper::new();
3817 wrapper.ephemeral(Some("ed25519")).unwrap();
3818 wrapper
3819 }
3820
3821 #[cfg(feature = "a2a")]
3822 #[test]
3823 fn test_export_agent_card_returns_valid_json() {
3824 let wrapper = ephemeral_wrapper();
3825 let card_json = wrapper.export_agent_card().unwrap();
3826 let card: Value = serde_json::from_str(&card_json).unwrap();
3827 assert!(card.get("name").is_some());
3828 assert!(card.get("protocolVersions").is_some());
3829 assert_eq!(card["protocolVersions"][0], "0.4.0");
3830 }
3831
3832 #[cfg(feature = "a2a")]
3833 #[test]
3834 #[allow(deprecated)]
3835 fn test_wrap_and_verify_a2a_artifact() {
3836 let wrapper = ephemeral_wrapper();
3837 let artifact = r#"{"content": "hello A2A"}"#;
3838
3839 let wrapped = wrapper
3840 .wrap_a2a_artifact(artifact, "message", None)
3841 .unwrap();
3842 let wrapped_value: Value = serde_json::from_str(&wrapped).unwrap();
3843 assert!(wrapped_value.get("jacsId").is_some());
3844 assert_eq!(wrapped_value["jacsType"], "a2a-message");
3845
3846 let result_json = wrapper.verify_a2a_artifact(&wrapped).unwrap();
3847 let result: Value = serde_json::from_str(&result_json).unwrap();
3848 assert_eq!(result["valid"], true);
3849 assert_eq!(result["status"], "SelfSigned");
3850 }
3851
3852 #[cfg(feature = "a2a")]
3853 #[test]
3854 fn test_sign_artifact_alias_matches_wrap() {
3855 let wrapper = ephemeral_wrapper();
3856 let artifact = r#"{"data": 42}"#;
3857
3858 let signed = wrapper.sign_artifact(artifact, "artifact", None).unwrap();
3859 let value: Value = serde_json::from_str(&signed).unwrap();
3860 assert_eq!(value["jacsType"], "a2a-artifact");
3861
3862 let result_json = wrapper.verify_a2a_artifact(&signed).unwrap();
3863 let result: Value = serde_json::from_str(&result_json).unwrap();
3864 assert_eq!(result["valid"], true);
3865 }
3866
3867 #[cfg(feature = "a2a")]
3868 #[test]
3869 #[allow(deprecated)]
3870 fn test_wrap_a2a_artifact_with_parent_chain() {
3871 let wrapper = ephemeral_wrapper();
3872
3873 let first = wrapper
3874 .wrap_a2a_artifact(r#"{"step": 1}"#, "task", None)
3875 .unwrap();
3876 let parents = format!("[{}]", first);
3877 let second = wrapper
3878 .wrap_a2a_artifact(r#"{"step": 2}"#, "task", Some(&parents))
3879 .unwrap();
3880
3881 let second_value: Value = serde_json::from_str(&second).unwrap();
3882 let parent_sigs = second_value["jacsParentSignatures"].as_array().unwrap();
3883 assert_eq!(parent_sigs.len(), 1);
3884 }
3885
3886 #[cfg(feature = "a2a")]
3887 #[test]
3888 #[allow(deprecated)]
3889 fn test_wrap_a2a_artifact_invalid_json_error() {
3890 let wrapper = ephemeral_wrapper();
3891 let result = wrapper.wrap_a2a_artifact("not json", "artifact", None);
3892 assert!(result.is_err());
3893 assert_eq!(result.unwrap_err().kind, ErrorKind::InvalidArgument);
3894 }
3895
3896 #[cfg(feature = "a2a")]
3897 #[test]
3898 fn test_verify_a2a_artifact_invalid_json_error() {
3899 let wrapper = ephemeral_wrapper();
3900 let result = wrapper.verify_a2a_artifact("not json");
3901 assert!(result.is_err());
3902 assert_eq!(result.unwrap_err().kind, ErrorKind::InvalidArgument);
3903 }
3904
3905 #[cfg(feature = "a2a")]
3906 #[test]
3907 fn test_export_agent_card_unloaded_agent_error() {
3908 let wrapper = AgentWrapper::new();
3909 let result = wrapper.export_agent_card();
3910 assert!(result.is_err());
3911 }
3912
3913 fn protocol_wrapper() -> AgentWrapper {
3919 let wrapper = AgentWrapper::new();
3920 wrapper.ephemeral(Some("ed25519")).unwrap();
3921 wrapper
3922 }
3923
3924 #[test]
3925 fn protocol_build_auth_header_starts_with_jacs() {
3926 let wrapper = protocol_wrapper();
3927 let header = wrapper
3928 .build_auth_header()
3929 .expect("build_auth_header failed");
3930 assert!(
3931 header.starts_with("JACS "),
3932 "Header must start with 'JACS ', got: {header}"
3933 );
3934 }
3935
3936 #[test]
3937 fn protocol_canonicalize_json_sorts_keys() {
3938 let wrapper = protocol_wrapper();
3939 let result = wrapper
3940 .canonicalize_json(r#"{"b":1,"a":2}"#)
3941 .expect("canonicalize_json failed");
3942 assert_eq!(result, r#"{"a":2,"b":1}"#);
3943 }
3944
3945 #[test]
3946 fn protocol_canonicalize_json_invalid_input() {
3947 let wrapper = protocol_wrapper();
3948 let result = wrapper.canonicalize_json("not json");
3949 assert!(result.is_err());
3950 assert_eq!(result.unwrap_err().kind, ErrorKind::SerializationFailed);
3951 }
3952
3953 #[test]
3954 fn protocol_sign_response_has_required_fields() {
3955 let wrapper = protocol_wrapper();
3956 let result = wrapper
3957 .sign_response(r#"{"answer": 42}"#)
3958 .expect("sign_response failed");
3959 let envelope: Value = serde_json::from_str(&result).expect("should be valid JSON");
3960 assert!(envelope.get("version").is_some(), "missing 'version'");
3961 assert!(
3962 envelope.get("jacsSignature").is_some(),
3963 "missing 'jacsSignature'"
3964 );
3965 assert_eq!(envelope["version"], "1.0.0");
3966 }
3967
3968 #[test]
3969 fn protocol_sign_response_invalid_payload() {
3970 let wrapper = protocol_wrapper();
3971 let result = wrapper.sign_response("not json");
3972 assert!(result.is_err());
3973 assert_eq!(result.unwrap_err().kind, ErrorKind::SerializationFailed);
3974 }
3975
3976 #[test]
3977 fn protocol_encode_verify_payload_round_trips() {
3978 let wrapper = protocol_wrapper();
3979 let original = r#"{"test":true}"#;
3980 let encoded = wrapper
3981 .encode_verify_payload(original)
3982 .expect("encode_verify_payload failed");
3983 assert!(!encoded.contains('+'), "URL-safe base64 must not contain +");
3984 assert!(!encoded.contains('/'), "URL-safe base64 must not contain /");
3985 assert!(
3986 !encoded.contains('='),
3987 "URL-safe base64 must not have padding"
3988 );
3989 let decoded = wrapper
3990 .decode_verify_payload(&encoded)
3991 .expect("decode_verify_payload failed");
3992 assert_eq!(decoded, original);
3993 }
3994
3995 #[test]
3996 fn protocol_extract_document_id_extracts_id() {
3997 let wrapper = protocol_wrapper();
3998 let id = wrapper
3999 .extract_document_id(r#"{"jacsDocumentId":"abc-123"}"#)
4000 .expect("extract_document_id failed");
4001 assert_eq!(id, "abc-123");
4002 }
4003
4004 #[test]
4005 fn protocol_extract_document_id_no_id_errors() {
4006 let wrapper = protocol_wrapper();
4007 let result = wrapper.extract_document_id(r#"{"name":"no-id"}"#);
4008 assert!(result.is_err());
4009 }
4010
4011 #[test]
4012 fn protocol_unwrap_signed_event_unknown_agent_unverified() {
4013 let wrapper = protocol_wrapper();
4014 let event = r#"{"data":{"result":"hello"},"jacsSignature":{"agentID":"unknown:v1","date":"2026-01-01T00:00:00Z","signature":"fakesig"}}"#;
4015 let keys = r#"{}"#;
4016 let result = wrapper
4017 .unwrap_signed_event(event, keys)
4018 .expect("unwrap_signed_event failed");
4019 let parsed: Value = serde_json::from_str(&result).expect("should be valid JSON");
4020 assert_eq!(parsed["verified"], false);
4021 assert_eq!(parsed["data"]["result"], "hello");
4022 }
4023
4024 #[test]
4025 fn protocol_unwrap_signed_event_legacy_payload() {
4026 let wrapper = protocol_wrapper();
4027 let event = r#"{"payload":{"status":"ok"}}"#;
4028 let keys = r#"{}"#;
4029 let result = wrapper
4030 .unwrap_signed_event(event, keys)
4031 .expect("unwrap_signed_event failed");
4032 let parsed: Value = serde_json::from_str(&result).expect("should be valid JSON");
4033 assert_eq!(parsed["verified"], false);
4034 assert_eq!(parsed["data"]["status"], "ok");
4035 }
4036
4037 #[test]
4038 fn protocol_unwrap_signed_event_plain_event() {
4039 let wrapper = protocol_wrapper();
4040 let event = r#"{"type":"heartbeat","ts":12345}"#;
4041 let keys = r#"{}"#;
4042 let result = wrapper
4043 .unwrap_signed_event(event, keys)
4044 .expect("unwrap_signed_event failed");
4045 let parsed: Value = serde_json::from_str(&result).expect("should be valid JSON");
4046 assert_eq!(parsed["verified"], false);
4047 assert_eq!(parsed["data"]["type"], "heartbeat");
4048 }
4049
4050 #[test]
4051 fn protocol_unwrap_signed_event_invalid_event_json() {
4052 let wrapper = protocol_wrapper();
4053 let result = wrapper.unwrap_signed_event("not json", "{}");
4054 assert!(result.is_err());
4055 assert_eq!(result.unwrap_err().kind, ErrorKind::SerializationFailed);
4056 }
4057
4058 #[test]
4059 fn protocol_unwrap_signed_event_invalid_keys_json() {
4060 let wrapper = protocol_wrapper();
4061 let result = wrapper.unwrap_signed_event(r#"{"type":"test"}"#, "not json");
4062 assert!(result.is_err());
4063 assert_eq!(result.unwrap_err().kind, ErrorKind::SerializationFailed);
4064 }
4065
4066 #[cfg(feature = "attestation")]
4071 mod attestation_tests {
4072 use super::*;
4073
4074 fn attestation_wrapper() -> AgentWrapper {
4075 let wrapper = AgentWrapper::new();
4076 wrapper.ephemeral(Some("ed25519")).unwrap();
4077 wrapper
4078 }
4079
4080 fn basic_attestation_params() -> String {
4081 json!({
4082 "subject": {
4083 "type": "artifact",
4084 "id": "test-artifact-001",
4085 "digests": { "sha256": "abc123" }
4086 },
4087 "claims": [{
4088 "name": "reviewed",
4089 "value": true,
4090 "confidence": 0.95,
4091 "assuranceLevel": "verified"
4092 }]
4093 })
4094 .to_string()
4095 }
4096
4097 #[test]
4098 fn binding_create_attestation_json() {
4099 let wrapper = attestation_wrapper();
4100 let result = wrapper.create_attestation(&basic_attestation_params());
4101 assert!(
4102 result.is_ok(),
4103 "create_attestation should succeed: {:?}",
4104 result.err()
4105 );
4106
4107 let json_str = result.unwrap();
4108 let doc: Value = serde_json::from_str(&json_str).unwrap();
4109 assert!(
4110 doc.get("attestation").is_some(),
4111 "returned JSON should contain 'attestation' key"
4112 );
4113 assert!(
4114 doc.get("jacsSignature").is_some(),
4115 "returned JSON should be signed"
4116 );
4117 }
4118
4119 #[test]
4120 fn binding_verify_attestation_json() {
4121 let wrapper = attestation_wrapper();
4122 let att_json = wrapper
4123 .create_attestation(&basic_attestation_params())
4124 .unwrap();
4125 let doc: Value = serde_json::from_str(&att_json).unwrap();
4126 let key = format!(
4127 "{}:{}",
4128 doc["jacsId"].as_str().unwrap(),
4129 doc["jacsVersion"].as_str().unwrap()
4130 );
4131
4132 let result = wrapper.verify_attestation(&key);
4133 assert!(
4134 result.is_ok(),
4135 "verify_attestation should succeed: {:?}",
4136 result.err()
4137 );
4138
4139 let result_json = result.unwrap();
4140 let result_value: Value = serde_json::from_str(&result_json).unwrap();
4141 assert_eq!(
4142 result_value["valid"], true,
4143 "attestation should verify as valid"
4144 );
4145 }
4146
4147 #[test]
4148 fn binding_verify_attestation_full_json() {
4149 let wrapper = attestation_wrapper();
4150 let att_json = wrapper
4151 .create_attestation(&basic_attestation_params())
4152 .unwrap();
4153 let doc: Value = serde_json::from_str(&att_json).unwrap();
4154 let key = format!(
4155 "{}:{}",
4156 doc["jacsId"].as_str().unwrap(),
4157 doc["jacsVersion"].as_str().unwrap()
4158 );
4159
4160 let result = wrapper.verify_attestation_full(&key);
4161 assert!(
4162 result.is_ok(),
4163 "verify_attestation_full should succeed: {:?}",
4164 result.err()
4165 );
4166
4167 let result_json = result.unwrap();
4168 let result_value: Value = serde_json::from_str(&result_json).unwrap();
4169 assert_eq!(
4170 result_value["valid"], true,
4171 "full attestation should verify as valid"
4172 );
4173 assert!(
4174 result_value.get("evidence").is_some(),
4175 "full verification result should contain 'evidence' array"
4176 );
4177 }
4178
4179 #[test]
4180 fn binding_lift_to_attestation_json() {
4181 let wrapper = attestation_wrapper();
4182
4183 let doc_json = json!({"title": "Test Document", "content": "Some content"}).to_string();
4185 let signed = wrapper
4186 .create_document(&doc_json, None, None, true, None, None)
4187 .unwrap();
4188
4189 let claims_json = json!([{
4190 "name": "reviewed",
4191 "value": true
4192 }])
4193 .to_string();
4194
4195 let result = wrapper.lift_to_attestation(&signed, &claims_json);
4196 assert!(
4197 result.is_ok(),
4198 "lift_to_attestation should succeed: {:?}",
4199 result.err()
4200 );
4201
4202 let att_json = result.unwrap();
4203 let doc: Value = serde_json::from_str(&att_json).unwrap();
4204 assert!(
4205 doc.get("attestation").is_some(),
4206 "lifted result should contain 'attestation' key"
4207 );
4208 assert!(
4209 doc.get("jacsSignature").is_some(),
4210 "lifted result should be signed"
4211 );
4212 }
4213
4214 #[test]
4215 fn binding_create_attestation_error_on_bad_json() {
4216 let wrapper = attestation_wrapper();
4217 let result = wrapper.create_attestation("not valid json {{{");
4218 assert!(result.is_err(), "bad JSON should error");
4219 assert_eq!(
4220 result.unwrap_err().kind,
4221 ErrorKind::SerializationFailed,
4222 "should be SerializationFailed error"
4223 );
4224 }
4225
4226 #[test]
4227 fn binding_create_attestation_error_on_missing_fields() {
4228 let wrapper = attestation_wrapper();
4229 let params = json!({
4231 "claims": [{"name": "test", "value": true}]
4232 })
4233 .to_string();
4234
4235 let result = wrapper.create_attestation(¶ms);
4236 assert!(result.is_err(), "missing subject should error");
4237 assert_eq!(
4238 result.unwrap_err().kind,
4239 ErrorKind::Validation,
4240 "should be Validation error"
4241 );
4242 }
4243
4244 #[test]
4245 fn binding_export_attestation_dsse() {
4246 let wrapper = attestation_wrapper();
4247 let att_json = wrapper
4248 .create_attestation(&basic_attestation_params())
4249 .unwrap();
4250
4251 let result = wrapper.export_attestation_dsse(&att_json);
4252 assert!(
4253 result.is_ok(),
4254 "export_attestation_dsse should succeed: {:?}",
4255 result.err()
4256 );
4257
4258 let dsse_json = result.unwrap();
4259 let envelope: Value = serde_json::from_str(&dsse_json).unwrap();
4260 assert_eq!(
4261 envelope["payloadType"].as_str().unwrap(),
4262 "application/vnd.in-toto+json"
4263 );
4264 }
4265 }
4266}