From b3b54fb2d2ea7c0e5bba6f04e58d3b8d977e0eef Mon Sep 17 00:00:00 2001 From: Jonathan Hendler Date: Sun, 29 Mar 2026 18:54:53 -0400 Subject: [PATCH 1/4] locs --- LINES_OF_CODE.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/LINES_OF_CODE.md b/LINES_OF_CODE.md index a9c37ba3..4c20c065 100644 --- a/LINES_OF_CODE.md +++ b/LINES_OF_CODE.md @@ -1,13 +1,13 @@ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Language Files Lines Code Comments Blanks ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Go 13 5859 4344 708 807 - Python 188 46396 36376 2037 7983 + Go 16 6521 4827 813 881 + Python 191 46952 36823 2064 8065 TypeScript 30 9105 6380 2000 725 ───────────────────────────────────────────────────────────────────────────────── - Rust 258 106330 87477 6054 12799 - |- Markdown 213 9703 491 7231 1981 - (Total) 116033 87968 13285 14780 + Rust 262 107208 88246 6071 12891 + |- Markdown 217 9784 491 7296 1997 + (Total) 116992 88737 13367 14888 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ - Total 489 177393 135068 18030 24295 + Total 499 179570 136767 18244 24559 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ From 21170ccddb9b8278cb1e42aadc162c02743fb941 Mon Sep 17 00:00:00 2001 From: Jonathan Hendler Date: Fri, 3 Apr 2026 00:26:25 +0200 Subject: [PATCH 2/4] pump fips --- jacs/Cargo.toml | 2 +- jacs/src/crypt/pq2025.rs | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/jacs/Cargo.toml b/jacs/Cargo.toml index 25c912c0..a710bab1 100644 --- a/jacs/Cargo.toml +++ b/jacs/Cargo.toml @@ -143,7 +143,7 @@ walkdir = "2.5.0" object_store = { version ="0.12.0", features = ["serde","serde_json", "aws", "http"] } # Post-quantum 2025 standards (ML-DSA and ML-KEM) fips203 = "0.4.3" -fips204 = "0.4.3" +fips204 = "0.4.6" # SQLite storage via sqlx (optional). PostgreSQL has been extracted to jacs-postgresql. sqlx = { version = "0.8.6", default-features = false, features = ["runtime-tokio-rustls"], optional = true } # rusqlite (sync SQLite bindings, optional -- lightweight alternative to sqlx SQLite) diff --git a/jacs/src/crypt/pq2025.rs b/jacs/src/crypt/pq2025.rs index 1c4405f2..def7934c 100644 --- a/jacs/src/crypt/pq2025.rs +++ b/jacs/src/crypt/pq2025.rs @@ -45,6 +45,10 @@ pub fn sign_string(secret_key: Vec, data: &String) -> Result Date: Fri, 3 Apr 2026 06:01:11 +0200 Subject: [PATCH 3/4] apache only --- Cargo.toml | 2 +- LICENSE | 4 +-- LICENSE-MIT | 21 ------------ README.md | 2 +- THIRD-PARTY-NOTICES | 46 +++++++++++++------------- binding-core/Cargo.toml | 2 +- deny.toml | 6 ++-- jacs-cli/Cargo.toml | 2 +- jacs-mcp/Cargo.toml | 2 +- jacs/Cargo.toml | 3 +- jacs/docs/nist_caisi_rfi_response.html | 2 +- jacs/docs/nist_caisi_rfi_response.md | 2 +- jacsgo/lib/Cargo.toml | 2 +- jacsnpm/Cargo.toml | 2 +- jacsnpm/package.json | 2 +- jacspy/Cargo.toml | 2 +- jacspy/pyproject.toml | 2 +- 17 files changed, 41 insertions(+), 63 deletions(-) delete mode 100644 LICENSE-MIT diff --git a/Cargo.toml b/Cargo.toml index 20474c3d..85592087 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,7 +18,7 @@ resolver = "3" rust-version = "1.93" readme = "README.md" authors = ["HAI.AI "] -license = "Apache-2.0 OR MIT" +license = "Apache-2.0" homepage = "https://humanassisted.github.io/JACS" repository = "https://github.com/HumanAssisted/JACS" keywords = ["cryptography", "json", "ai", "data", "ml-ops"] diff --git a/LICENSE b/LICENSE index 4ec386de..8059e80e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ -This project is dual-licensed under Apache-2.0 OR MIT, at your option. +This project is licensed under Apache-2.0. -See LICENSE-APACHE and LICENSE-MIT for details. +See LICENSE-APACHE for details. Copyright 2024, 2025, 2026 Human Assisted Intelligence, PBC diff --git a/LICENSE-MIT b/LICENSE-MIT deleted file mode 100644 index 4d0bd986..00000000 --- a/LICENSE-MIT +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2024, 2025, 2026 Human Assisted Intelligence, PBC - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/README.md b/README.md index eecfa26c..ade40cff 100644 --- a/README.md +++ b/README.md @@ -136,4 +136,4 @@ Report vulnerabilities to security@hai.ai. Do not open public issues for securit --- -v0.9.7 | [Apache-2.0 OR MIT](./LICENSE-APACHE) | [Third-Party Notices](./THIRD-PARTY-NOTICES) +v0.9.7 | [Apache-2.0](./LICENSE-APACHE) | [Third-Party Notices](./THIRD-PARTY-NOTICES) diff --git a/THIRD-PARTY-NOTICES b/THIRD-PARTY-NOTICES index aa24e3b6..6112d597 100644 --- a/THIRD-PARTY-NOTICES +++ b/THIRD-PARTY-NOTICES @@ -2,7 +2,7 @@ THIRD-PARTY SOFTWARE NOTICES AND INFORMATION JACS incorporates third-party software components. The following notices are provided for informational purposes. JACS is licensed under -Apache-2.0 OR MIT (see LICENSE-APACHE and LICENSE-MIT). +Apache-2.0 (see LICENSE-APACHE). =============================================================================== @@ -679,28 +679,28 @@ THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH REGARD TO ------------------------------------------------------------------------------- --- MIT --- -The MIT License (MIT) - -Copyright (c) 2016 Johann Tuffe - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +The MIT License (MIT) + +Copyright (c) 2016 Johann Tuffe + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ------------------------------------------------------------------------------- diff --git a/binding-core/Cargo.toml b/binding-core/Cargo.toml index d79634d5..bf128851 100644 --- a/binding-core/Cargo.toml +++ b/binding-core/Cargo.toml @@ -7,7 +7,7 @@ resolver = "3" description = "Shared core logic for JACS language bindings (Python, Node.js, etc.)" readme = "../README.md" authors = ["JACS Contributors"] -license = "Apache-2.0 OR MIT" +license = "Apache-2.0" repository = "https://github.com/HumanAssisted/JACS" homepage = "https://humanassisted.github.io/JACS" diff --git a/deny.toml b/deny.toml index 8778776e..2c2fe3f9 100644 --- a/deny.toml +++ b/deny.toml @@ -32,17 +32,17 @@ allow = [ # Workspace crates without explicit license field use the workspace license [[licenses.clarify]] name = "jacsnpm" -expression = "Apache-2.0 OR MIT" +expression = "Apache-2.0" license-files = [] [[licenses.clarify]] name = "jacspy" -expression = "Apache-2.0 OR MIT" +expression = "Apache-2.0" license-files = [] [[licenses.clarify]] name = "jacsgo" -expression = "Apache-2.0 OR MIT" +expression = "Apache-2.0" license-files = [] # ring contains legacy OpenSSL-licensed code alongside ISC/Apache-2.0 diff --git a/jacs-cli/Cargo.toml b/jacs-cli/Cargo.toml index 40c6f68e..beaa9254 100644 --- a/jacs-cli/Cargo.toml +++ b/jacs-cli/Cargo.toml @@ -6,7 +6,7 @@ rust-version = "1.93" description = "JACS CLI: command-line interface for JSON AI Communication Standard" readme = "README.md" authors = ["HAI.AI "] -license = "Apache-2.0 OR MIT" +license = "Apache-2.0" homepage = "https://humanassisted.github.io/JACS" repository = "https://github.com/HumanAssisted/JACS" keywords = ["cryptography", "json", "ai", "data", "ml-ops"] diff --git a/jacs-mcp/Cargo.toml b/jacs-mcp/Cargo.toml index 4fac39ab..02953547 100644 --- a/jacs-mcp/Cargo.toml +++ b/jacs-mcp/Cargo.toml @@ -5,7 +5,7 @@ edition = "2024" rust-version = "1.93" description = "MCP server for JACS: data provenance and cryptographic signing of agent state" readme = "README.md" -license = "Apache-2.0 OR MIT" +license = "Apache-2.0" homepage = "https://humanassisted.github.io/JACS" repository = "https://github.com/HumanAssisted/JACS" authors = ["JACS Contributors"] diff --git a/jacs/Cargo.toml b/jacs/Cargo.toml index a710bab1..1c437fa8 100644 --- a/jacs/Cargo.toml +++ b/jacs/Cargo.toml @@ -29,7 +29,6 @@ include = [ "README.md", "LICENSE", "LICENSE-APACHE", - "LICENSE-MIT", "build.rs", "CHANGELOG.md", "basic-schemas.png", @@ -44,7 +43,7 @@ include = [ description = "JACS JSON AI Communication Standard" readme = "README.md" authors = ["HAI.AI "] -license = "Apache-2.0 OR MIT" +license = "Apache-2.0" homepage = "https://humanassisted.github.io/JACS" repository = "https://github.com/HumanAssisted/JACS" keywords = ["cryptography", "json", "ai", "data", "ml-ops"] diff --git a/jacs/docs/nist_caisi_rfi_response.html b/jacs/docs/nist_caisi_rfi_response.html index 03658e85..75d1df8b 100644 --- a/jacs/docs/nist_caisi_rfi_response.html +++ b/jacs/docs/nist_caisi_rfi_response.html @@ -711,7 +711,7 @@

