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::path::{Path, PathBuf};
28use std::str::FromStr;
29use std::sync::{Arc, Mutex, MutexGuard, PoisonError};
30
31pub mod conversion;
32pub mod doc_wrapper;
33pub mod simple_wrapper;
34
35pub use doc_wrapper::DocumentServiceWrapper;
36pub use simple_wrapper::SimpleAgentWrapper;
37pub use simple_wrapper::sign_message_json;
38pub use simple_wrapper::verify_json;
39
40#[derive(Debug)]
45pub struct BindingCoreError {
46 pub message: String,
47 pub kind: ErrorKind,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum ErrorKind {
53 LockFailed,
55 AgentLoad,
57 Validation,
59 SigningFailed,
61 VerificationFailed,
63 DocumentFailed,
65 AgreementFailed,
67 SerializationFailed,
69 InvalidArgument,
71 TrustFailed,
73 NetworkFailed,
75 KeyNotFound,
77 Generic,
79}
80
81impl BindingCoreError {
82 pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
83 Self {
84 message: message.into(),
85 kind,
86 }
87 }
88
89 pub fn lock_failed(message: impl Into<String>) -> Self {
90 Self::new(ErrorKind::LockFailed, message)
91 }
92
93 pub fn agent_load(message: impl Into<String>) -> Self {
94 Self::new(ErrorKind::AgentLoad, message)
95 }
96
97 pub fn validation(message: impl Into<String>) -> Self {
98 Self::new(ErrorKind::Validation, message)
99 }
100
101 pub fn signing_failed(message: impl Into<String>) -> Self {
102 Self::new(ErrorKind::SigningFailed, message)
103 }
104
105 pub fn verification_failed(message: impl Into<String>) -> Self {
106 Self::new(ErrorKind::VerificationFailed, message)
107 }
108
109 pub fn document_failed(message: impl Into<String>) -> Self {
110 Self::new(ErrorKind::DocumentFailed, message)
111 }
112
113 pub fn agreement_failed(message: impl Into<String>) -> Self {
114 Self::new(ErrorKind::AgreementFailed, message)
115 }
116
117 pub fn serialization_failed(message: impl Into<String>) -> Self {
118 Self::new(ErrorKind::SerializationFailed, message)
119 }
120
121 pub fn invalid_argument(message: impl Into<String>) -> Self {
122 Self::new(ErrorKind::InvalidArgument, message)
123 }
124
125 pub fn trust_failed(message: impl Into<String>) -> Self {
126 Self::new(ErrorKind::TrustFailed, message)
127 }
128
129 pub fn network_failed(message: impl Into<String>) -> Self {
130 Self::new(ErrorKind::NetworkFailed, message)
131 }
132
133 pub fn key_not_found(message: impl Into<String>) -> Self {
134 Self::new(ErrorKind::KeyNotFound, message)
135 }
136
137 pub fn generic(message: impl Into<String>) -> Self {
138 Self::new(ErrorKind::Generic, message)
139 }
140}
141
142impl std::fmt::Display for BindingCoreError {
143 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144 write!(f, "{}", self.message)
145 }
146}
147
148impl std::error::Error for BindingCoreError {}
149
150impl<T> From<PoisonError<T>> for BindingCoreError {
151 fn from(e: PoisonError<T>) -> Self {
152 Self::lock_failed(format!("Failed to acquire lock: {}", e))
153 }
154}
155
156pub type BindingResult<T> = Result<T, BindingCoreError>;
158
159fn serialize_agent_info(info: &jacs::simple::AgentInfo) -> BindingResult<String> {
160 serde_json::to_string(info).map_err(|e| {
161 BindingCoreError::serialization_failed(format!("Failed to serialize AgentInfo: {}", e))
162 })
163}
164
165fn resolve_existing_config_path(config_path: &str) -> BindingResult<String> {
166 let requested = Path::new(config_path);
167 let resolved = if requested.is_absolute() {
168 requested.to_path_buf()
169 } else {
170 std::env::current_dir()
171 .map_err(|e| {
172 BindingCoreError::agent_load(format!(
173 "Failed to determine current working directory: {}",
174 e
175 ))
176 })?
177 .join(requested)
178 };
179
180 if !resolved.exists() {
181 return Err(BindingCoreError::agent_load(format!(
182 "Config file not found: {}",
183 resolved.display()
184 )));
185 }
186
187 Ok(normalize_path(&resolved).to_string_lossy().into_owned())
188}
189
190fn normalize_path(path: &Path) -> PathBuf {
191 let mut normalized = PathBuf::new();
192 for component in path.components() {
193 match component {
194 std::path::Component::CurDir => {}
195 std::path::Component::ParentDir => {
196 normalized.pop();
197 }
198 other => normalized.push(other.as_os_str()),
199 }
200 }
201 normalized
202}
203
204fn resolve_path_from_cwd(path: &str) -> BindingResult<PathBuf> {
205 let requested = Path::new(path);
206 if requested.is_absolute() {
207 return Ok(normalize_path(requested));
208 }
209
210 Ok(normalize_path(
211 &std::env::current_dir()
212 .map_err(|e| {
213 BindingCoreError::agent_load(format!(
214 "Failed to determine current working directory: {}",
215 e
216 ))
217 })?
218 .join(requested),
219 ))
220}
221
222fn resolve_relative_to_config(config_path: &Path, candidate: &str) -> PathBuf {
223 let candidate_path = Path::new(candidate);
224 if candidate_path.is_absolute() {
225 return normalize_path(candidate_path);
226 }
227
228 let base_dir = config_path.parent().unwrap_or_else(|| Path::new("."));
229 normalize_path(&base_dir.join(candidate_path))
230}
231
232fn read_password_file(path: &Path) -> BindingResult<Option<String>> {
233 if !path.exists() {
234 return Ok(None);
235 }
236
237 let contents = fs::read_to_string(path).map_err(|e| {
238 BindingCoreError::generic(format!(
239 "Failed to read password file {}: {}",
240 path.display(),
241 e
242 ))
243 })?;
244 let password = contents.trim_end_matches(|c| c == '\n' || c == '\r').trim();
245 if password.is_empty() {
246 return Ok(None);
247 }
248 Ok(Some(password.to_string()))
249}
250
251fn missing_password_message(error: &str) -> bool {
252 error.contains("No private key password available")
253}
254
255fn truthy_env_var(name: &str) -> bool {
256 std::env::var(name)
257 .ok()
258 .map(|value| {
259 let value = value.trim();
260 value.eq_ignore_ascii_case("true") || value == "1"
261 })
262 .unwrap_or(false)
263}
264
265const DEFAULT_NETWORK_TIMEOUT_MS: u64 = 10_000;
266const DEFAULT_KEYS_BASE_URL: &str = "https://hai.ai";
267
268fn build_blocking_json_client(timeout_ms: u64) -> BindingResult<BlockingClient> {
269 BlockingClient::builder()
270 .timeout(std::time::Duration::from_millis(timeout_ms.max(1)))
271 .build()
272 .map_err(|e| {
273 BindingCoreError::network_failed(format!("Failed to build HTTP client: {}", e))
274 })
275}
276
277fn is_loopback_host(host: &str) -> bool {
278 matches!(
279 host.trim().trim_matches(['[', ']']),
280 "localhost" | "127.0.0.1" | "::1"
281 )
282}
283
284fn validate_network_url(url: &Url, description: &str) -> BindingResult<()> {
285 match url.scheme() {
286 "https" => Ok(()),
287 "http" if url.host_str().is_some_and(is_loopback_host) => Ok(()),
288 "http" => Err(BindingCoreError::network_failed(format!(
289 "{} must use HTTPS (got '{}'). Only localhost URLs are allowed over HTTP for testing.",
290 description, url
291 ))),
292 other => Err(BindingCoreError::invalid_argument(format!(
293 "{} must use http or https (got scheme '{}')",
294 description, other
295 ))),
296 }
297}
298
299fn content_type_header(response: &reqwest::blocking::Response) -> String {
300 response
301 .headers()
302 .get(CONTENT_TYPE)
303 .and_then(|value| value.to_str().ok())
304 .unwrap_or("")
305 .to_string()
306}
307
308fn parse_json_object_body(
309 body: &str,
310 invalid_json_message: String,
311 non_object_message: String,
312) -> BindingResult<String> {
313 let value: Value = serde_json::from_str(body)
314 .map_err(|e| BindingCoreError::validation(format!("{}: {}", invalid_json_message, e)))?;
315 if !value.is_object() {
316 return Err(BindingCoreError::validation(non_object_message));
317 }
318 serde_json::to_string(&value).map_err(|e| {
319 BindingCoreError::serialization_failed(format!("Failed to serialize JSON response: {}", e))
320 })
321}
322
323fn resolve_keys_base_url(override_base_url: Option<&str>) -> String {
324 if let Some(value) = override_base_url {
325 let trimmed = value.trim();
326 if !trimmed.is_empty() {
327 return trimmed.trim_end_matches('/').to_string();
328 }
329 }
330
331 if let Ok(value) = std::env::var("JACS_KEYS_BASE_URL") {
332 let trimmed = value.trim();
333 if !trimmed.is_empty() {
334 return trimmed.trim_end_matches('/').to_string();
335 }
336 }
337
338 if let Ok(value) = std::env::var("HAI_KEYS_BASE_URL") {
339 let trimmed = value.trim();
340 if !trimmed.is_empty() {
341 return trimmed.trim_end_matches('/').to_string();
342 }
343 }
344 DEFAULT_KEYS_BASE_URL.to_string()
345}
346
347fn normalize_public_key_hash(public_key_hash: &str) -> BindingResult<String> {
348 let trimmed = public_key_hash.trim();
349 if trimmed.is_empty() {
350 return Err(BindingCoreError::invalid_argument(
351 "public_key_hash cannot be empty",
352 ));
353 }
354 if trimmed.starts_with("sha256:") {
355 Ok(trimmed.to_string())
356 } else {
357 Ok(format!("sha256:{}", trimmed))
358 }
359}
360
361fn decode_public_key_base64(public_key_b64: &str) -> BindingResult<Vec<u8>> {
362 for engine in [
363 &general_purpose::STANDARD,
364 &general_purpose::STANDARD_NO_PAD,
365 &general_purpose::URL_SAFE,
366 &general_purpose::URL_SAFE_NO_PAD,
367 ] {
368 if let Ok(decoded) = engine.decode(public_key_b64) {
369 return Ok(decoded);
370 }
371 }
372
373 Err(BindingCoreError::invalid_argument(
374 "Public key must be valid base64 or base64url text.",
375 ))
376}
377
378fn build_jwk_set_from_public_key_bytes(
379 public_key: &[u8],
380 key_algorithm: &str,
381 key_id: &str,
382) -> BindingResult<Value> {
383 let normalized_algorithm = key_algorithm.trim().to_ascii_lowercase();
384
385 if normalized_algorithm.contains("ed25519")
386 || (normalized_algorithm.is_empty() && public_key.len() == 32)
387 {
388 if public_key.len() != 32 {
389 return Err(BindingCoreError::invalid_argument(format!(
390 "Ed25519 public key must be 32 bytes, got {} bytes.",
391 public_key.len()
392 )));
393 }
394
395 return Ok(json!({
396 "keys": [{
397 "kty": "OKP",
398 "crv": "Ed25519",
399 "x": general_purpose::URL_SAFE_NO_PAD.encode(public_key),
400 "kid": key_id,
401 "use": "sig",
402 "alg": "EdDSA",
403 }]
404 }));
405 }
406
407 if normalized_algorithm.contains("rsa") || normalized_algorithm.is_empty() {
408 use rsa::traits::PublicKeyParts;
409 use rsa::{RsaPublicKey, pkcs1::DecodeRsaPublicKey, pkcs8::DecodePublicKey};
410
411 let rsa_key = if let Ok(key) = RsaPublicKey::from_pkcs1_der(public_key) {
412 key
413 } else if let Ok(key) = RsaPublicKey::from_public_key_der(public_key) {
414 key
415 } else if let Ok(pem) = std::str::from_utf8(public_key) {
416 match RsaPublicKey::from_public_key_pem(pem) {
417 Ok(key) => key,
418 Err(e) if normalized_algorithm.contains("rsa") => {
419 return Err(BindingCoreError::invalid_argument(format!(
420 "Failed to parse RSA public key for JWK export: {}",
421 e
422 )));
423 }
424 Err(_) => return Ok(json!({ "keys": [] })),
425 }
426 } else if normalized_algorithm.contains("rsa") {
427 return Err(BindingCoreError::invalid_argument(
428 "Failed to parse RSA public key for JWK export.",
429 ));
430 } else {
431 return Ok(json!({ "keys": [] }));
432 };
433
434 return Ok(json!({
435 "keys": [{
436 "kty": "RSA",
437 "kid": key_id,
438 "alg": "RS256",
439 "use": "sig",
440 "n": general_purpose::URL_SAFE_NO_PAD.encode(rsa_key.n().to_bytes_be()),
441 "e": general_purpose::URL_SAFE_NO_PAD.encode(rsa_key.e().to_bytes_be()),
442 }]
443 }));
444 }
445
446 Ok(json!({ "keys": [] }))
447}
448
449fn resolve_password_context(
450 config_path: Option<&str>,
451 key_directory: Option<&str>,
452) -> BindingResult<(PathBuf, Option<String>)> {
453 let mut agent_id = None;
454
455 if let Some(config_path) = config_path {
456 let resolved_config_path = resolve_path_from_cwd(config_path)?;
457 if resolved_config_path.exists() {
458 let config = Config::from_file(resolved_config_path.to_string_lossy().as_ref())
459 .map_err(|e| {
460 BindingCoreError::agent_load(format!(
461 "Failed to load config from {}: {}",
462 resolved_config_path.display(),
463 e
464 ))
465 })?;
466 let configured_key_dir = config
467 .jacs_key_directory()
468 .as_deref()
469 .unwrap_or("./jacs_keys");
470 let resolved_key_dir =
471 resolve_relative_to_config(&resolved_config_path, configured_key_dir);
472 agent_id = config.jacs_agent_id_and_version().clone();
473
474 if let Some(key_directory) = key_directory {
475 return Ok((resolve_path_from_cwd(key_directory)?, agent_id));
476 }
477 return Ok((resolved_key_dir, agent_id));
478 }
479
480 if let Some(key_directory) = key_directory {
481 return Ok((resolve_path_from_cwd(key_directory)?, None));
482 }
483
484 return Ok((
485 resolve_relative_to_config(&resolved_config_path, "./jacs_keys"),
486 None,
487 ));
488 }
489
490 if let Some(key_directory) = key_directory {
491 return Ok((resolve_path_from_cwd(key_directory)?, None));
492 }
493
494 Ok((resolve_path_from_cwd("./jacs_keys")?, agent_id))
495}
496
497fn generate_private_key_password_value() -> String {
498 use rand::Rng;
499
500 const UPPER: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ";
501 const LOWER: &[u8] = b"abcdefghijklmnopqrstuvwxyz";
502 const DIGITS: &[u8] = b"0123456789";
503 const SPECIAL: &[u8] = b"!@#$%^&*()-_=+";
504 const ALL: &[u8] =
505 b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()-_=+";
506
507 let mut rng = rand::rng();
508 let mut password = String::with_capacity(32);
509
510 password.push(UPPER[rng.random_range(0..UPPER.len())] as char);
511 password.push(LOWER[rng.random_range(0..LOWER.len())] as char);
512 password.push(DIGITS[rng.random_range(0..DIGITS.len())] as char);
513 password.push(SPECIAL[rng.random_range(0..SPECIAL.len())] as char);
514
515 for _ in 4..32 {
516 password.push(ALL[rng.random_range(0..ALL.len())] as char);
517 }
518
519 password
520}
521
522fn persist_password_file(key_directory: &Path, password: &str) -> BindingResult<()> {
523 fs::create_dir_all(key_directory).map_err(|e| {
524 BindingCoreError::generic(format!(
525 "Failed to create key directory {}: {}",
526 key_directory.display(),
527 e
528 ))
529 })?;
530
531 let password_path = key_directory.join(".jacs_password");
532 fs::write(&password_path, password).map_err(|e| {
533 BindingCoreError::generic(format!(
534 "Failed to write password file {}: {}",
535 password_path.display(),
536 e
537 ))
538 })?;
539
540 #[cfg(unix)]
541 {
542 use std::os::unix::fs::PermissionsExt;
543
544 let permissions = std::fs::Permissions::from_mode(0o600);
545 fs::set_permissions(&password_path, permissions).map_err(|e| {
546 BindingCoreError::generic(format!(
547 "Failed to set password file permissions on {}: {}",
548 password_path.display(),
549 e
550 ))
551 })?;
552 }
553
554 Ok(())
555}
556
557fn is_editable_level(level: &str) -> bool {
558 matches!(level, "artifact" | "config")
559}
560
561fn normalize_agent_id_for_compare(agent_id: &str) -> &str {
562 agent_id.split(':').next().unwrap_or(agent_id)
563}
564
565fn extract_agreement_payload(value: &Value) -> Value {
566 if let Some(payload) = value.get("jacsDocument") {
567 return payload.clone();
568 }
569 if let Some(payload) = value.get("content") {
570 return payload.clone();
571 }
572 if let Some(obj) = value.as_object() {
573 let mut filtered = serde_json::Map::new();
574 for (k, v) in obj {
575 if !k.starts_with("jacs") && k != "$schema" {
576 filtered.insert(k.clone(), v.clone());
577 }
578 }
579 if !filtered.is_empty() {
580 return Value::Object(filtered);
581 }
582 }
583 Value::Null
584}
585
586fn create_editable_agreement_document(
587 agent: &mut Agent,
588 payload: Value,
589) -> BindingResult<JACSDocument> {
590 let wrapped = json!({
591 "jacsType": "artifact",
592 "jacsLevel": "artifact",
593 "content": payload
594 });
595 agent
596 .create_document_and_load(&wrapped.to_string(), None, None)
597 .map_err(|e| {
598 BindingCoreError::document_failed(format!(
599 "Failed to create editable agreement document: {}",
600 e
601 ))
602 })
603}
604
605fn ensure_editable_agreement_document(
606 agent: &mut Agent,
607 document_string: &str,
608) -> BindingResult<JACSDocument> {
609 match agent.load_document(document_string) {
610 Ok(doc) => {
611 let level = doc
612 .value
613 .get("jacsLevel")
614 .and_then(|v| v.as_str())
615 .unwrap_or("");
616 if is_editable_level(level) {
617 Ok(doc)
618 } else {
619 let payload = extract_agreement_payload(doc.getvalue());
620 create_editable_agreement_document(agent, payload)
621 }
622 }
623 Err(load_err) => {
624 if let Ok(parsed) = serde_json::from_str::<Value>(document_string)
625 && (parsed.get("jacsId").is_some() || parsed.get("jacsVersion").is_some())
626 {
627 return Err(BindingCoreError::document_failed(format!(
628 "Failed to load document: {}",
629 load_err
630 )));
631 }
632 let payload = serde_json::from_str::<Value>(document_string)
633 .unwrap_or_else(|_| Value::String(document_string.to_string()));
634 create_editable_agreement_document(agent, payload)
635 }
636 }
637}
638
639#[derive(Clone)]
648pub struct AgentWrapper {
649 inner: Arc<Mutex<Agent>>,
650 private_key_password: Arc<Mutex<Option<String>>>,
651}
652
653impl Default for AgentWrapper {
658 fn default() -> Self {
659 Self::new()
660 }
661}
662
663impl AgentWrapper {
664 pub fn new() -> Self {
666 Self {
667 inner: Arc::new(Mutex::new(jacs::get_empty_agent())),
668 private_key_password: Arc::new(Mutex::new(None)),
669 }
670 }
671
672 pub fn from_inner(inner: Arc<Mutex<Agent>>) -> Self {
677 Self {
678 inner,
679 private_key_password: Arc::new(Mutex::new(None)),
680 }
681 }
682
683 pub fn inner_arc(&self) -> Arc<Mutex<Agent>> {
688 Arc::clone(&self.inner)
689 }
690
691 fn lock(&self) -> BindingResult<MutexGuard<'_, Agent>> {
693 self.inner.lock().map_err(BindingCoreError::from)
694 }
695
696 fn configured_private_key_password(&self) -> BindingResult<Option<String>> {
697 self.private_key_password
698 .lock()
699 .map_err(BindingCoreError::from)
700 .map(|password| password.clone())
701 }
702
703 fn with_private_key_password<T>(
704 &self,
705 operation: impl FnOnce() -> BindingResult<T>,
706 ) -> BindingResult<T> {
707 {
712 let password = self.configured_private_key_password()?;
713 let mut agent = self.lock()?;
714 agent.set_password(password);
715 }
716 operation()
717 }
718
719 pub fn set_private_key_password(&self, password: Option<String>) -> BindingResult<()> {
725 let mut slot = self
726 .private_key_password
727 .lock()
728 .map_err(BindingCoreError::from)?;
729 *slot = password.and_then(|value| if value.is_empty() { None } else { Some(value) });
730 Ok(())
731 }
732
733 pub fn load(&self, config_path: String) -> BindingResult<String> {
738 let password = self.configured_private_key_password()?;
739 let new_agent = self.load_agent_from_config(&config_path, true, password.as_deref())?;
740 *self.lock()? = new_agent;
741 Ok("Agent loaded".to_string())
742 }
743
744 pub fn load_file_only(&self, config_path: String) -> BindingResult<String> {
749 let new_agent = self.load_agent_from_config(&config_path, false, None)?;
750 *self.lock()? = new_agent;
751 Ok("Agent loaded (file-only)".to_string())
752 }
753
754 pub fn load_with_info(&self, config_path: String) -> BindingResult<String> {
756 let resolved_config_path = resolve_existing_config_path(&config_path)?;
757 let password = self.configured_private_key_password()?;
758 let new_agent =
759 self.load_agent_from_config(&resolved_config_path, true, password.as_deref())?;
760 let info = jacs::simple::build_loaded_agent_info(&new_agent, &resolved_config_path)
761 .map_err(|e| BindingCoreError::agent_load(format!("Failed to load agent: {}", e)))?;
762 *self.lock()? = new_agent;
763 serialize_agent_info(&info)
764 }
765
766 fn load_agent_from_config(
771 &self,
772 config_path: &str,
773 apply_env: bool,
774 password: Option<&str>,
775 ) -> BindingResult<Agent> {
776 let mut config = Config::from_file(config_path)
777 .map_err(|e| BindingCoreError::agent_load(format!("Failed to load config: {}", e)))?;
778 if apply_env {
779 config.apply_env_overrides();
780 }
781 Agent::from_config(config, password)
782 .map_err(|e| BindingCoreError::agent_load(format!("Failed to load agent: {}", e)))
783 }
784
785 pub fn set_storage_root(&self, root: std::path::PathBuf) -> BindingResult<()> {
792 let mut agent = self.lock()?;
793 agent
794 .set_storage_root(root)
795 .map_err(|e| BindingCoreError::generic(format!("Failed to set storage root: {}", e)))?;
796 Ok(())
797 }
798
799 pub fn sign_agent(
801 &self,
802 agent_string: &str,
803 public_key: Vec<u8>,
804 public_key_enc_type: String,
805 ) -> BindingResult<String> {
806 self.with_private_key_password(|| {
807 let mut agent = self.lock()?;
808
809 let mut external_agent: Value = agent.validate_agent(agent_string).map_err(|e| {
810 BindingCoreError::validation(format!("Agent validation failed: {}", e))
811 })?;
812
813 agent
814 .signature_verification_procedure(
815 &external_agent,
816 None,
817 &AGENT_SIGNATURE_FIELDNAME.to_string(),
818 public_key,
819 Some(public_key_enc_type),
820 None,
821 None,
822 )
823 .map_err(|e| {
824 BindingCoreError::verification_failed(format!(
825 "Signature verification failed: {}",
826 e
827 ))
828 })?;
829
830 let registration_signature = agent
831 .signing_procedure(
832 &external_agent,
833 None,
834 &AGENT_REGISTRATION_SIGNATURE_FIELDNAME.to_string(),
835 )
836 .map_err(|e| {
837 BindingCoreError::signing_failed(format!("Signing procedure failed: {}", e))
838 })?;
839
840 external_agent[AGENT_REGISTRATION_SIGNATURE_FIELDNAME] = registration_signature;
841 Ok(external_agent.to_string())
842 })
843 }
844
845 pub fn verify_string(
847 &self,
848 data: &str,
849 signature_base64: &str,
850 public_key: Vec<u8>,
851 public_key_enc_type: String,
852 ) -> BindingResult<bool> {
853 let agent = self.lock()?;
854
855 if data.is_empty()
856 || signature_base64.is_empty()
857 || public_key.is_empty()
858 || public_key_enc_type.is_empty()
859 {
860 return Err(BindingCoreError::invalid_argument(format!(
861 "One parameter is empty: data={}, signature_base64={}, public_key_enc_type={}",
862 data.is_empty(),
863 signature_base64.is_empty(),
864 public_key_enc_type
865 )));
866 }
867
868 agent
869 .verify_string(
870 &data.to_string(),
871 &signature_base64.to_string(),
872 public_key,
873 Some(public_key_enc_type),
874 )
875 .map_err(|e| {
876 BindingCoreError::verification_failed(format!(
877 "Signature verification failed: {}",
878 e
879 ))
880 })?;
881
882 Ok(true)
883 }
884
885 pub fn sign_string(&self, data: &str) -> BindingResult<String> {
887 self.with_private_key_password(|| {
888 let mut agent = self.lock()?;
889 agent.sign_string(&data.to_string()).map_err(|e| {
890 BindingCoreError::signing_failed(format!("Failed to sign string: {}", e))
891 })
892 })
893 }
894
895 pub fn sign_batch(&self, messages: Vec<String>) -> BindingResult<Vec<String>> {
897 self.with_private_key_password(|| {
898 let mut agent = self.lock()?;
899 let refs: Vec<&str> = messages.iter().map(|s| s.as_str()).collect();
900 agent
901 .sign_batch(&refs)
902 .map_err(|e| BindingCoreError::signing_failed(format!("Batch sign failed: {}", e)))
903 })
904 }
905
906 pub fn verify_agent(&self, agentfile: Option<String>) -> BindingResult<bool> {
908 self.with_private_key_password(|| {
909 let mut agent = self.lock()?;
910
911 if let Some(file) = agentfile {
912 let loaded_agent = jacs::load_agent(Some(file)).map_err(|e| {
913 BindingCoreError::agent_load(format!("Failed to load agent: {}", e))
914 })?;
915 *agent = loaded_agent;
916 }
917
918 agent.verify_self_signature().map_err(|e| {
919 BindingCoreError::verification_failed(format!(
920 "Failed to verify agent signature: {}",
921 e
922 ))
923 })?;
924
925 agent.verify_self_hash().map_err(|e| {
926 BindingCoreError::verification_failed(format!("Failed to verify agent hash: {}", e))
927 })?;
928
929 Ok(true)
930 })
931 }
932
933 pub fn update_agent(&self, new_agent_string: &str) -> BindingResult<String> {
935 self.with_private_key_password(|| {
936 let mut agent = self.lock()?;
937 agent
938 .update_self(new_agent_string)
939 .map_err(|e| BindingCoreError::agent_load(format!("Failed to update agent: {}", e)))
940 })
941 }
942
943 pub fn verify_document(&self, document_string: &str) -> BindingResult<bool> {
945 let mut agent = self.lock()?;
946
947 let doc = agent.load_document(document_string).map_err(|e| {
948 BindingCoreError::document_failed(format!("Failed to load document: {}", e))
949 })?;
950
951 let document_key = doc.getkey();
952 let value = doc.getvalue();
953
954 agent.verify_hash(value).map_err(|e| {
955 BindingCoreError::verification_failed(format!("Failed to verify document hash: {}", e))
956 })?;
957
958 if agent
962 .verify_document_signature(&document_key, None, None, None, None)
963 .is_err()
964 {
965 agent
966 .verify_external_document_signature(&document_key)
967 .map_err(|e| {
968 BindingCoreError::verification_failed(format!(
969 "Failed to verify document signature: {}",
970 e
971 ))
972 })?;
973 }
974
975 Ok(true)
976 }
977
978 pub fn update_document(
980 &self,
981 document_key: &str,
982 new_document_string: &str,
983 attachments: Option<Vec<String>>,
984 embed: Option<bool>,
985 ) -> BindingResult<String> {
986 self.with_private_key_password(|| {
987 let mut agent = self.lock()?;
988
989 let doc = agent
990 .update_document(document_key, new_document_string, attachments, embed)
991 .map_err(|e| {
992 BindingCoreError::document_failed(format!("Failed to update document: {}", e))
993 })?;
994
995 Ok(doc.to_string())
996 })
997 }
998
999 pub fn verify_signature(
1001 &self,
1002 document_string: &str,
1003 signature_field: Option<String>,
1004 ) -> BindingResult<bool> {
1005 let mut agent = self.lock()?;
1006
1007 let doc = agent.load_document(document_string).map_err(|e| {
1008 BindingCoreError::document_failed(format!("Failed to load document: {}", e))
1009 })?;
1010
1011 let document_key = doc.getkey();
1012 let sig_field_ref = signature_field.as_ref();
1013
1014 agent
1015 .verify_document_signature(
1016 &document_key,
1017 sig_field_ref.map(|s| s.as_str()),
1018 None,
1019 None,
1020 None,
1021 )
1022 .map_err(|e| {
1023 BindingCoreError::verification_failed(format!("Failed to verify signature: {}", e))
1024 })?;
1025
1026 Ok(true)
1027 }
1028
1029 pub fn create_agreement(
1031 &self,
1032 document_string: &str,
1033 agentids: Vec<String>,
1034 question: Option<String>,
1035 context: Option<String>,
1036 agreement_fieldname: Option<String>,
1037 ) -> BindingResult<String> {
1038 self.create_agreement_with_options(
1039 document_string,
1040 agentids,
1041 question,
1042 context,
1043 agreement_fieldname,
1044 None,
1045 None,
1046 None,
1047 None,
1048 )
1049 }
1050
1051 pub fn create_agreement_with_options(
1059 &self,
1060 document_string: &str,
1061 agentids: Vec<String>,
1062 question: Option<String>,
1063 context: Option<String>,
1064 agreement_fieldname: Option<String>,
1065 timeout: Option<String>,
1066 quorum: Option<u32>,
1067 required_algorithms: Option<Vec<String>>,
1068 minimum_strength: Option<String>,
1069 ) -> BindingResult<String> {
1070 use jacs::agent::agreement::{Agreement, AgreementOptions};
1071
1072 self.with_private_key_password(|| {
1073 let mut agent = self.lock()?;
1074 let base_doc = ensure_editable_agreement_document(&mut agent, document_string)?;
1075 let document_key = base_doc.getkey();
1076
1077 let options = AgreementOptions {
1078 timeout,
1079 quorum,
1080 required_algorithms,
1081 minimum_strength,
1082 };
1083
1084 let agreement_doc = agent
1085 .create_agreement_with_options(
1086 &document_key,
1087 agentids.as_slice(),
1088 question.as_deref(),
1089 context.as_deref(),
1090 agreement_fieldname,
1091 &options,
1092 )
1093 .map_err(|e| {
1094 BindingCoreError::agreement_failed(format!("Failed to create agreement: {}", e))
1095 })?;
1096
1097 Ok(agreement_doc.value.to_string())
1098 })
1099 }
1100
1101 pub fn sign_agreement(
1103 &self,
1104 document_string: &str,
1105 agreement_fieldname: Option<String>,
1106 ) -> BindingResult<String> {
1107 self.with_private_key_password(|| {
1108 let mut agent = self.lock()?;
1109 let doc = agent.load_document(document_string).map_err(|e| {
1110 BindingCoreError::document_failed(format!("Failed to load document: {}", e))
1111 })?;
1112 let document_key = doc.getkey();
1113 let signed_doc = agent
1114 .sign_agreement(&document_key, agreement_fieldname)
1115 .map_err(|e| {
1116 BindingCoreError::agreement_failed(format!("Failed to sign agreement: {}", e))
1117 })?;
1118
1119 Ok(signed_doc.value.to_string())
1120 })
1121 }
1122
1123 pub fn create_document(
1125 &self,
1126 document_string: &str,
1127 custom_schema: Option<String>,
1128 outputfilename: Option<String>,
1129 no_save: bool,
1130 attachments: Option<&str>,
1131 embed: Option<bool>,
1132 ) -> BindingResult<String> {
1133 self.with_private_key_password(|| {
1134 let mut agent = self.lock()?;
1135
1136 jacs::shared::document_create(
1137 &mut agent,
1138 document_string,
1139 custom_schema,
1140 outputfilename,
1141 no_save,
1142 attachments,
1143 embed,
1144 )
1145 .map_err(|e| {
1146 BindingCoreError::document_failed(format!("Failed to create document: {}", e))
1147 })
1148 })
1149 }
1150
1151 pub fn save_signed_document(
1157 &self,
1158 document_string: &str,
1159 outputfilename: Option<String>,
1160 export_embedded: Option<bool>,
1161 extract_only: Option<bool>,
1162 ) -> BindingResult<String> {
1163 let mut agent = self.lock()?;
1164 let doc = agent.load_document(document_string).map_err(|e| {
1165 BindingCoreError::document_failed(format!("Failed to load signed document: {}", e))
1166 })?;
1167 let document_key = doc.getkey();
1168 agent
1169 .save_document(&document_key, outputfilename, export_embedded, extract_only)
1170 .map_err(|e| {
1171 BindingCoreError::document_failed(format!(
1172 "Failed to persist signed document '{}': {}",
1173 document_key, e
1174 ))
1175 })?;
1176
1177 Ok(document_key)
1178 }
1179
1180 pub fn list_document_keys(&self) -> BindingResult<Vec<String>> {
1182 let mut agent = self.lock()?;
1183 Ok(agent.get_document_keys())
1184 }
1185
1186 pub fn check_agreement(
1188 &self,
1189 document_string: &str,
1190 agreement_fieldname: Option<String>,
1191 ) -> BindingResult<String> {
1192 let mut agent = self.lock()?;
1193 let doc = agent.load_document(document_string).map_err(|e| {
1194 BindingCoreError::document_failed(format!("Failed to load document: {}", e))
1195 })?;
1196 let document_key = doc.getkey();
1197 let agreement_fieldname_key = agreement_fieldname
1198 .clone()
1199 .unwrap_or_else(|| AGENT_AGREEMENT_FIELDNAME.to_string());
1200
1201 agent
1202 .check_agreement(&document_key, Some(agreement_fieldname_key.clone()))
1203 .map_err(|e| {
1204 BindingCoreError::agreement_failed(format!("Failed to check agreement: {}", e))
1205 })?;
1206
1207 let requested = doc
1208 .agreement_requested_agents(Some(agreement_fieldname_key.clone()))
1209 .map_err(|e| {
1210 BindingCoreError::agreement_failed(format!(
1211 "Failed to read requested signers: {}",
1212 e
1213 ))
1214 })?;
1215
1216 let pending = doc
1217 .agreement_unsigned_agents(Some(agreement_fieldname_key.clone()))
1218 .map_err(|e| {
1219 BindingCoreError::agreement_failed(format!("Failed to read pending signers: {}", e))
1220 })?;
1221
1222 let signatures = doc
1223 .value
1224 .get(&agreement_fieldname_key)
1225 .and_then(|agreement| agreement.get("signatures"))
1226 .and_then(|v| v.as_array())
1227 .cloned()
1228 .unwrap_or_default();
1229
1230 let mut signed_at_by_agent: HashMap<String, String> = HashMap::new();
1231 for signature in signatures {
1232 if let Some(agent_id) = signature.get("agentID").and_then(|v| v.as_str()) {
1233 let normalized = normalize_agent_id_for_compare(agent_id).to_string();
1234 let signed_at = signature
1235 .get("date")
1236 .and_then(|v| v.as_str())
1237 .unwrap_or("")
1238 .to_string();
1239 signed_at_by_agent.insert(normalized, signed_at);
1240 }
1241 }
1242
1243 let signers = requested
1244 .iter()
1245 .map(|agent_id| {
1246 let normalized = normalize_agent_id_for_compare(agent_id).to_string();
1247 let signed_at = signed_at_by_agent
1248 .get(&normalized)
1249 .filter(|ts| !ts.is_empty())
1250 .cloned();
1251 let signed = signed_at.is_some();
1252 let mut signer = json!({
1253 "agentId": agent_id,
1254 "agent_id": agent_id,
1255 "signed": signed
1256 });
1257 if let Some(ts) = signed_at {
1258 signer["signedAt"] = json!(ts.clone());
1259 signer["signed_at"] = json!(ts);
1260 }
1261 signer
1262 })
1263 .collect::<Vec<Value>>();
1264
1265 let result = json!({
1266 "complete": pending.is_empty(),
1267 "signers": signers,
1268 "pending": pending
1269 });
1270
1271 Ok(result.to_string())
1272 }
1273
1274 pub fn sign_request(&self, payload_value: Value) -> BindingResult<String> {
1276 self.with_private_key_password(|| {
1277 let mut agent = self.lock()?;
1278
1279 let wrapper_value = serde_json::json!({
1280 "jacs_payload": payload_value
1281 });
1282
1283 let wrapper_string = serde_json::to_string(&wrapper_value).map_err(|e| {
1284 BindingCoreError::serialization_failed(format!(
1285 "Failed to serialize wrapper JSON: {}",
1286 e
1287 ))
1288 })?;
1289
1290 jacs::shared::document_create(
1291 &mut agent,
1292 &wrapper_string,
1293 None,
1294 None,
1295 true, None,
1297 Some(false),
1298 )
1299 .map_err(|e| {
1300 BindingCoreError::document_failed(format!("Failed to create document: {}", e))
1301 })
1302 })
1303 }
1304
1305 pub fn verify_response(&self, document_string: String) -> BindingResult<Value> {
1307 let mut agent = self.lock()?;
1308
1309 agent
1310 .verify_payload(document_string, None)
1311 .map_err(|e| BindingCoreError::verification_failed(e.to_string()))
1312 }
1313
1314 pub fn verify_response_with_agent_id(
1316 &self,
1317 document_string: String,
1318 ) -> BindingResult<(Value, String)> {
1319 let mut agent = self.lock()?;
1320
1321 agent
1322 .verify_payload_with_agent_id(document_string, None)
1323 .map_err(|e| BindingCoreError::verification_failed(e.to_string()))
1324 }
1325
1326 pub fn verify_document_by_id(&self, document_id: &str) -> BindingResult<bool> {
1331 use jacs::storage::StorageDocumentTraits;
1332
1333 if !document_id.contains(':') {
1335 return Err(BindingCoreError::invalid_argument(format!(
1336 "Document ID must be in 'uuid:version' format, got '{}'. \
1337 Use verify_document() with the full JSON string instead.",
1338 document_id
1339 )));
1340 }
1341
1342 let storage = jacs::storage::MultiStorage::default_new().map_err(|e| {
1343 BindingCoreError::generic(format!("Failed to initialize storage: {}", e))
1344 })?;
1345
1346 let doc = storage.get_document(document_id).map_err(|e| {
1347 BindingCoreError::document_failed(format!(
1348 "Failed to load document '{}' from storage: {}",
1349 document_id, e
1350 ))
1351 })?;
1352
1353 let doc_str = serde_json::to_string(&doc.value).map_err(|e| {
1354 BindingCoreError::serialization_failed(format!(
1355 "Failed to serialize document '{}': {}",
1356 document_id, e
1357 ))
1358 })?;
1359
1360 self.verify_document(&doc_str)
1361 }
1362
1363 pub fn get_document_by_id(&self, document_id: &str) -> BindingResult<String> {
1367 if !document_id.contains(':') {
1368 return Err(BindingCoreError::invalid_argument(format!(
1369 "Document ID must be in 'uuid:version' format, got '{}'.",
1370 document_id
1371 )));
1372 }
1373
1374 let agent = self.lock()?;
1375 let doc = agent.get_document(document_id).map_err(|e| {
1376 BindingCoreError::document_failed(format!(
1377 "Failed to load document '{}' from storage: {}",
1378 document_id, e
1379 ))
1380 })?;
1381
1382 serde_json::to_string(&doc.value).map_err(|e| {
1383 BindingCoreError::serialization_failed(format!(
1384 "Failed to serialize document '{}': {}",
1385 document_id, e
1386 ))
1387 })
1388 }
1389
1390 pub fn get_agent_id(&self) -> BindingResult<String> {
1392 let agent = self.lock()?;
1393 let value = agent
1394 .get_value()
1395 .ok_or_else(|| BindingCoreError::agent_load("Agent not loaded. Call load() first."))?;
1396 value
1397 .get("jacsId")
1398 .and_then(|v| v.as_str())
1399 .map(str::to_string)
1400 .filter(|id| !id.is_empty())
1401 .ok_or_else(|| {
1402 BindingCoreError::agent_load(
1403 "Agent not loaded or has no jacsId. Call load() first.",
1404 )
1405 })
1406 }
1407
1408 pub fn reencrypt_key(&self, old_password: &str, new_password: &str) -> BindingResult<()> {
1413 use jacs::crypt::aes_encrypt::reencrypt_private_key;
1414
1415 let agent = self.lock()?;
1417 let key_path = if let Some(config) = &agent.config {
1418 let key_dir = config
1419 .jacs_key_directory()
1420 .as_deref()
1421 .unwrap_or("./jacs_keys");
1422 let key_file = config
1423 .jacs_agent_private_key_filename()
1424 .as_deref()
1425 .unwrap_or("jacs.private.pem.enc");
1426 format!("{}/{}", key_dir, key_file)
1427 } else {
1428 "./jacs_keys/jacs.private.pem.enc".to_string()
1429 };
1430 drop(agent);
1431
1432 let encrypted_data = std::fs::read(&key_path).map_err(|e| {
1433 BindingCoreError::generic(format!(
1434 "Failed to read private key file '{}': {}",
1435 key_path, e
1436 ))
1437 })?;
1438
1439 let re_encrypted = reencrypt_private_key(&encrypted_data, old_password, new_password)
1440 .map_err(|e| BindingCoreError::generic(format!("Re-encryption failed: {}", e)))?;
1441
1442 std::fs::write(&key_path, &re_encrypted).map_err(|e| {
1443 BindingCoreError::generic(format!(
1444 "Failed to write re-encrypted key to '{}': {}",
1445 key_path, e
1446 ))
1447 })?;
1448
1449 Ok(())
1450 }
1451
1452 pub fn ephemeral(&self, algorithm: Option<&str>) -> BindingResult<String> {
1458 let algo = match algorithm.unwrap_or("pq2025") {
1460 "ed25519" => "ring-Ed25519",
1461 "rsa-pss" => "RSA-PSS",
1462 "pq2025" => "pq2025",
1463 other => other,
1464 };
1465
1466 let mut agent = Agent::ephemeral(algo).map_err(|e| {
1467 BindingCoreError::agent_load(format!("Failed to create ephemeral agent: {}", e))
1468 })?;
1469
1470 let template = jacs::create_minimal_blank_agent("ai".to_string(), None, None, None)
1471 .map_err(|e| {
1472 BindingCoreError::agent_load(format!(
1473 "Failed to create minimal agent template: {}",
1474 e
1475 ))
1476 })?;
1477 let mut agent_json: Value = serde_json::from_str(&template).map_err(|e| {
1478 BindingCoreError::serialization_failed(format!(
1479 "Failed to parse agent template JSON: {}",
1480 e
1481 ))
1482 })?;
1483 if let Some(obj) = agent_json.as_object_mut() {
1484 obj.insert("name".to_string(), json!("ephemeral"));
1485 obj.insert("description".to_string(), json!("Ephemeral JACS agent"));
1486 }
1487
1488 let instance = agent
1489 .create_agent_and_load(&agent_json.to_string(), true, Some(algo))
1490 .map_err(|e| {
1491 BindingCoreError::agent_load(format!("Failed to initialize ephemeral agent: {}", e))
1492 })?;
1493
1494 let agent_id = instance["jacsId"].as_str().unwrap_or("").to_string();
1495 let version = instance["jacsVersion"].as_str().unwrap_or("").to_string();
1496
1497 let mut inner = self.lock()?;
1499 *inner = agent;
1500
1501 let info = json!({
1502 "agent_id": agent_id,
1503 "name": "ephemeral",
1504 "version": version,
1505 "algorithm": algo,
1506 });
1507
1508 serde_json::to_string_pretty(&info).map_err(|e| {
1509 BindingCoreError::serialization_failed(format!(
1510 "Failed to serialize ephemeral agent info: {}",
1511 e
1512 ))
1513 })
1514 }
1515
1516 pub fn diagnostics(&self) -> String {
1518 let mut info = jacs::simple::diagnostics();
1519
1520 if let Ok(agent) = self.inner.lock() {
1521 if agent.ready() {
1522 info["agent_loaded"] = json!(true);
1523 if let Some(value) = agent.get_value() {
1524 info["agent_id"] = json!(value.get("jacsId").and_then(|v| v.as_str()));
1525 info["agent_version"] =
1526 json!(value.get("jacsVersion").and_then(|v| v.as_str()));
1527 }
1528 }
1529 if let Some(config) = &agent.config {
1530 if let Some(dir) = config.jacs_data_directory().as_ref() {
1531 info["data_directory"] = json!(dir);
1532 }
1533 if let Some(dir) = config.jacs_key_directory().as_ref() {
1534 info["key_directory"] = json!(dir);
1535 }
1536 if let Some(storage) = config.jacs_default_storage().as_ref() {
1537 info["default_storage"] = json!(storage);
1538 }
1539 if let Some(algo) = config.jacs_agent_key_algorithm().as_ref() {
1540 info["key_algorithm"] = json!(algo);
1541 }
1542 }
1543 }
1544
1545 serde_json::to_string_pretty(&info).unwrap_or_default()
1546 }
1547
1548 pub fn get_setup_instructions(&self, domain: &str, ttl: u32) -> BindingResult<String> {
1552 use jacs::agent::boilerplate::BoilerPlate;
1553 use jacs::dns::bootstrap::{
1554 DigestEncoding, build_dns_record, dnssec_guidance, emit_azure_cli,
1555 emit_cloudflare_curl, emit_gcloud_dns, emit_plain_bind, emit_route53_change_batch,
1556 tld_requirement_text,
1557 };
1558
1559 let agent = self.lock()?;
1560 let agent_value = agent.get_value().cloned().unwrap_or(json!({}));
1561 let agent_id = agent_value
1562 .get("jacsId")
1563 .and_then(|v| v.as_str())
1564 .unwrap_or("");
1565 if agent_id.is_empty() {
1566 return Err(BindingCoreError::agent_load(
1567 "Agent not loaded or has no jacsId. Call load() first.",
1568 ));
1569 }
1570
1571 let pk = agent
1572 .get_public_key()
1573 .map_err(|e| BindingCoreError::generic(format!("Failed to get public key: {}", e)))?;
1574 let digest = jacs::dns::bootstrap::pubkey_digest_b64(&pk);
1575 let rr = build_dns_record(domain, ttl, agent_id, &digest, DigestEncoding::Base64);
1576
1577 let dns_record_bind = emit_plain_bind(&rr);
1578 let dns_owner = rr.owner.clone();
1579 let dns_record_value = rr.txt.clone();
1580
1581 let mut provider_commands = std::collections::HashMap::new();
1582 provider_commands.insert("bind".to_string(), dns_record_bind.clone());
1583 provider_commands.insert("route53".to_string(), emit_route53_change_batch(&rr));
1584 provider_commands.insert("gcloud".to_string(), emit_gcloud_dns(&rr, "YOUR_ZONE_NAME"));
1585 provider_commands.insert(
1586 "azure".to_string(),
1587 emit_azure_cli(&rr, "YOUR_RG", domain, "_v1.agent.jacs"),
1588 );
1589 provider_commands.insert(
1590 "cloudflare".to_string(),
1591 emit_cloudflare_curl(&rr, "YOUR_ZONE_ID"),
1592 );
1593
1594 let mut dnssec_instructions = std::collections::HashMap::new();
1595 for name in &["aws", "cloudflare", "azure", "gcloud"] {
1596 dnssec_instructions.insert(name.to_string(), dnssec_guidance(name).to_string());
1597 }
1598
1599 let tld_requirement = tld_requirement_text().to_string();
1600
1601 let well_known = json!({
1602 "jacs_agent_id": agent_id,
1603 "jacs_public_key_hash": digest,
1604 "jacs_dns_record": dns_owner,
1605 });
1606 let well_known_json = serde_json::to_string_pretty(&well_known).unwrap_or_default();
1607
1608 let summary = format!(
1609 "Setup instructions for agent {agent_id} on domain {domain}:\n\
1610 \n\
1611 1. DNS: Publish the following TXT record:\n\
1612 {bind}\n\
1613 \n\
1614 2. DNSSEC: {dnssec}\n\
1615 \n\
1616 3. Domain requirement: {tld}\n\
1617 \n\
1618 4. .well-known: Serve the well-known JSON at /.well-known/jacs-agent.json",
1619 agent_id = agent_id,
1620 domain = domain,
1621 bind = dns_record_bind,
1622 dnssec = dnssec_guidance("aws"),
1623 tld = tld_requirement,
1624 );
1625
1626 let result = json!({
1627 "dns_record_bind": dns_record_bind,
1628 "dns_record_value": dns_record_value,
1629 "dns_owner": dns_owner,
1630 "provider_commands": provider_commands,
1631 "dnssec_instructions": dnssec_instructions,
1632 "tld_requirement": tld_requirement,
1633 "well_known_json": well_known_json,
1634 "summary": summary,
1635 });
1636
1637 serde_json::to_string_pretty(&result).map_err(|e| {
1638 BindingCoreError::serialization_failed(format!(
1639 "Failed to serialize setup instructions: {}",
1640 e
1641 ))
1642 })
1643 }
1644
1645 pub fn export_agent(&self) -> BindingResult<String> {
1647 let agent = self.lock()?;
1648 let value = agent
1649 .get_value()
1650 .cloned()
1651 .ok_or_else(|| BindingCoreError::agent_load("Agent not loaded. Call load() first."))?;
1652 serde_json::to_string_pretty(&value).map_err(|e| {
1653 BindingCoreError::serialization_failed(format!(
1654 "Failed to serialize agent document: {}",
1655 e
1656 ))
1657 })
1658 }
1659
1660 pub fn get_public_key_pem(&self) -> BindingResult<String> {
1662 let agent = self.lock()?;
1663 let public_key = BoilerPlate::get_public_key(&*agent)
1664 .map_err(|e| BindingCoreError::generic(format!("Failed to get public key: {}", e)))?;
1665 Ok(jacs::crypt::normalize_public_key_pem(&public_key))
1666 }
1667
1668 pub fn get_agent_json(&self) -> BindingResult<String> {
1672 self.export_agent()
1673 }
1674}
1675
1676#[cfg(feature = "a2a")]
1677impl AgentWrapper {
1678 pub fn export_agent_card(&self) -> BindingResult<String> {
1686 let agent = self.lock()?;
1687 let card = jacs::a2a::agent_card::export_agent_card(&agent).map_err(|e| {
1688 BindingCoreError::generic(format!("Failed to export agent card: {}", e))
1689 })?;
1690 serde_json::to_string_pretty(&card).map_err(|e| {
1691 BindingCoreError::serialization_failed(format!("Failed to serialize agent card: {}", e))
1692 })
1693 }
1694
1695 pub fn generate_well_known_documents(
1699 &self,
1700 a2a_algorithm: Option<&str>,
1701 ) -> BindingResult<String> {
1702 let agent = self.lock()?;
1703 let card = jacs::a2a::agent_card::export_agent_card(&agent).map_err(|e| {
1704 BindingCoreError::generic(format!("Failed to export agent card: {}", e))
1705 })?;
1706
1707 let a2a_alg = a2a_algorithm.unwrap_or("ring-Ed25519");
1708 let dual_keys = jacs::a2a::keys::create_jwk_keys(None, Some(a2a_alg)).map_err(|e| {
1709 BindingCoreError::generic(format!("Failed to generate A2A keys: {}", e))
1710 })?;
1711
1712 let agent_id = agent
1713 .get_id()
1714 .map_err(|e| BindingCoreError::generic(format!("Failed to get agent ID: {}", e)))?;
1715
1716 let jws = jacs::a2a::extension::sign_agent_card_jws(
1717 &card,
1718 &dual_keys.a2a_private_key,
1719 &dual_keys.a2a_algorithm,
1720 &agent_id,
1721 )
1722 .map_err(|e| BindingCoreError::generic(format!("Failed to sign Agent Card: {}", e)))?;
1723
1724 let documents = jacs::a2a::extension::generate_well_known_documents(
1725 &agent,
1726 &card,
1727 &dual_keys.a2a_public_key,
1728 &dual_keys.a2a_algorithm,
1729 &jws,
1730 )
1731 .map_err(|e| {
1732 BindingCoreError::generic(format!("Failed to generate well-known documents: {}", e))
1733 })?;
1734
1735 let pairs: Vec<Value> = documents
1737 .into_iter()
1738 .map(|(path, doc)| serde_json::json!({ "path": path, "document": doc }))
1739 .collect();
1740 serde_json::to_string_pretty(&pairs).map_err(|e| {
1741 BindingCoreError::serialization_failed(format!(
1742 "Failed to serialize well-known documents: {}",
1743 e
1744 ))
1745 })
1746 }
1747
1748 #[deprecated(since = "0.9.0", note = "Use sign_artifact() instead")]
1752 pub fn wrap_a2a_artifact(
1753 &self,
1754 artifact_json: &str,
1755 artifact_type: &str,
1756 parent_signatures_json: Option<&str>,
1757 ) -> BindingResult<String> {
1758 if std::env::var("JACS_SHOW_DEPRECATIONS").is_ok() {
1759 tracing::warn!("wrap_a2a_artifact is deprecated, use sign_artifact instead");
1760 }
1761
1762 let artifact: Value = serde_json::from_str(artifact_json).map_err(|e| {
1763 BindingCoreError::invalid_argument(format!("Invalid artifact JSON: {}", e))
1764 })?;
1765
1766 let parent_signatures: Option<Vec<Value>> = match parent_signatures_json {
1767 Some(json_str) => {
1768 let parsed: Vec<Value> = serde_json::from_str(json_str).map_err(|e| {
1769 BindingCoreError::invalid_argument(format!(
1770 "Invalid parent signatures JSON array: {}",
1771 e
1772 ))
1773 })?;
1774 Some(parsed)
1775 }
1776 None => None,
1777 };
1778
1779 let mut agent = self.lock()?;
1780 let wrapped = jacs::a2a::provenance::wrap_artifact_with_provenance(
1781 &mut agent,
1782 artifact,
1783 artifact_type,
1784 parent_signatures,
1785 )
1786 .map_err(|e| BindingCoreError::signing_failed(format!("Failed to wrap artifact: {}", e)))?;
1787
1788 serde_json::to_string_pretty(&wrapped).map_err(|e| {
1789 BindingCoreError::serialization_failed(format!(
1790 "Failed to serialize wrapped artifact: {}",
1791 e
1792 ))
1793 })
1794 }
1795
1796 pub fn sign_artifact(
1801 &self,
1802 artifact_json: &str,
1803 artifact_type: &str,
1804 parent_signatures_json: Option<&str>,
1805 ) -> BindingResult<String> {
1806 #[allow(deprecated)]
1807 self.wrap_a2a_artifact(artifact_json, artifact_type, parent_signatures_json)
1808 }
1809
1810 pub fn verify_a2a_artifact(&self, wrapped_json: &str) -> BindingResult<String> {
1814 let wrapped: Value = serde_json::from_str(wrapped_json).map_err(|e| {
1815 BindingCoreError::invalid_argument(format!("Invalid wrapped artifact JSON: {}", e))
1816 })?;
1817
1818 let agent = self.lock()?;
1819 let result =
1820 jacs::a2a::provenance::verify_wrapped_artifact(&agent, &wrapped).map_err(|e| {
1821 BindingCoreError::verification_failed(format!(
1822 "A2A artifact verification error: {}",
1823 e
1824 ))
1825 })?;
1826
1827 serde_json::to_string_pretty(&result).map_err(|e| {
1828 BindingCoreError::serialization_failed(format!(
1829 "Failed to serialize verification result: {}",
1830 e
1831 ))
1832 })
1833 }
1834 pub fn assess_a2a_agent(&self, agent_card_json: &str, policy: &str) -> BindingResult<String> {
1838 use jacs::a2a::AgentCard;
1839 use jacs::a2a::trust::{A2ATrustPolicy, assess_a2a_agent};
1840
1841 let card: AgentCard = serde_json::from_str(agent_card_json).map_err(|e| {
1842 BindingCoreError::invalid_argument(format!("Invalid Agent Card JSON: {}", e))
1843 })?;
1844
1845 let trust_policy = A2ATrustPolicy::from_str_loose(policy).map_err(|e| {
1846 BindingCoreError::invalid_argument(format!("Invalid trust policy '{}': {}", policy, e))
1847 })?;
1848
1849 let agent = self.lock()?;
1850 let assessment = assess_a2a_agent(&agent, &card, trust_policy);
1851
1852 serde_json::to_string_pretty(&assessment).map_err(|e| {
1853 BindingCoreError::serialization_failed(format!(
1854 "Failed to serialize trust assessment: {}",
1855 e
1856 ))
1857 })
1858 }
1859
1860 pub fn verify_a2a_artifact_with_policy(
1876 &self,
1877 wrapped_json: &str,
1878 agent_card_json: &str,
1879 policy: &str,
1880 ) -> BindingResult<String> {
1881 use jacs::a2a::AgentCard;
1882 use jacs::a2a::trust::A2ATrustPolicy;
1883
1884 let wrapped: Value = serde_json::from_str(wrapped_json).map_err(|e| {
1885 BindingCoreError::invalid_argument(format!("Invalid wrapped artifact JSON: {}", e))
1886 })?;
1887
1888 let card: AgentCard = serde_json::from_str(agent_card_json).map_err(|e| {
1889 BindingCoreError::invalid_argument(format!("Invalid Agent Card JSON: {}", e))
1890 })?;
1891
1892 let trust_policy = A2ATrustPolicy::from_str_loose(policy).map_err(|e| {
1893 BindingCoreError::invalid_argument(format!("Invalid trust policy '{}': {}", policy, e))
1894 })?;
1895
1896 let agent = self.lock()?;
1897 let result = jacs::a2a::provenance::verify_wrapped_artifact_with_policy(
1898 &agent,
1899 &wrapped,
1900 &card,
1901 trust_policy,
1902 )
1903 .map_err(|e| {
1904 BindingCoreError::verification_failed(format!(
1905 "A2A artifact verification with policy error: {}",
1906 e
1907 ))
1908 })?;
1909
1910 serde_json::to_string_pretty(&result).map_err(|e| {
1911 BindingCoreError::serialization_failed(format!(
1912 "Failed to serialize verification result: {}",
1913 e
1914 ))
1915 })
1916 }
1917}
1918
1919impl AgentWrapper {
1920 #[cfg(feature = "attestation")]
1935 pub fn create_attestation(&self, params_json: &str) -> BindingResult<String> {
1936 use jacs::attestation::AttestationTraits;
1937 use jacs::attestation::types::*;
1938
1939 let params: Value = serde_json::from_str(params_json).map_err(|e| {
1940 BindingCoreError::serialization_failed(format!(
1941 "Failed to parse attestation params JSON: {}. \
1942 Provide a valid JSON object with 'subject' and 'claims' fields.",
1943 e
1944 ))
1945 })?;
1946
1947 let subject: AttestationSubject =
1949 serde_json::from_value(params.get("subject").cloned().ok_or_else(|| {
1950 BindingCoreError::validation(
1951 "Missing required 'subject' field in attestation params",
1952 )
1953 })?)
1954 .map_err(|e| BindingCoreError::validation(format!("Invalid 'subject' field: {}", e)))?;
1955
1956 let claims: Vec<Claim> =
1958 serde_json::from_value(params.get("claims").cloned().ok_or_else(|| {
1959 BindingCoreError::validation(
1960 "Missing required 'claims' field in attestation params",
1961 )
1962 })?)
1963 .map_err(|e| BindingCoreError::validation(format!("Invalid 'claims' field: {}", e)))?;
1964
1965 let evidence: Vec<EvidenceRef> = if let Some(ev) = params.get("evidence") {
1967 serde_json::from_value(ev.clone()).map_err(|e| {
1968 BindingCoreError::validation(format!("Invalid 'evidence' field: {}", e))
1969 })?
1970 } else {
1971 vec![]
1972 };
1973
1974 let derivation: Option<Derivation> = if let Some(d) = params.get("derivation") {
1976 Some(serde_json::from_value(d.clone()).map_err(|e| {
1977 BindingCoreError::validation(format!("Invalid 'derivation' field: {}", e))
1978 })?)
1979 } else {
1980 None
1981 };
1982
1983 let policy_context: Option<PolicyContext> = if let Some(p) = params.get("policyContext") {
1985 Some(serde_json::from_value(p.clone()).map_err(|e| {
1986 BindingCoreError::validation(format!("Invalid 'policyContext' field: {}", e))
1987 })?)
1988 } else {
1989 None
1990 };
1991
1992 let mut agent = self.lock()?;
1993 let jacs_doc = agent
1994 .create_attestation(
1995 &subject,
1996 &claims,
1997 &evidence,
1998 derivation.as_ref(),
1999 policy_context.as_ref(),
2000 )
2001 .map_err(|e| {
2002 BindingCoreError::document_failed(format!("Failed to create attestation: {}", e))
2003 })?;
2004
2005 serde_json::to_string_pretty(&jacs_doc.value).map_err(|e| {
2006 BindingCoreError::serialization_failed(format!(
2007 "Failed to serialize attestation: {}",
2008 e
2009 ))
2010 })
2011 }
2012
2013 #[cfg(feature = "attestation")]
2018 pub fn verify_attestation(&self, document_key: &str) -> BindingResult<String> {
2019 let agent = self.lock()?;
2020 let result = agent
2021 .verify_attestation_local_impl(document_key)
2022 .map_err(|e| {
2023 BindingCoreError::verification_failed(format!(
2024 "Attestation local verification failed: {}",
2025 e
2026 ))
2027 })?;
2028
2029 serde_json::to_string_pretty(&result).map_err(|e| {
2030 BindingCoreError::serialization_failed(format!(
2031 "Failed to serialize verification result: {}",
2032 e
2033 ))
2034 })
2035 }
2036
2037 #[cfg(feature = "attestation")]
2042 pub fn verify_attestation_full(&self, document_key: &str) -> BindingResult<String> {
2043 let agent = self.lock()?;
2044 let result = agent
2045 .verify_attestation_full_impl(document_key)
2046 .map_err(|e| {
2047 BindingCoreError::verification_failed(format!(
2048 "Attestation full verification failed: {}",
2049 e
2050 ))
2051 })?;
2052
2053 serde_json::to_string_pretty(&result).map_err(|e| {
2054 BindingCoreError::serialization_failed(format!(
2055 "Failed to serialize verification result: {}",
2056 e
2057 ))
2058 })
2059 }
2060
2061 #[cfg(feature = "attestation")]
2066 pub fn lift_to_attestation(
2067 &self,
2068 signed_doc_json: &str,
2069 claims_json: &str,
2070 ) -> BindingResult<String> {
2071 use jacs::attestation::types::Claim;
2072
2073 let claims: Vec<Claim> = serde_json::from_str(claims_json).map_err(|e| {
2074 BindingCoreError::serialization_failed(format!(
2075 "Failed to parse claims JSON: {}. \
2076 Provide a valid JSON array of claim objects.",
2077 e
2078 ))
2079 })?;
2080
2081 let mut agent = self.lock()?;
2082 let jacs_doc =
2083 jacs::attestation::migration::lift_to_attestation(&mut agent, signed_doc_json, &claims)
2084 .map_err(|e| {
2085 BindingCoreError::document_failed(format!(
2086 "Failed to lift document to attestation: {}",
2087 e
2088 ))
2089 })?;
2090
2091 serde_json::to_string_pretty(&jacs_doc.value).map_err(|e| {
2092 BindingCoreError::serialization_failed(format!(
2093 "Failed to serialize attestation: {}",
2094 e
2095 ))
2096 })
2097 }
2098
2099 #[cfg(feature = "attestation")]
2103 pub fn export_attestation_dsse(&self, attestation_json: &str) -> BindingResult<String> {
2104 let att_value: Value = serde_json::from_str(attestation_json).map_err(|e| {
2105 BindingCoreError::serialization_failed(format!(
2106 "Failed to parse attestation JSON: {}",
2107 e
2108 ))
2109 })?;
2110
2111 let envelope = jacs::attestation::dsse::export_dsse(&att_value).map_err(|e| {
2112 BindingCoreError::document_failed(format!("Failed to export DSSE envelope: {}", e))
2113 })?;
2114
2115 serde_json::to_string_pretty(&envelope).map_err(|e| {
2116 BindingCoreError::serialization_failed(format!(
2117 "Failed to serialize DSSE envelope: {}",
2118 e
2119 ))
2120 })
2121 }
2122
2123 pub fn build_auth_header(&self) -> BindingResult<String> {
2132 let mut agent = self.lock()?;
2133 jacs::protocol::build_auth_header(&mut agent).map_err(|e| {
2134 BindingCoreError::signing_failed(format!("Failed to build auth header: {}", e))
2135 })
2136 }
2137
2138 pub fn canonicalize_json(&self, json_string: &str) -> BindingResult<String> {
2142 let value: Value = serde_json::from_str(json_string).map_err(|e| {
2143 BindingCoreError::serialization_failed(format!(
2144 "Failed to parse JSON for canonicalization: {}",
2145 e
2146 ))
2147 })?;
2148 Ok(jacs::protocol::canonicalize_json(&value))
2149 }
2150
2151 pub fn sign_response(&self, payload_json: &str) -> BindingResult<String> {
2157 let mut agent = self.lock()?;
2158 let payload: Value = serde_json::from_str(payload_json).map_err(|e| {
2159 BindingCoreError::serialization_failed(format!(
2160 "Failed to parse payload JSON for sign_response: {}",
2161 e
2162 ))
2163 })?;
2164 let result = jacs::protocol::sign_response(&mut agent, &payload).map_err(|e| {
2165 BindingCoreError::signing_failed(format!("Failed to sign response: {}", e))
2166 })?;
2167 serde_json::to_string(&result).map_err(|e| {
2168 BindingCoreError::serialization_failed(format!(
2169 "Failed to serialize signed response: {}",
2170 e
2171 ))
2172 })
2173 }
2174
2175 pub fn encode_verify_payload(&self, document: &str) -> BindingResult<String> {
2180 Ok(jacs::protocol::encode_verify_payload(document))
2181 }
2182
2183 pub fn decode_verify_payload(&self, encoded: &str) -> BindingResult<String> {
2186 jacs::protocol::decode_verify_payload(encoded).map_err(|e| {
2187 BindingCoreError::serialization_failed(format!(
2188 "Failed to decode verify payload: {}",
2189 e
2190 ))
2191 })
2192 }
2193
2194 pub fn extract_document_id(&self, document: &str) -> BindingResult<String> {
2199 jacs::protocol::extract_document_id(document)
2200 .map_err(|e| BindingCoreError::generic(format!("Failed to extract document ID: {}", e)))
2201 }
2202
2203 pub fn unwrap_signed_event(
2212 &self,
2213 event_json: &str,
2214 server_keys_json: &str,
2215 ) -> BindingResult<String> {
2216 let agent = self.lock()?;
2217 let event: Value = serde_json::from_str(event_json).map_err(|e| {
2218 BindingCoreError::serialization_failed(format!(
2219 "Failed to parse event JSON for unwrap_signed_event: {}",
2220 e
2221 ))
2222 })?;
2223 let keys_map: HashMap<String, String> =
2224 serde_json::from_str(server_keys_json).map_err(|e| {
2225 BindingCoreError::serialization_failed(format!(
2226 "Failed to parse server keys JSON for unwrap_signed_event: {}",
2227 e
2228 ))
2229 })?;
2230 let keys: HashMap<String, Vec<u8>> = keys_map
2231 .into_iter()
2232 .map(|(k, v)| {
2233 let bytes = base64::engine::general_purpose::STANDARD
2234 .decode(&v)
2235 .unwrap_or_else(|_| v.into_bytes());
2236 (k, bytes)
2237 })
2238 .collect();
2239 let (data, verified) =
2240 jacs::protocol::unwrap_signed_event(&agent, &event, &keys).map_err(|e| {
2241 BindingCoreError::verification_failed(format!(
2242 "Failed to unwrap signed event: {}",
2243 e
2244 ))
2245 })?;
2246 let result = json!({"data": data, "verified": verified});
2247 serde_json::to_string(&result).map_err(|e| {
2248 BindingCoreError::serialization_failed(format!(
2249 "Failed to serialize unwrap_signed_event result: {}",
2250 e
2251 ))
2252 })
2253 }
2254}
2255
2256pub fn diagnostics_standalone() -> String {
2263 serde_json::to_string_pretty(&jacs::simple::diagnostics()).unwrap_or_default()
2264}
2265
2266#[derive(Debug, Clone)]
2272pub struct VerificationResult {
2273 pub valid: bool,
2275 pub signer_id: String,
2277 pub timestamp: String,
2279 pub agent_version: String,
2281}
2282
2283pub fn verify_document_standalone(
2302 signed_document: &str,
2303 key_resolution: Option<&str>,
2304 data_directory: Option<&str>,
2305 key_directory: Option<&str>,
2306) -> BindingResult<VerificationResult> {
2307 use std::collections::HashSet;
2308 use std::path::{Path, PathBuf};
2309 use std::sync::{Mutex, OnceLock};
2310
2311 fn absolutize_dir(raw: &str) -> String {
2312 let p = PathBuf::from(raw);
2313 if p.is_absolute() {
2314 p.to_string_lossy().to_string()
2315 } else {
2316 std::env::current_dir()
2317 .unwrap_or_else(|_| PathBuf::from("."))
2318 .join(p)
2319 .to_string_lossy()
2320 .to_string()
2321 }
2322 }
2323
2324 fn sig_field(doc: &str, field: &str) -> String {
2325 serde_json::from_str::<Value>(doc)
2326 .ok()
2327 .and_then(|v| {
2328 v.get("jacsSignature")
2329 .and_then(|s| s.get(field))
2330 .and_then(|f| f.as_str())
2331 .map(String::from)
2332 })
2333 .unwrap_or_default()
2334 }
2335
2336 fn has_local_key_cache(root: &Path, key_hash: &str) -> bool {
2337 if key_hash.is_empty() {
2338 return false;
2339 }
2340 root.join("public_keys")
2341 .join(format!("{}.pem", key_hash))
2342 .exists()
2343 && root
2344 .join("public_keys")
2345 .join(format!("{}.enc_type", key_hash))
2346 .exists()
2347 }
2348
2349 fn build_fixture_key_cache(cache_root: &Path, source_dirs: &[PathBuf]) -> usize {
2350 let public_keys_dir = cache_root.join("public_keys");
2351 if std::fs::create_dir_all(&public_keys_dir).is_err() {
2352 return 0;
2353 }
2354
2355 let mut written: HashSet<String> = HashSet::new();
2356 for dir in source_dirs {
2357 let entries = match std::fs::read_dir(dir) {
2358 Ok(v) => v,
2359 Err(_) => continue,
2360 };
2361
2362 for entry in entries.flatten() {
2363 let path = entry.path();
2364 if !path.is_file() {
2365 continue;
2366 }
2367 let Some(name) = path.file_name().and_then(|s| s.to_str()) else {
2368 continue;
2369 };
2370 let Some(prefix) = name.strip_suffix("_metadata.json") else {
2371 continue;
2372 };
2373
2374 let metadata = match std::fs::read_to_string(&path)
2375 .ok()
2376 .and_then(|s| serde_json::from_str::<Value>(&s).ok())
2377 {
2378 Some(v) => v,
2379 None => continue,
2380 };
2381 let key_hash = metadata
2382 .get("public_key_hash")
2383 .and_then(|v| v.as_str())
2384 .unwrap_or("")
2385 .trim();
2386 let signing_algorithm = metadata
2387 .get("signing_algorithm")
2388 .and_then(|v| v.as_str())
2389 .unwrap_or("")
2390 .trim();
2391 if key_hash.is_empty() || signing_algorithm.is_empty() {
2392 continue;
2393 }
2394 if written.contains(key_hash) {
2395 continue;
2396 }
2397
2398 let key_path = dir.join(format!("{}_public_key.pem", prefix));
2399 let key_bytes = match std::fs::read(&key_path) {
2400 Ok(v) => v,
2401 Err(_) => continue,
2402 };
2403
2404 if std::fs::write(public_keys_dir.join(format!("{}.pem", key_hash)), key_bytes)
2405 .is_err()
2406 {
2407 continue;
2408 }
2409 if std::fs::write(
2410 public_keys_dir.join(format!("{}.enc_type", key_hash)),
2411 signing_algorithm.as_bytes(),
2412 )
2413 .is_err()
2414 {
2415 continue;
2416 }
2417
2418 written.insert(key_hash.to_string());
2419 }
2420 }
2421
2422 written.len()
2423 }
2424
2425 fn standalone_verify_lock() -> &'static Mutex<()> {
2426 static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
2427 LOCK.get_or_init(|| Mutex::new(()))
2428 }
2429
2430 let _lock = standalone_verify_lock()
2431 .lock()
2432 .map_err(|e| BindingCoreError::generic(format!("Failed to lock standalone verify: {e}")))?;
2433
2434 let signer_id = sig_field(signed_document, "agentID");
2435 let timestamp = sig_field(signed_document, "date");
2436 let agent_version = sig_field(signed_document, "agentVersion");
2437 let signer_public_key_hash = sig_field(signed_document, "publicKeyHash");
2438
2439 let temp_dir = std::env::temp_dir().to_string_lossy().to_string();
2442 let raw_data_dir = data_directory
2443 .map(String::from)
2444 .unwrap_or_else(|| temp_dir.clone());
2445 let raw_key_dir = key_directory
2446 .map(String::from)
2447 .unwrap_or_else(|| raw_data_dir.clone());
2448
2449 let absolute_data_dir = absolutize_dir(&raw_data_dir);
2450 let absolute_key_dir = absolutize_dir(&raw_key_dir);
2451
2452 let mut effective_storage_root = if data_directory.is_some() {
2455 absolute_data_dir.clone()
2456 } else if key_directory.is_some() {
2457 absolute_key_dir.clone()
2458 } else {
2459 absolute_data_dir.clone()
2460 };
2461 let mut temp_cache_root: Option<PathBuf> = None;
2462
2463 let local_requested = key_resolution.map_or(true, |kr| {
2468 kr.split(',')
2469 .any(|part| part.trim().eq_ignore_ascii_case("local"))
2470 });
2471 if local_requested && !signer_public_key_hash.is_empty() {
2472 let current_root = PathBuf::from(&effective_storage_root);
2473 if !has_local_key_cache(¤t_root, &signer_public_key_hash) {
2474 let mut source_dirs = Vec::new();
2475 let data_path = PathBuf::from(&absolute_data_dir);
2476 let key_path = PathBuf::from(&absolute_key_dir);
2477 if data_path.exists() {
2478 source_dirs.push(data_path);
2479 }
2480 if key_path.exists() && !source_dirs.iter().any(|p| p == &key_path) {
2481 source_dirs.push(key_path);
2482 }
2483
2484 if !source_dirs.is_empty() {
2485 let nonce = std::time::SystemTime::now()
2486 .duration_since(std::time::UNIX_EPOCH)
2487 .map(|d| d.as_nanos())
2488 .unwrap_or(0);
2489 let cache_root = std::env::temp_dir().join(format!(
2490 "jacs_standalone_keycache_{}_{}",
2491 std::process::id(),
2492 nonce
2493 ));
2494 let _ = build_fixture_key_cache(&cache_root, &source_dirs);
2495 if has_local_key_cache(&cache_root, &signer_public_key_hash) {
2496 effective_storage_root = cache_root.to_string_lossy().to_string();
2497 temp_cache_root = Some(cache_root);
2498 } else {
2499 let _ = std::fs::remove_dir_all(&cache_root);
2500 }
2501 }
2502 }
2503 }
2504 let explicit_local_key_available = local_requested
2505 && !signer_public_key_hash.is_empty()
2506 && has_local_key_cache(
2507 &PathBuf::from(&effective_storage_root),
2508 &signer_public_key_hash,
2509 );
2510
2511 let data_dir = String::new();
2514 let key_dir = String::new();
2515
2516 let config = Config::new(
2517 Some("false".to_string()),
2518 Some(data_dir.clone()),
2519 Some(key_dir.clone()),
2520 Some("jacs.private.pem.enc".to_string()),
2521 Some("jacs.public.pem".to_string()),
2522 Some("pq2025".to_string()),
2523 None,
2524 Some("".to_string()),
2525 Some("fs".to_string()),
2526 );
2527 let config_json = serde_json::to_string_pretty(&config).map_err(|e| {
2528 BindingCoreError::serialization_failed(format!("Failed to serialize config: {}", e))
2529 })?;
2530
2531 let thread_id = format!("{:?}", std::thread::current().id())
2532 .chars()
2533 .map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
2534 .collect::<String>();
2535 let nonce = std::time::SystemTime::now()
2536 .duration_since(std::time::UNIX_EPOCH)
2537 .map(|d| d.as_nanos())
2538 .unwrap_or(0);
2539 let config_path = std::env::temp_dir().join(format!(
2540 "jacs_standalone_verify_config_{}_{}_{}.json",
2541 std::process::id(),
2542 thread_id,
2543 nonce
2544 ));
2545 std::fs::write(&config_path, &config_json)
2546 .map_err(|e| BindingCoreError::generic(format!("Failed to write temp config: {}", e)))?;
2547
2548 use jacs::storage::jenv;
2555
2556 struct KeyResolutionGuard {
2558 had_override: bool,
2559 prev_value: Option<String>,
2560 }
2561 impl Drop for KeyResolutionGuard {
2562 fn drop(&mut self) {
2563 if self.had_override {
2564 if let Some(ref val) = self.prev_value {
2565 let _ = jacs::storage::jenv::set_env_var("JACS_KEY_RESOLUTION", val);
2566 } else {
2567 let _ = jacs::storage::jenv::clear_env_var("JACS_KEY_RESOLUTION");
2568 }
2569 } else {
2570 let _ = jacs::storage::jenv::clear_env_var("JACS_KEY_RESOLUTION");
2571 }
2572 }
2573 }
2574 let kr_had_override = jenv::has_jenv_override("JACS_KEY_RESOLUTION");
2575 let kr_prev = if kr_had_override {
2576 jenv::get_env_var("JACS_KEY_RESOLUTION", false)
2577 .ok()
2578 .flatten()
2579 } else {
2580 None
2581 };
2582 if let Some(kr) = key_resolution {
2583 let _ = jenv::set_env_var("JACS_KEY_RESOLUTION", kr);
2584 } else {
2585 let _ = jenv::clear_env_var("JACS_KEY_RESOLUTION");
2586 }
2587 let _kr_guard = KeyResolutionGuard {
2588 had_override: kr_had_override,
2589 prev_value: kr_prev,
2590 };
2591
2592 let result: BindingResult<VerificationResult> = (|| {
2593 let wrapper = AgentWrapper::new();
2594 wrapper.load_file_only(config_path.to_string_lossy().to_string())?;
2595 let _ = wrapper.set_storage_root(PathBuf::from(&effective_storage_root));
2596
2597 if explicit_local_key_available {
2598 let key_base = PathBuf::from(&effective_storage_root)
2599 .join("public_keys")
2600 .join(&signer_public_key_hash);
2601 let public_key = std::fs::read(key_base.with_extension("pem")).map_err(|e| {
2602 BindingCoreError::verification_failed(format!(
2603 "Failed to load local public key for hash '{}': {}",
2604 signer_public_key_hash, e
2605 ))
2606 })?;
2607 let enc_type = std::fs::read_to_string(key_base.with_extension("enc_type"))
2608 .map_err(|e| {
2609 BindingCoreError::verification_failed(format!(
2610 "Failed to load local public key type for hash '{}': {}",
2611 signer_public_key_hash, e
2612 ))
2613 })?
2614 .trim()
2615 .to_string();
2616
2617 let mut agent = wrapper.lock()?;
2618 let doc = agent.load_document(signed_document).map_err(|e| {
2619 BindingCoreError::document_failed(format!("Failed to load document: {}", e))
2620 })?;
2621 let document_key = doc.getkey();
2622 let value = doc.getvalue();
2623 agent.verify_hash(value).map_err(|e| {
2624 BindingCoreError::verification_failed(format!(
2625 "Failed to verify document hash: {}",
2626 e
2627 ))
2628 })?;
2629 agent
2630 .verify_document_signature(
2631 &document_key,
2632 None,
2633 None,
2634 Some(public_key),
2635 Some(enc_type.clone()),
2636 )
2637 .map_err(|e| {
2638 BindingCoreError::verification_failed(format!(
2639 "Failed to verify document signature (enc_type={}): {}",
2640 enc_type, e
2641 ))
2642 })?;
2643
2644 return Ok(VerificationResult {
2645 valid: true,
2646 signer_id: signer_id.clone(),
2647 timestamp: timestamp.clone(),
2648 agent_version: agent_version.clone(),
2649 });
2650 }
2651
2652 let valid = wrapper.verify_document(signed_document)?;
2653 Ok(VerificationResult {
2654 valid,
2655 signer_id: signer_id.clone(),
2656 timestamp: timestamp.clone(),
2657 agent_version: agent_version.clone(),
2658 })
2659 })();
2660
2661 let _ = std::fs::remove_file(&config_path);
2663 if let Some(cache_root) = temp_cache_root {
2664 let _ = std::fs::remove_dir_all(cache_root);
2665 }
2666
2667 match result {
2668 Ok(r) => Ok(r),
2669 Err(e) => {
2670 if e.kind == ErrorKind::VerificationFailed
2671 || e.kind == ErrorKind::DocumentFailed
2672 || e.kind == ErrorKind::InvalidArgument
2673 {
2674 Ok(VerificationResult {
2675 valid: false,
2676 signer_id,
2677 timestamp,
2678 agent_version,
2679 })
2680 } else {
2681 Err(e)
2682 }
2683 }
2684 }
2685}
2686
2687pub fn hash_string(data: &str) -> String {
2693 jacs_hash_string(&data.to_string())
2694}
2695
2696pub fn hash_public_key_base64(public_key_b64: &str) -> BindingResult<String> {
2698 let public_key = decode_public_key_base64(public_key_b64)?;
2699 Ok(jacs::crypt::hash::hash_public_key(&public_key))
2700}
2701
2702pub fn build_jwk_set_from_public_key(
2704 public_key_b64: &str,
2705 key_algorithm: &str,
2706 key_id: &str,
2707) -> BindingResult<String> {
2708 let public_key = decode_public_key_base64(public_key_b64)?;
2709 let jwk_set = build_jwk_set_from_public_key_bytes(&public_key, key_algorithm, key_id)?;
2710 serde_json::to_string(&jwk_set).map_err(|e| {
2711 BindingCoreError::serialization_failed(format!("Failed to serialize JWK set: {}", e))
2712 })
2713}
2714
2715pub fn ensure_network_access(capability: &str) -> BindingResult<()> {
2717 let capability = jacs::config::NetworkCapability::from_str(capability)
2718 .map_err(BindingCoreError::invalid_argument)?;
2719 jacs::config::ensure_network_access(capability)
2720 .map_err(|e| BindingCoreError::network_failed(e.to_string()))
2721}
2722
2723pub fn fetch_agent_card(base_url: &str, timeout_ms: Option<u64>) -> BindingResult<String> {
2725 let trimmed = base_url.trim();
2726 if trimmed.is_empty() {
2727 return Err(BindingCoreError::invalid_argument(
2728 "Agent base URL cannot be empty",
2729 ));
2730 }
2731
2732 let card_url = format!(
2733 "{}/.well-known/agent-card.json",
2734 trimmed.trim_end_matches('/')
2735 );
2736 let parsed_url = Url::parse(&card_url).map_err(|e| {
2737 BindingCoreError::invalid_argument(format!("Invalid agent URL '{}': {}", base_url, e))
2738 })?;
2739 validate_network_url(&parsed_url, "Agent Card URL")?;
2740 jacs::config::ensure_network_access(jacs::config::NetworkCapability::AgentCardFetch)
2741 .map_err(|e| BindingCoreError::network_failed(e.to_string()))?;
2742
2743 let client = build_blocking_json_client(timeout_ms.unwrap_or(DEFAULT_NETWORK_TIMEOUT_MS))?;
2744 let response = client
2745 .get(parsed_url.clone())
2746 .header(ACCEPT, "application/json")
2747 .send()
2748 .map_err(|e| {
2749 if e.is_timeout() {
2750 BindingCoreError::network_failed(format!(
2751 "Agent discovery timed out: {}",
2752 parsed_url
2753 ))
2754 } else {
2755 BindingCoreError::network_failed(format!(
2756 "Agent unreachable: {} ({})",
2757 parsed_url, e
2758 ))
2759 }
2760 })?;
2761
2762 if response.status() == StatusCode::NOT_FOUND {
2763 return Err(BindingCoreError::network_failed(format!(
2764 "Agent card not found (404): {}",
2765 parsed_url
2766 )));
2767 }
2768
2769 if !response.status().is_success() {
2770 return Err(BindingCoreError::network_failed(format!(
2771 "Agent card request failed (HTTP {}): {}",
2772 response.status(),
2773 parsed_url
2774 )));
2775 }
2776
2777 let content_type = content_type_header(&response);
2778 if !content_type.is_empty() && !content_type.to_ascii_lowercase().contains("json") {
2779 return Err(BindingCoreError::validation(format!(
2780 "Agent card response is not JSON (content-type: {}): {}",
2781 content_type, parsed_url
2782 )));
2783 }
2784
2785 let body = response.text().map_err(|e| {
2786 BindingCoreError::network_failed(format!(
2787 "Failed to read Agent Card response from {}: {}",
2788 parsed_url, e
2789 ))
2790 })?;
2791
2792 parse_json_object_body(
2793 &body,
2794 format!("Agent card is not valid JSON: {}", parsed_url),
2795 format!("Agent card at {} is not a JSON object", parsed_url),
2796 )
2797}
2798
2799pub fn fetch_remote_key_lookup(
2801 base_url: Option<&str>,
2802 jacs_id: Option<&str>,
2803 version: Option<&str>,
2804 public_key_hash: Option<&str>,
2805 timeout_ms: Option<u64>,
2806) -> BindingResult<String> {
2807 let resolved_base_url = resolve_keys_base_url(base_url);
2808 let mut parsed_url = Url::parse(&resolved_base_url).map_err(|e| {
2809 BindingCoreError::invalid_argument(format!(
2810 "Invalid JACS key base URL '{}': {}",
2811 resolved_base_url, e
2812 ))
2813 })?;
2814 validate_network_url(&parsed_url, "JACS key lookup base URL")?;
2815
2816 {
2817 let mut segments = parsed_url.path_segments_mut().map_err(|_| {
2818 BindingCoreError::invalid_argument(format!(
2819 "Invalid JACS key base URL '{}': cannot append path segments",
2820 resolved_base_url
2821 ))
2822 })?;
2823
2824 if let Some(hash) = public_key_hash
2825 .map(str::trim)
2826 .filter(|value| !value.is_empty())
2827 {
2828 let normalized_hash = normalize_public_key_hash(hash)?;
2829 segments.extend(["jacs", "v1", "keys", "by-hash"]);
2830 segments.push(&normalized_hash);
2831 } else {
2832 let agent_id = jacs_id
2833 .map(str::trim)
2834 .filter(|value| !value.is_empty())
2835 .ok_or_else(|| {
2836 BindingCoreError::invalid_argument(
2837 "fetch_remote_key_lookup requires jacs_id or public_key_hash",
2838 )
2839 })?;
2840 let resolved_version = version
2841 .map(str::trim)
2842 .filter(|value| !value.is_empty())
2843 .unwrap_or("latest");
2844 segments.extend(["jacs", "v1", "agents"]);
2845 segments.push(agent_id);
2846 segments.push("keys");
2847 segments.push(resolved_version);
2848 }
2849 }
2850
2851 jacs::config::ensure_network_access(jacs::config::NetworkCapability::RemoteKeyFetch)
2852 .map_err(|e| BindingCoreError::network_failed(e.to_string()))?;
2853
2854 let client = build_blocking_json_client(timeout_ms.unwrap_or(DEFAULT_NETWORK_TIMEOUT_MS))?;
2855 let response = client
2856 .get(parsed_url.clone())
2857 .header(ACCEPT, "application/json")
2858 .send()
2859 .map_err(|e| {
2860 if e.is_timeout() {
2861 BindingCoreError::network_failed(format!(
2862 "Remote key lookup timed out: {}",
2863 parsed_url
2864 ))
2865 } else {
2866 BindingCoreError::network_failed(format!(
2867 "Failed to reach key lookup endpoint {}: {}",
2868 parsed_url, e
2869 ))
2870 }
2871 })?;
2872
2873 let status = response.status();
2874 let content_type = content_type_header(&response);
2875 let body = response.text().map_err(|e| {
2876 BindingCoreError::network_failed(format!(
2877 "Failed to read key lookup response from {}: {}",
2878 parsed_url, e
2879 ))
2880 })?;
2881
2882 if !status.is_success() {
2883 let detail = if body.trim().is_empty() {
2884 status.canonical_reason().unwrap_or("unknown error")
2885 } else {
2886 body.trim()
2887 };
2888 return Err(BindingCoreError::network_failed(format!(
2889 "HTTP {} from key lookup endpoint: {}",
2890 status.as_u16(),
2891 detail
2892 )));
2893 }
2894
2895 if !content_type.is_empty() && !content_type.to_ascii_lowercase().contains("json") {
2896 return Err(BindingCoreError::validation(format!(
2897 "Key lookup endpoint returned non-JSON response: {}",
2898 parsed_url
2899 )));
2900 }
2901
2902 parse_json_object_body(
2903 &body,
2904 format!(
2905 "Key lookup endpoint returned non-JSON response: {}",
2906 parsed_url
2907 ),
2908 format!(
2909 "Key lookup endpoint returned a non-object response: {}",
2910 parsed_url
2911 ),
2912 )
2913}
2914
2915pub fn resolve_private_key_password(
2919 config_path: Option<&str>,
2920 key_directory: Option<&str>,
2921 explicit_password: Option<&str>,
2922) -> BindingResult<String> {
2923 if let Some(password) = explicit_password {
2924 if password.trim().is_empty() {
2925 return Err(BindingCoreError::invalid_argument(
2926 "Explicit password provided but empty or whitespace-only.",
2927 ));
2928 }
2929 return Ok(password.to_string());
2930 }
2931
2932 if let Ok(password) = std::env::var("JACS_PRIVATE_KEY_PASSWORD") {
2933 if password.trim().is_empty() {
2934 return Err(BindingCoreError::invalid_argument(
2935 "JACS_PRIVATE_KEY_PASSWORD is set but empty or whitespace-only.",
2936 ));
2937 }
2938 return Ok(password);
2939 }
2940
2941 let (resolved_key_directory, agent_id) = resolve_password_context(config_path, key_directory)?;
2942
2943 match jacs::crypt::aes_encrypt::resolve_private_key_password(None, agent_id.as_deref()) {
2944 Ok(password) => Ok(password),
2945 Err(e) if missing_password_message(&e.to_string()) => Ok(read_password_file(
2946 &resolved_key_directory.join(".jacs_password"),
2947 )?
2948 .unwrap_or_default()),
2949 Err(e) => Err(BindingCoreError::generic(format!(
2950 "Failed to resolve private key password: {}",
2951 e
2952 ))),
2953 }
2954}
2955
2956pub fn quickstart_private_key_password(
2958 config_path: Option<&str>,
2959 key_directory: Option<&str>,
2960) -> BindingResult<String> {
2961 let existing = resolve_private_key_password(config_path, key_directory, None)?;
2962 if !existing.is_empty() {
2963 return Ok(existing);
2964 }
2965
2966 let password = generate_private_key_password_value();
2967 if truthy_env_var("JACS_SAVE_PASSWORD_FILE") {
2968 let (resolved_key_directory, _agent_id) =
2969 resolve_password_context(config_path, key_directory)?;
2970 persist_password_file(&resolved_key_directory, &password)?;
2971 }
2972
2973 Ok(password)
2974}
2975
2976pub fn create_config(
2978 jacs_use_security: Option<String>,
2979 jacs_data_directory: Option<String>,
2980 jacs_key_directory: Option<String>,
2981 jacs_agent_private_key_filename: Option<String>,
2982 jacs_agent_public_key_filename: Option<String>,
2983 jacs_agent_key_algorithm: Option<String>,
2984 jacs_private_key_password: Option<String>,
2985 jacs_agent_id_and_version: Option<String>,
2986 jacs_default_storage: Option<String>,
2987) -> BindingResult<String> {
2988 let config = Config::new(
2989 jacs_use_security,
2990 jacs_data_directory,
2991 jacs_key_directory,
2992 jacs_agent_private_key_filename,
2993 jacs_agent_public_key_filename,
2994 jacs_agent_key_algorithm,
2995 jacs_private_key_password,
2996 jacs_agent_id_and_version,
2997 jacs_default_storage,
2998 );
2999
3000 serde_json::to_string_pretty(&config).map_err(|e| {
3001 BindingCoreError::serialization_failed(format!("Failed to serialize config: {}", e))
3002 })
3003}
3004
3005pub fn trust_agent(agent_json: &str) -> BindingResult<String> {
3011 jacs::trust::trust_agent(agent_json)
3012 .map_err(|e| BindingCoreError::trust_failed(format!("Failed to trust agent: {}", e)))
3013}
3014
3015pub fn trust_agent_with_key(agent_json: &str, public_key_pem: &str) -> BindingResult<String> {
3019 if public_key_pem.trim().is_empty() {
3020 return Err(BindingCoreError::invalid_argument(
3021 "public_key_pem cannot be empty",
3022 ));
3023 }
3024 jacs::trust::trust_agent_with_key(agent_json, Some(public_key_pem)).map_err(|e| {
3025 BindingCoreError::trust_failed(format!("Failed to trust agent with explicit key: {}", e))
3026 })
3027}
3028
3029pub fn list_trusted_agents() -> BindingResult<Vec<String>> {
3031 jacs::trust::list_trusted_agents().map_err(|e| {
3032 BindingCoreError::trust_failed(format!("Failed to list trusted agents: {}", e))
3033 })
3034}
3035
3036pub fn untrust_agent(agent_id: &str) -> BindingResult<()> {
3038 jacs::trust::untrust_agent(agent_id)
3039 .map_err(|e| BindingCoreError::trust_failed(format!("Failed to untrust agent: {}", e)))
3040}
3041
3042pub fn is_trusted(agent_id: &str) -> bool {
3044 jacs::trust::is_trusted(agent_id)
3045}
3046
3047pub fn get_trusted_agent(agent_id: &str) -> BindingResult<String> {
3049 jacs::trust::get_trusted_agent(agent_id)
3050 .map_err(|e| BindingCoreError::trust_failed(format!("Failed to get trusted agent: {}", e)))
3051}
3052
3053pub fn audit(config_path: Option<&str>, recent_n: Option<u32>) -> BindingResult<String> {
3062 use jacs::audit::{AuditOptions, audit as jacs_audit};
3063
3064 let mut opts = AuditOptions::default();
3065 opts.config_path = config_path.map(String::from);
3066 if let Some(n) = recent_n {
3067 opts.recent_verify_count = Some(n);
3068 }
3069 let result =
3070 jacs_audit(opts).map_err(|e| BindingCoreError::generic(format!("Audit failed: {}", e)))?;
3071 serde_json::to_string_pretty(&result).map_err(|e| {
3072 BindingCoreError::serialization_failed(format!("Failed to serialize audit result: {}", e))
3073 })
3074}
3075
3076pub fn create_agent_programmatic(
3084 name: &str,
3085 password: &str,
3086 algorithm: Option<&str>,
3087 data_directory: Option<&str>,
3088 key_directory: Option<&str>,
3089 config_path: Option<&str>,
3090 agent_type: Option<&str>,
3091 description: Option<&str>,
3092 domain: Option<&str>,
3093 default_storage: Option<&str>,
3094) -> BindingResult<String> {
3095 use jacs::simple::{CreateAgentParams, SimpleAgent};
3096
3097 let params = CreateAgentParams {
3098 name: name.to_string(),
3099 password: password.to_string(),
3100 algorithm: algorithm.unwrap_or("pq2025").to_string(),
3101 data_directory: data_directory.unwrap_or("./jacs_data").to_string(),
3102 key_directory: key_directory.unwrap_or("./jacs_keys").to_string(),
3103 config_path: config_path.unwrap_or("./jacs.config.json").to_string(),
3104 agent_type: agent_type.unwrap_or("ai").to_string(),
3105 description: description.unwrap_or("").to_string(),
3106 domain: domain.unwrap_or("").to_string(),
3107 default_storage: default_storage.unwrap_or("fs").to_string(),
3108 storage: None,
3109 };
3110
3111 let (_agent, info) = SimpleAgent::create_with_params(params)
3112 .map_err(|e| BindingCoreError::agent_load(format!("Failed to create agent: {}", e)))?;
3113
3114 serde_json::to_string_pretty(&info).map_err(|e| {
3115 BindingCoreError::serialization_failed(format!("Failed to serialize agent info: {}", e))
3116 })
3117}
3118
3119pub fn handle_agent_create(filename: Option<&String>, create_keys: bool) -> BindingResult<()> {
3121 jacs::cli_utils::create::handle_agent_create(filename, create_keys)
3122 .map_err(|e| BindingCoreError::generic(e.to_string()))
3123}
3124
3125pub fn handle_agent_create_auto(
3128 filename: Option<&String>,
3129 create_keys: bool,
3130 auto_update_config: bool,
3131) -> BindingResult<()> {
3132 jacs::cli_utils::create::handle_agent_create_auto(filename, create_keys, auto_update_config)
3133 .map_err(|e| BindingCoreError::generic(e.to_string()))
3134}
3135
3136pub fn handle_config_create() -> BindingResult<()> {
3138 jacs::cli_utils::create::handle_config_create()
3139 .map_err(|e| BindingCoreError::generic(e.to_string()))
3140}
3141
3142pub use jacs::dns::bootstrap::DnsVerificationResult;
3148
3149pub fn verify_agent_dns(agent_json: &str, domain: &str) -> BindingResult<DnsVerificationResult> {
3154 jacs::dns::bootstrap::verify_agent_dns(agent_json, domain).map_err(|e| {
3155 BindingCoreError::invalid_argument(format!("DNS verification setup failed: {}", e))
3156 })
3157}
3158
3159pub use jacs;
3164
3165#[cfg(test)]
3170mod tests {
3171 use super::*;
3172 use std::path::PathBuf;
3173
3174 fn cross_language_fixtures_dir() -> Option<PathBuf> {
3175 let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3176 .parent()?
3177 .to_path_buf();
3178 let dir = workspace.join("jacs/tests/fixtures/cross-language");
3179 if dir.exists() { Some(dir) } else { None }
3180 }
3181
3182 #[test]
3183 fn verify_standalone_invalid_json_returns_valid_false() {
3184 let result = verify_document_standalone("not json", Some("local"), None, None).unwrap();
3185 assert!(!result.valid);
3186 assert_eq!(result.signer_id, "");
3187 }
3188
3189 #[test]
3190 fn verify_standalone_tampered_document_returns_valid_false_with_signer_id() {
3191 let tampered = r#"{"jacsSignature":{"agentID":"golden-test-agent","agentVersion":"v1"},"jacsSha256":"x"}"#;
3192 let result = verify_document_standalone(tampered, Some("local"), None, None).unwrap();
3193 assert!(!result.valid);
3194 assert_eq!(result.signer_id, "golden-test-agent");
3195 }
3196
3197 #[test]
3198 fn verify_standalone_golden_invalid_signature_returns_valid_false() {
3199 let invalid_sig =
3200 std::fs::read_to_string("../jacs/tests/fixtures/golden/invalid_signature.json")
3201 .unwrap_or_else(|_| {
3202 r#"{"jacsSignature":{"agentID":"golden-test-agent"},"jacsSha256":"x"}"#
3203 .to_string()
3204 });
3205 let result = verify_document_standalone(
3206 &invalid_sig,
3207 Some("local"),
3208 Some("../jacs/tests/fixtures"),
3209 Some("../jacs/tests/fixtures/keys"),
3210 )
3211 .unwrap();
3212 assert!(!result.valid);
3213 assert_eq!(result.signer_id, "golden-test-agent");
3214 }
3215
3216 #[test]
3217 fn verify_standalone_nonexistent_key_directory_returns_valid_false() {
3218 let doc = r#"{"jacsSignature":{"agentID":"some-agent"},"jacsSha256":"x"}"#;
3219 let result = verify_document_standalone(
3220 doc,
3221 Some("local"),
3222 Some("/nonexistent_data"),
3223 Some("/nonexistent_keys"),
3224 )
3225 .unwrap();
3226 assert!(!result.valid);
3227 assert_eq!(result.signer_id, "some-agent");
3228 }
3229
3230 #[test]
3231 #[ignore = "pre-existing: cross-language fixture verification fails with relative parent paths"]
3232 fn verify_standalone_accepts_relative_parent_paths_from_subdir() {
3233 let Some(fixtures_dir) = cross_language_fixtures_dir() else {
3234 eprintln!("Skipping: cross-language fixtures directory not found");
3235 return;
3236 };
3237 let signed_path = fixtures_dir.join("python_ed25519_signed.json");
3238 if !signed_path.exists() {
3239 eprintln!(
3240 "Skipping: fixture '{}' not found",
3241 signed_path.to_string_lossy()
3242 );
3243 return;
3244 }
3245 let signed = std::fs::read_to_string(&signed_path).expect("read python fixture");
3246
3247 let workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
3248 .parent()
3249 .expect("workspace root")
3250 .to_path_buf();
3251 let jacsnpm_dir = workspace.join("jacsnpm");
3252 if !jacsnpm_dir.exists() {
3253 eprintln!("Skipping: jacsnpm directory not found");
3254 return;
3255 }
3256
3257 struct CwdGuard(PathBuf);
3258 impl Drop for CwdGuard {
3259 fn drop(&mut self) {
3260 let _ = std::env::set_current_dir(&self.0);
3261 }
3262 }
3263
3264 let original_cwd = std::env::current_dir().expect("current dir");
3265 std::env::set_current_dir(&jacsnpm_dir).expect("chdir to jacsnpm");
3266 let _guard = CwdGuard(original_cwd);
3267
3268 let rel = "../jacs/tests/fixtures/cross-language";
3269 let result = verify_document_standalone(&signed, Some("local"), Some(rel), Some(rel))
3270 .expect("standalone verify should not error");
3271 assert!(result.valid, "relative parent-path fixture should verify");
3272 }
3273
3274 #[test]
3275 fn verify_standalone_accepts_absolute_fixture_paths() {
3276 let Some(fixtures_dir) = cross_language_fixtures_dir() else {
3277 eprintln!("Skipping: cross-language fixtures directory not found");
3278 return;
3279 };
3280 let signed_path = fixtures_dir.join("python_ed25519_signed.json");
3281 if !signed_path.exists() {
3282 eprintln!(
3283 "Skipping: fixture '{}' not found",
3284 signed_path.to_string_lossy()
3285 );
3286 return;
3287 }
3288 let signed = std::fs::read_to_string(&signed_path).expect("read python fixture");
3289 let fixtures_abs = fixtures_dir
3290 .canonicalize()
3291 .unwrap_or_else(|_| fixtures_dir.clone());
3292 let fixtures_abs_str = fixtures_abs.to_string_lossy().to_string();
3293
3294 let result = verify_document_standalone(
3295 &signed,
3296 Some("local"),
3297 Some(&fixtures_abs_str),
3298 Some(&fixtures_abs_str),
3299 )
3300 .expect("standalone verify should not error");
3301 assert!(result.valid, "absolute-path fixture should verify");
3302 }
3303
3304 #[test]
3305 fn verify_standalone_uses_key_directory_when_data_directory_missing() {
3306 let Some(fixtures_dir) = cross_language_fixtures_dir() else {
3307 eprintln!("Skipping: cross-language fixtures directory not found");
3308 return;
3309 };
3310 let signed_path = fixtures_dir.join("python_ed25519_signed.json");
3311 if !signed_path.exists() {
3312 eprintln!(
3313 "Skipping: fixture '{}' not found",
3314 signed_path.to_string_lossy()
3315 );
3316 return;
3317 }
3318 let signed = std::fs::read_to_string(&signed_path).expect("read python fixture");
3319 let fixtures_abs = fixtures_dir
3320 .canonicalize()
3321 .unwrap_or_else(|_| fixtures_dir.clone());
3322 let fixtures_abs_str = fixtures_abs.to_string_lossy().to_string();
3323
3324 let result =
3325 verify_document_standalone(&signed, Some("local"), None, Some(&fixtures_abs_str))
3326 .expect("standalone verify should not error");
3327 assert!(
3328 result.valid,
3329 "key_directory should be usable as standalone storage root when data_directory is omitted"
3330 );
3331 }
3332
3333 #[test]
3334 fn verify_standalone_ignores_polluting_env_overrides() {
3335 let Some(fixtures_dir) = cross_language_fixtures_dir() else {
3336 eprintln!("Skipping: cross-language fixtures directory not found");
3337 return;
3338 };
3339 let signed_path = fixtures_dir.join("python_ed25519_signed.json");
3340 if !signed_path.exists() {
3341 eprintln!(
3342 "Skipping: fixture '{}' not found",
3343 signed_path.to_string_lossy()
3344 );
3345 return;
3346 }
3347 let signed = std::fs::read_to_string(&signed_path).expect("read python fixture");
3348 let fixtures_abs = fixtures_dir
3349 .canonicalize()
3350 .unwrap_or_else(|_| fixtures_dir.clone());
3351 let fixtures_abs_str = fixtures_abs.to_string_lossy().to_string();
3352
3353 struct EnvRestore(Vec<(&'static str, Option<std::ffi::OsString>)>);
3354 impl Drop for EnvRestore {
3355 fn drop(&mut self) {
3356 for (k, v) in &self.0 {
3357 if let Some(val) = v {
3358 unsafe { std::env::set_var(k, val) }
3360 } else {
3361 unsafe { std::env::remove_var(k) }
3363 }
3364 }
3365 }
3366 }
3367
3368 let keys = [
3369 "JACS_DATA_DIRECTORY",
3370 "JACS_KEY_DIRECTORY",
3371 "JACS_DEFAULT_STORAGE",
3372 "JACS_KEY_RESOLUTION",
3373 ];
3374 let mut prev = Vec::new();
3375 for k in keys {
3376 prev.push((k, std::env::var_os(k)));
3377 }
3378 let _restore = EnvRestore(prev);
3379
3380 unsafe {
3383 std::env::set_var("JACS_DATA_DIRECTORY", "/tmp/does-not-exist");
3384 std::env::set_var("JACS_KEY_DIRECTORY", "/tmp/does-not-exist");
3385 std::env::set_var("JACS_DEFAULT_STORAGE", "memory");
3386 std::env::set_var("JACS_KEY_RESOLUTION", "remote");
3387 }
3388
3389 let result = verify_document_standalone(
3390 &signed,
3391 Some("local"),
3392 Some(&fixtures_abs_str),
3393 Some(&fixtures_abs_str),
3394 )
3395 .expect("standalone verify should not error");
3396
3397 assert!(
3398 result.valid,
3399 "verification should ignore ambient JACS_* env pollution"
3400 );
3401 }
3402
3403 #[test]
3404 fn audit_default_returns_ok_json_has_risks_and_health_checks() {
3405 let json = audit(None, None).unwrap();
3406 let v: serde_json::Value = serde_json::from_str(&json).unwrap();
3407 assert!(v.get("risks").is_some(), "audit JSON should have risks");
3408 assert!(
3409 v.get("health_checks").is_some(),
3410 "audit JSON should have health_checks"
3411 );
3412 }
3413
3414 fn ephemeral_wrapper() -> AgentWrapper {
3420 let wrapper = AgentWrapper::new();
3421 wrapper.ephemeral(Some("ed25519")).unwrap();
3422 wrapper
3423 }
3424
3425 #[cfg(feature = "a2a")]
3426 #[test]
3427 fn test_export_agent_card_returns_valid_json() {
3428 let wrapper = ephemeral_wrapper();
3429 let card_json = wrapper.export_agent_card().unwrap();
3430 let card: Value = serde_json::from_str(&card_json).unwrap();
3431 assert!(card.get("name").is_some());
3432 assert!(card.get("protocolVersions").is_some());
3433 assert_eq!(card["protocolVersions"][0], "0.4.0");
3434 }
3435
3436 #[cfg(feature = "a2a")]
3437 #[test]
3438 #[allow(deprecated)]
3439 fn test_wrap_and_verify_a2a_artifact() {
3440 let wrapper = ephemeral_wrapper();
3441 let artifact = r#"{"content": "hello A2A"}"#;
3442
3443 let wrapped = wrapper
3444 .wrap_a2a_artifact(artifact, "message", None)
3445 .unwrap();
3446 let wrapped_value: Value = serde_json::from_str(&wrapped).unwrap();
3447 assert!(wrapped_value.get("jacsId").is_some());
3448 assert_eq!(wrapped_value["jacsType"], "a2a-message");
3449
3450 let result_json = wrapper.verify_a2a_artifact(&wrapped).unwrap();
3451 let result: Value = serde_json::from_str(&result_json).unwrap();
3452 assert_eq!(result["valid"], true);
3453 assert_eq!(result["status"], "SelfSigned");
3454 }
3455
3456 #[cfg(feature = "a2a")]
3457 #[test]
3458 fn test_sign_artifact_alias_matches_wrap() {
3459 let wrapper = ephemeral_wrapper();
3460 let artifact = r#"{"data": 42}"#;
3461
3462 let signed = wrapper.sign_artifact(artifact, "artifact", None).unwrap();
3463 let value: Value = serde_json::from_str(&signed).unwrap();
3464 assert_eq!(value["jacsType"], "a2a-artifact");
3465
3466 let result_json = wrapper.verify_a2a_artifact(&signed).unwrap();
3467 let result: Value = serde_json::from_str(&result_json).unwrap();
3468 assert_eq!(result["valid"], true);
3469 }
3470
3471 #[cfg(feature = "a2a")]
3472 #[test]
3473 #[allow(deprecated)]
3474 fn test_wrap_a2a_artifact_with_parent_chain() {
3475 let wrapper = ephemeral_wrapper();
3476
3477 let first = wrapper
3478 .wrap_a2a_artifact(r#"{"step": 1}"#, "task", None)
3479 .unwrap();
3480 let parents = format!("[{}]", first);
3481 let second = wrapper
3482 .wrap_a2a_artifact(r#"{"step": 2}"#, "task", Some(&parents))
3483 .unwrap();
3484
3485 let second_value: Value = serde_json::from_str(&second).unwrap();
3486 let parent_sigs = second_value["jacsParentSignatures"].as_array().unwrap();
3487 assert_eq!(parent_sigs.len(), 1);
3488 }
3489
3490 #[cfg(feature = "a2a")]
3491 #[test]
3492 #[allow(deprecated)]
3493 fn test_wrap_a2a_artifact_invalid_json_error() {
3494 let wrapper = ephemeral_wrapper();
3495 let result = wrapper.wrap_a2a_artifact("not json", "artifact", None);
3496 assert!(result.is_err());
3497 assert_eq!(result.unwrap_err().kind, ErrorKind::InvalidArgument);
3498 }
3499
3500 #[cfg(feature = "a2a")]
3501 #[test]
3502 fn test_verify_a2a_artifact_invalid_json_error() {
3503 let wrapper = ephemeral_wrapper();
3504 let result = wrapper.verify_a2a_artifact("not json");
3505 assert!(result.is_err());
3506 assert_eq!(result.unwrap_err().kind, ErrorKind::InvalidArgument);
3507 }
3508
3509 #[cfg(feature = "a2a")]
3510 #[test]
3511 fn test_export_agent_card_unloaded_agent_error() {
3512 let wrapper = AgentWrapper::new();
3513 let result = wrapper.export_agent_card();
3514 assert!(result.is_err());
3515 }
3516
3517 fn protocol_wrapper() -> AgentWrapper {
3523 let wrapper = AgentWrapper::new();
3524 wrapper.ephemeral(Some("ed25519")).unwrap();
3525 wrapper
3526 }
3527
3528 #[test]
3529 fn protocol_build_auth_header_starts_with_jacs() {
3530 let wrapper = protocol_wrapper();
3531 let header = wrapper
3532 .build_auth_header()
3533 .expect("build_auth_header failed");
3534 assert!(
3535 header.starts_with("JACS "),
3536 "Header must start with 'JACS ', got: {header}"
3537 );
3538 }
3539
3540 #[test]
3541 fn protocol_canonicalize_json_sorts_keys() {
3542 let wrapper = protocol_wrapper();
3543 let result = wrapper
3544 .canonicalize_json(r#"{"b":1,"a":2}"#)
3545 .expect("canonicalize_json failed");
3546 assert_eq!(result, r#"{"a":2,"b":1}"#);
3547 }
3548
3549 #[test]
3550 fn protocol_canonicalize_json_invalid_input() {
3551 let wrapper = protocol_wrapper();
3552 let result = wrapper.canonicalize_json("not json");
3553 assert!(result.is_err());
3554 assert_eq!(result.unwrap_err().kind, ErrorKind::SerializationFailed);
3555 }
3556
3557 #[test]
3558 fn protocol_sign_response_has_required_fields() {
3559 let wrapper = protocol_wrapper();
3560 let result = wrapper
3561 .sign_response(r#"{"answer": 42}"#)
3562 .expect("sign_response failed");
3563 let envelope: Value = serde_json::from_str(&result).expect("should be valid JSON");
3564 assert!(envelope.get("version").is_some(), "missing 'version'");
3565 assert!(
3566 envelope.get("jacsSignature").is_some(),
3567 "missing 'jacsSignature'"
3568 );
3569 assert_eq!(envelope["version"], "1.0.0");
3570 }
3571
3572 #[test]
3573 fn protocol_sign_response_invalid_payload() {
3574 let wrapper = protocol_wrapper();
3575 let result = wrapper.sign_response("not json");
3576 assert!(result.is_err());
3577 assert_eq!(result.unwrap_err().kind, ErrorKind::SerializationFailed);
3578 }
3579
3580 #[test]
3581 fn protocol_encode_verify_payload_round_trips() {
3582 let wrapper = protocol_wrapper();
3583 let original = r#"{"test":true}"#;
3584 let encoded = wrapper
3585 .encode_verify_payload(original)
3586 .expect("encode_verify_payload failed");
3587 assert!(!encoded.contains('+'), "URL-safe base64 must not contain +");
3588 assert!(!encoded.contains('/'), "URL-safe base64 must not contain /");
3589 assert!(
3590 !encoded.contains('='),
3591 "URL-safe base64 must not have padding"
3592 );
3593 let decoded = wrapper
3594 .decode_verify_payload(&encoded)
3595 .expect("decode_verify_payload failed");
3596 assert_eq!(decoded, original);
3597 }
3598
3599 #[test]
3600 fn protocol_extract_document_id_extracts_id() {
3601 let wrapper = protocol_wrapper();
3602 let id = wrapper
3603 .extract_document_id(r#"{"jacsDocumentId":"abc-123"}"#)
3604 .expect("extract_document_id failed");
3605 assert_eq!(id, "abc-123");
3606 }
3607
3608 #[test]
3609 fn protocol_extract_document_id_no_id_errors() {
3610 let wrapper = protocol_wrapper();
3611 let result = wrapper.extract_document_id(r#"{"name":"no-id"}"#);
3612 assert!(result.is_err());
3613 }
3614
3615 #[test]
3616 fn protocol_unwrap_signed_event_unknown_agent_unverified() {
3617 let wrapper = protocol_wrapper();
3618 let event = r#"{"data":{"result":"hello"},"jacsSignature":{"agentID":"unknown:v1","date":"2026-01-01T00:00:00Z","signature":"fakesig"}}"#;
3619 let keys = r#"{}"#;
3620 let result = wrapper
3621 .unwrap_signed_event(event, keys)
3622 .expect("unwrap_signed_event failed");
3623 let parsed: Value = serde_json::from_str(&result).expect("should be valid JSON");
3624 assert_eq!(parsed["verified"], false);
3625 assert_eq!(parsed["data"]["result"], "hello");
3626 }
3627
3628 #[test]
3629 fn protocol_unwrap_signed_event_legacy_payload() {
3630 let wrapper = protocol_wrapper();
3631 let event = r#"{"payload":{"status":"ok"}}"#;
3632 let keys = r#"{}"#;
3633 let result = wrapper
3634 .unwrap_signed_event(event, keys)
3635 .expect("unwrap_signed_event failed");
3636 let parsed: Value = serde_json::from_str(&result).expect("should be valid JSON");
3637 assert_eq!(parsed["verified"], false);
3638 assert_eq!(parsed["data"]["status"], "ok");
3639 }
3640
3641 #[test]
3642 fn protocol_unwrap_signed_event_plain_event() {
3643 let wrapper = protocol_wrapper();
3644 let event = r#"{"type":"heartbeat","ts":12345}"#;
3645 let keys = r#"{}"#;
3646 let result = wrapper
3647 .unwrap_signed_event(event, keys)
3648 .expect("unwrap_signed_event failed");
3649 let parsed: Value = serde_json::from_str(&result).expect("should be valid JSON");
3650 assert_eq!(parsed["verified"], false);
3651 assert_eq!(parsed["data"]["type"], "heartbeat");
3652 }
3653
3654 #[test]
3655 fn protocol_unwrap_signed_event_invalid_event_json() {
3656 let wrapper = protocol_wrapper();
3657 let result = wrapper.unwrap_signed_event("not json", "{}");
3658 assert!(result.is_err());
3659 assert_eq!(result.unwrap_err().kind, ErrorKind::SerializationFailed);
3660 }
3661
3662 #[test]
3663 fn protocol_unwrap_signed_event_invalid_keys_json() {
3664 let wrapper = protocol_wrapper();
3665 let result = wrapper.unwrap_signed_event(r#"{"type":"test"}"#, "not json");
3666 assert!(result.is_err());
3667 assert_eq!(result.unwrap_err().kind, ErrorKind::SerializationFailed);
3668 }
3669
3670 #[cfg(feature = "attestation")]
3675 mod attestation_tests {
3676 use super::*;
3677
3678 fn attestation_wrapper() -> AgentWrapper {
3679 let wrapper = AgentWrapper::new();
3680 wrapper.ephemeral(Some("ed25519")).unwrap();
3681 wrapper
3682 }
3683
3684 fn basic_attestation_params() -> String {
3685 json!({
3686 "subject": {
3687 "type": "artifact",
3688 "id": "test-artifact-001",
3689 "digests": { "sha256": "abc123" }
3690 },
3691 "claims": [{
3692 "name": "reviewed",
3693 "value": true,
3694 "confidence": 0.95,
3695 "assuranceLevel": "verified"
3696 }]
3697 })
3698 .to_string()
3699 }
3700
3701 #[test]
3702 fn binding_create_attestation_json() {
3703 let wrapper = attestation_wrapper();
3704 let result = wrapper.create_attestation(&basic_attestation_params());
3705 assert!(
3706 result.is_ok(),
3707 "create_attestation should succeed: {:?}",
3708 result.err()
3709 );
3710
3711 let json_str = result.unwrap();
3712 let doc: Value = serde_json::from_str(&json_str).unwrap();
3713 assert!(
3714 doc.get("attestation").is_some(),
3715 "returned JSON should contain 'attestation' key"
3716 );
3717 assert!(
3718 doc.get("jacsSignature").is_some(),
3719 "returned JSON should be signed"
3720 );
3721 }
3722
3723 #[test]
3724 fn binding_verify_attestation_json() {
3725 let wrapper = attestation_wrapper();
3726 let att_json = wrapper
3727 .create_attestation(&basic_attestation_params())
3728 .unwrap();
3729 let doc: Value = serde_json::from_str(&att_json).unwrap();
3730 let key = format!(
3731 "{}:{}",
3732 doc["jacsId"].as_str().unwrap(),
3733 doc["jacsVersion"].as_str().unwrap()
3734 );
3735
3736 let result = wrapper.verify_attestation(&key);
3737 assert!(
3738 result.is_ok(),
3739 "verify_attestation should succeed: {:?}",
3740 result.err()
3741 );
3742
3743 let result_json = result.unwrap();
3744 let result_value: Value = serde_json::from_str(&result_json).unwrap();
3745 assert_eq!(
3746 result_value["valid"], true,
3747 "attestation should verify as valid"
3748 );
3749 }
3750
3751 #[test]
3752 fn binding_verify_attestation_full_json() {
3753 let wrapper = attestation_wrapper();
3754 let att_json = wrapper
3755 .create_attestation(&basic_attestation_params())
3756 .unwrap();
3757 let doc: Value = serde_json::from_str(&att_json).unwrap();
3758 let key = format!(
3759 "{}:{}",
3760 doc["jacsId"].as_str().unwrap(),
3761 doc["jacsVersion"].as_str().unwrap()
3762 );
3763
3764 let result = wrapper.verify_attestation_full(&key);
3765 assert!(
3766 result.is_ok(),
3767 "verify_attestation_full should succeed: {:?}",
3768 result.err()
3769 );
3770
3771 let result_json = result.unwrap();
3772 let result_value: Value = serde_json::from_str(&result_json).unwrap();
3773 assert_eq!(
3774 result_value["valid"], true,
3775 "full attestation should verify as valid"
3776 );
3777 assert!(
3778 result_value.get("evidence").is_some(),
3779 "full verification result should contain 'evidence' array"
3780 );
3781 }
3782
3783 #[test]
3784 fn binding_lift_to_attestation_json() {
3785 let wrapper = attestation_wrapper();
3786
3787 let doc_json = json!({"title": "Test Document", "content": "Some content"}).to_string();
3789 let signed = wrapper
3790 .create_document(&doc_json, None, None, true, None, None)
3791 .unwrap();
3792
3793 let claims_json = json!([{
3794 "name": "reviewed",
3795 "value": true
3796 }])
3797 .to_string();
3798
3799 let result = wrapper.lift_to_attestation(&signed, &claims_json);
3800 assert!(
3801 result.is_ok(),
3802 "lift_to_attestation should succeed: {:?}",
3803 result.err()
3804 );
3805
3806 let att_json = result.unwrap();
3807 let doc: Value = serde_json::from_str(&att_json).unwrap();
3808 assert!(
3809 doc.get("attestation").is_some(),
3810 "lifted result should contain 'attestation' key"
3811 );
3812 assert!(
3813 doc.get("jacsSignature").is_some(),
3814 "lifted result should be signed"
3815 );
3816 }
3817
3818 #[test]
3819 fn binding_create_attestation_error_on_bad_json() {
3820 let wrapper = attestation_wrapper();
3821 let result = wrapper.create_attestation("not valid json {{{");
3822 assert!(result.is_err(), "bad JSON should error");
3823 assert_eq!(
3824 result.unwrap_err().kind,
3825 ErrorKind::SerializationFailed,
3826 "should be SerializationFailed error"
3827 );
3828 }
3829
3830 #[test]
3831 fn binding_create_attestation_error_on_missing_fields() {
3832 let wrapper = attestation_wrapper();
3833 let params = json!({
3835 "claims": [{"name": "test", "value": true}]
3836 })
3837 .to_string();
3838
3839 let result = wrapper.create_attestation(¶ms);
3840 assert!(result.is_err(), "missing subject should error");
3841 assert_eq!(
3842 result.unwrap_err().kind,
3843 ErrorKind::Validation,
3844 "should be Validation error"
3845 );
3846 }
3847
3848 #[test]
3849 fn binding_export_attestation_dsse() {
3850 let wrapper = attestation_wrapper();
3851 let att_json = wrapper
3852 .create_attestation(&basic_attestation_params())
3853 .unwrap();
3854
3855 let result = wrapper.export_attestation_dsse(&att_json);
3856 assert!(
3857 result.is_ok(),
3858 "export_attestation_dsse should succeed: {:?}",
3859 result.err()
3860 );
3861
3862 let dsse_json = result.unwrap();
3863 let envelope: Value = serde_json::from_str(&dsse_json).unwrap();
3864 assert_eq!(
3865 envelope["payloadType"].as_str().unwrap(),
3866 "application/vnd.in-toto+json"
3867 );
3868 }
3869 }
3870}