7. References

team and is based on practical experience developing and deploying cryptographic security controls for AI agent systems. All capabilities described are implemented and tested in the JACS codebase (v0.9.2, -Apache 2.0 OR MIT license, 1,200+ tests across 5 language +Apache 2.0 license, 1,200+ tests across 5 language targets).

diff --git a/jacs/docs/nist_caisi_rfi_response.md b/jacs/docs/nist_caisi_rfi_response.md index 3cc6b7d4..68a0b97f 100644 --- a/jacs/docs/nist_caisi_rfi_response.md +++ b/jacs/docs/nist_caisi_rfi_response.md @@ -374,4 +374,4 @@ As agent-to-agent communication protocols mature (Google A2A, Anthropic MCP), se --- -*This response represents the views of the HAI.AI / JACS project team and is based on practical experience developing and deploying cryptographic security controls for AI agent systems. All capabilities described are implemented and tested in the JACS codebase (v0.9.2, Apache 2.0 OR MIT license, 1,200+ tests across 5 language targets).* +*This response represents the views of the HAI.AI / JACS project team and is based on practical experience developing and deploying cryptographic security controls for AI agent systems. All capabilities described are implemented and tested in the JACS codebase (v0.9.2, Apache 2.0 license, 1,200+ tests across 5 language targets).* diff --git a/jacsgo/lib/Cargo.toml b/jacsgo/lib/Cargo.toml index f77fdece..68624097 100644 --- a/jacsgo/lib/Cargo.toml +++ b/jacsgo/lib/Cargo.toml @@ -4,7 +4,7 @@ version = "0.9.13" edition = "2024" rust-version = "1.93" resolver = "3" -license = "Apache-2.0 OR MIT" +license = "Apache-2.0" [lib] name = "jacsgo" diff --git a/jacsnpm/Cargo.toml b/jacsnpm/Cargo.toml index 6fda89c0..82d128b8 100644 --- a/jacsnpm/Cargo.toml +++ b/jacsnpm/Cargo.toml @@ -4,7 +4,7 @@ version = "0.9.13" edition = "2024" rust-version = "1.93" resolver = "3" -license = "Apache-2.0 OR MIT" +license = "Apache-2.0" [lib] name = "jacsnpm" diff --git a/jacsnpm/package.json b/jacsnpm/package.json index 8479af7a..1a491c4b 100644 --- a/jacsnpm/package.json +++ b/jacsnpm/package.json @@ -151,7 +151,7 @@ } }, "author": "HAI.AI", - "license": "Apache-2.0 OR MIT", + "license": "Apache-2.0", "homepage": "https://github.com/HumanAssisted/JACS", "repository": { "type": "git", diff --git a/jacspy/Cargo.toml b/jacspy/Cargo.toml index dc6a3f30..0b767697 100644 --- a/jacspy/Cargo.toml +++ b/jacspy/Cargo.toml @@ -4,7 +4,7 @@ version = "0.9.13" edition = "2024" rust-version = "1.93" resolver = "3" -license = "Apache-2.0 OR MIT" +license = "Apache-2.0" exclude = ["examples/**"] [lib] diff --git a/jacspy/pyproject.toml b/jacspy/pyproject.toml index 6931b5cd..1789cea6 100644 --- a/jacspy/pyproject.toml +++ b/jacspy/pyproject.toml @@ -7,7 +7,7 @@ version = "0.9.13" description = "JACS - JSON AI Communication Standard: Cryptographic signing and verification for AI agents." readme = "README.md" requires-python = ">=3.10" -license = { text = "Apache-2.0 OR MIT" } +license = { text = "Apache-2.0" } authors = [ { name = "HAI.AI", email = "engineering@hai.io" }, ] From b0ad9b553baceed60a83802e4f49a806529eee02 Mon Sep 17 00:00:00 2001 From: Jonathan Hendler Date: Mon, 6 Apr 2026 17:01:16 +0200 Subject: [PATCH 4/4] cleanup --- jacs/src/agent/agreement.rs | 6 ++ jacs/src/dns/bootstrap.rs | 104 ++++++++++++++++++++++++++++++-- jacs/src/keystore/mod.rs | 114 ++++++++++++++++++++++++++++++++++-- jacs/src/storage/mod.rs | 5 +- 4 files changed, 215 insertions(+), 14 deletions(-) diff --git a/jacs/src/agent/agreement.rs b/jacs/src/agent/agreement.rs index 100bc22c..961ece1e 100644 --- a/jacs/src/agent/agreement.rs +++ b/jacs/src/agent/agreement.rs @@ -20,6 +20,12 @@ use serde_json::json; use std::collections::HashSet; use tracing::{debug, info, warn}; +// WARNING: This module uses `.expect()` in multiple signing/verification +// paths (12+ call sites). These will panic on malformed input. +// Acceptable because agreements are NOT in the launch path (2026-04-06). +// TODO: Convert all `.expect()` calls to `?` error propagation before +// enabling agreements in production. See docs/0403INCONCISTANCIES.md item B5. + /// Options for creating and checking agreements. /// /// All fields are optional. When omitted, the existing behavior is preserved: diff --git a/jacs/src/dns/bootstrap.rs b/jacs/src/dns/bootstrap.rs index 413015c2..0fc01939 100644 --- a/jacs/src/dns/bootstrap.rs +++ b/jacs/src/dns/bootstrap.rs @@ -184,6 +184,25 @@ pub fn emit_cloudflare_curl(rr: &DnsRecord, zone_id_hint: &str) -> String { ) } +/// Find the first JACS-formatted TXT record from a list of individual TXT record strings. +/// +/// DNS lookups may return multiple TXT records (SPF, DKIM, DMARC, etc.) at the same +/// domain name. This function filters for the record starting with `v=jacs` and ignores +/// all others, preventing non-JACS records from corrupting the parsed result. +/// +/// Returns `Err(JacsError::DnsRecordInvalid)` if no JACS record is found. +pub fn find_jacs_txt_record(records: Vec, domain: &str) -> Result { + for record in records { + if record.starts_with("v=jacs") { + return Ok(record); + } + } + Err(JacsError::DnsRecordInvalid { + domain: domain.to_string(), + reason: "No v=jacs TXT record found at this domain".to_string(), + }) +} + #[cfg(not(target_arch = "wasm32"))] pub fn resolve_txt_dnssec(owner: &str) -> Result { use hickory_resolver::Resolver; @@ -202,18 +221,20 @@ pub fn resolve_txt_dnssec(owner: &str) -> Result { domain: owner.to_string(), reason: format!("DNS lookup failed: {e}"), })?; - let mut s = String::new(); + let mut records = Vec::new(); for rr in resp.iter() { + let mut record = String::new(); for part in rr.txt_data() { - s.push_str(&String::from_utf8(part.to_vec()).map_err(|e| { + record.push_str(&String::from_utf8(part.to_vec()).map_err(|e| { JacsError::DnsRecordInvalid { domain: owner.to_string(), reason: format!("UTF-8 decode failed: {e}"), } })?); } + records.push(record); } - Ok(s) + find_jacs_txt_record(records, owner) } #[cfg(not(target_arch = "wasm32"))] @@ -234,18 +255,20 @@ pub fn resolve_txt_insecure(owner: &str) -> Result { domain: owner.to_string(), reason: format!("DNS lookup failed: {e}"), })?; - let mut s = String::new(); + let mut records = Vec::new(); for rr in resp.iter() { + let mut record = String::new(); for part in rr.txt_data() { - s.push_str(&String::from_utf8(part.to_vec()).map_err(|e| { + record.push_str(&String::from_utf8(part.to_vec()).map_err(|e| { JacsError::DnsRecordInvalid { domain: owner.to_string(), reason: format!("UTF-8 decode failed: {e}"), } })?); } + records.push(record); } - Ok(s) + find_jacs_txt_record(records, owner) } pub fn verify_pubkey_via_dns_or_embedded( @@ -868,6 +891,75 @@ mod tests { } } + // --- H1 fix: find_jacs_txt_record filters non-JACS TXT records --- + + #[test] + fn test_find_jacs_txt_record_mixed_spf_and_jacs() { + let records = vec![ + "v=spf1 include:_spf.google.com ~all".to_string(), + "v=jacs; jacs_agent_id=a1; alg=SHA-256; enc=base64; jac_public_key_hash=abc" + .to_string(), + ]; + let result = find_jacs_txt_record(records, "example.com").unwrap(); + assert!( + result.starts_with("v=jacs"), + "Should return JACS record, got: {}", + result + ); + assert!( + result.contains("jacs_agent_id=a1"), + "Should contain agent ID, got: {}", + result + ); + } + + #[test] + fn test_find_jacs_txt_record_no_jacs_record() { + let records = vec![ + "v=spf1 include:_spf.google.com ~all".to_string(), + "google-site-verification=abc123".to_string(), + ]; + let result = find_jacs_txt_record(records, "example.com"); + assert!(result.is_err(), "Should return error when no JACS record"); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("No v=jacs TXT record"), + "Error should mention 'No v=jacs TXT record', got: {}", + err_msg + ); + } + + #[test] + fn test_find_jacs_txt_record_empty_records() { + let records: Vec = vec![]; + let result = find_jacs_txt_record(records, "example.com"); + assert!(result.is_err(), "Should return error for empty records"); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("No v=jacs TXT record"), + "Error should mention 'No v=jacs TXT record', got: {}", + err_msg + ); + } + + #[test] + fn test_find_jacs_txt_record_jacs_among_garbage() { + // JACS record between other unrelated TXT records + let records = vec![ + "v=spf1 include:_spf.google.com ~all".to_string(), + "google-site-verification=abc123".to_string(), + "v=jacs; jacs_agent_id=agent-42; alg=SHA-256; enc=hex; jac_public_key_hash=deadbeef" + .to_string(), + "v=DMARC1; p=reject; rua=mailto:dmarc@example.com".to_string(), + ]; + let result = find_jacs_txt_record(records, "example.com").unwrap(); + assert!( + result.contains("jacs_agent_id=agent-42"), + "Should extract the JACS record, got: {}", + result + ); + } + #[test] fn test_verify_pubkey_embedded_fallback_no_hai_references() { // Test that embedded fingerprint verification works without any hai.ai involvement diff --git a/jacs/src/keystore/mod.rs b/jacs/src/keystore/mod.rs index 5f2dd459..06faaa4b 100644 --- a/jacs/src/keystore/mod.rs +++ b/jacs/src/keystore/mod.rs @@ -537,6 +537,26 @@ impl InMemoryKeyStore { algorithm: algorithm.to_string(), } } + + /// Poison the private_key mutex for testing. Only available in test builds. + #[cfg(test)] + fn poison_private_key_mutex(&self) { + let mutex = &self.private_key; + let _result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let _guard = mutex.lock().unwrap(); + panic!("intentional panic to poison mutex"); + })); + } + + /// Poison the public_key mutex for testing. Only available in test builds. + #[cfg(test)] + fn poison_public_key_mutex(&self) { + let mutex = &self.public_key; + let _result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + let _guard = mutex.lock().unwrap(); + panic!("intentional panic to poison mutex"); + })); + } } impl fmt::Debug for InMemoryKeyStore { @@ -545,9 +565,12 @@ impl fmt::Debug for InMemoryKeyStore { .field("algorithm", &self.algorithm) .field( "has_private_key", - &self.private_key.lock().unwrap().is_some(), + &self.private_key.lock().ok().as_ref().map(|g| g.is_some()), + ) + .field( + "has_public_key", + &self.public_key.lock().ok().as_ref().map(|g| g.is_some()), ) - .field("has_public_key", &self.public_key.lock().unwrap().is_some()) .finish() } } @@ -583,8 +606,12 @@ impl KeyStore for InMemoryKeyStore { }; // Store copies in memory — no disk, no encryption. // Private key is wrapped in LockedVec for mlock + zeroize-on-drop protection. - *self.private_key.lock().unwrap() = Some(LockedVec::new(priv_key.clone())); - *self.public_key.lock().unwrap() = Some(pub_key.clone()); + *self.private_key.lock().map_err(|e| { + JacsError::CryptoError(format!("KeyStore private_key mutex poisoned: {e}")) + })? = Some(LockedVec::new(priv_key.clone())); + *self.public_key.lock().map_err(|e| { + JacsError::CryptoError(format!("KeyStore public_key mutex poisoned: {e}")) + })? = Some(pub_key.clone()); Ok((priv_key, pub_key)) } @@ -593,7 +620,9 @@ impl KeyStore for InMemoryKeyStore { // without holding the Mutex beyond this scope. self.private_key .lock() - .unwrap() + .map_err(|e| { + JacsError::CryptoError(format!("KeyStore private_key mutex poisoned: {e}")) + })? .as_ref() .map(|lv| LockedVec::new(lv.as_slice().to_vec())) .ok_or_else(|| "InMemoryKeyStore: no private key generated yet".into()) @@ -602,7 +631,9 @@ impl KeyStore for InMemoryKeyStore { fn load_public(&self) -> Result, JacsError> { self.public_key .lock() - .unwrap() + .map_err(|e| { + JacsError::CryptoError(format!("KeyStore public_key mutex poisoned: {e}")) + })? .clone() .ok_or_else(|| "InMemoryKeyStore: no public key generated yet".into()) } @@ -1461,4 +1492,75 @@ mod tests { let _ = std::fs::remove_dir_all(&dir_name); clear_fs_test_env(); } + + // --- M7 fix: Mutex poison handling returns Err instead of panicking --- + + #[test] + fn test_in_memory_generate_poisoned_mutex_returns_err() { + let ks = InMemoryKeyStore::new("ring-Ed25519"); + ks.poison_private_key_mutex(); + let spec = KeySpec { + algorithm: "ring-Ed25519".to_string(), + key_id: None, + }; + let result = ks.generate(&spec); + assert!( + result.is_err(), + "generate() should return Err on poisoned mutex, not panic" + ); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("mutex poisoned"), + "Error should mention 'mutex poisoned', got: {}", + err_msg + ); + } + + #[test] + fn test_in_memory_load_private_poisoned_mutex_returns_err() { + let ks = InMemoryKeyStore::new("ring-Ed25519"); + ks.poison_private_key_mutex(); + let result = ks.load_private(); + assert!( + result.is_err(), + "load_private() should return Err on poisoned mutex, not panic" + ); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("mutex poisoned"), + "Error should mention 'mutex poisoned', got: {}", + err_msg + ); + } + + #[test] + fn test_in_memory_load_public_poisoned_mutex_returns_err() { + let ks = InMemoryKeyStore::new("ring-Ed25519"); + ks.poison_public_key_mutex(); + let result = ks.load_public(); + assert!( + result.is_err(), + "load_public() should return Err on poisoned mutex, not panic" + ); + let err_msg = result.unwrap_err().to_string(); + assert!( + err_msg.contains("mutex poisoned"), + "Error should mention 'mutex poisoned', got: {}", + err_msg + ); + } + + #[test] + fn test_in_memory_debug_poisoned_mutex_does_not_panic() { + let ks = InMemoryKeyStore::new("ring-Ed25519"); + ks.poison_private_key_mutex(); + ks.poison_public_key_mutex(); + // Debug formatting should not panic even with poisoned mutexes + let debug_str = format!("{:?}", ks); + assert!( + debug_str.contains("InMemoryKeyStore"), + "Debug should still produce output, got: {}", + debug_str + ); + } } diff --git a/jacs/src/storage/mod.rs b/jacs/src/storage/mod.rs index ce0847e5..29f2a87d 100644 --- a/jacs/src/storage/mod.rs +++ b/jacs/src/storage/mod.rs @@ -865,8 +865,9 @@ impl StorageDocumentTraits for MultiStorage { _v1: &str, _v2: &str, ) -> Result { - // Placeholder implementation - // TODO: Implement proper document merging logic + // TODO: Document merging not yet implemented. + // See docs/0403INCONCISTANCIES.md item L12. + // This is a known gap -- merging is not needed for launch. Err("Document merging not yet implemented: feature pending".into()) }