Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
## 0.5.2

### Security

- **[CRITICAL] Fixed trust store path traversal**: Agent IDs used in trust store file operations are now validated as proper UUID:UUID format and resulting paths are canonicalized to prevent directory traversal attacks via malicious agent IDs.

- **[CRITICAL] Fixed URL injection in HAI key fetch**: `agent_id` and `version` parameters in `fetch_public_key_from_hai()` and `verify_hai_registration_sync()` are now validated as UUIDs before URL interpolation, preventing path traversal in HTTP requests.

- **[HIGH] Added configurable signature expiration**: Signatures can now be configured to expire via `JACS_MAX_SIGNATURE_AGE_SECONDS` env var (e.g., `7776000` for 90 days). Default is `0` (no expiration) since JACS documents are designed to be idempotent and eternal.

- **[HIGH] Added strict algorithm enforcement mode**: Set `JACS_REQUIRE_EXPLICIT_ALGORITHM=true` to reject signature verification when `signingAlgorithm` is missing, preventing heuristic-based algorithm detection.

- **[HIGH] Fixed memory leak in schema domain whitelist**: Replaced `Box::leak()` with `OnceLock` for one-time parsing of `JACS_SCHEMA_ALLOWED_DOMAINS`, preventing unbounded memory growth.

- **[MEDIUM] Improved signed content canonicalization**: Fields are now sorted alphabetically before signing, non-string fields use canonical JSON serialization, and verification fails if zero fields are extracted.

- **[MEDIUM] Added HTTPS enforcement for HAI key service**: `HAI_KEYS_BASE_URL` must use HTTPS (localhost exempted for testing).

- **[MEDIUM] Added plaintext key warning**: Loading unencrypted private keys now emits a `tracing::warn` recommending encryption.

- **[LOW] Increased PBKDF2 iterations to 600,000**: Per OWASP 2024 recommendation (was 100,000). Automatic migration fallback: decryption tries new count first, then falls back to legacy 100,000 with a warning to re-encrypt.

- **[LOW] Deprecated `decrypt_private_key()`**: Use `decrypt_private_key_secure()` which returns `ZeroizingVec` for automatic memory zeroization.

- **[LOW] Added rate limiting on HAI key fetch**: Outgoing requests to the HAI key service are now rate-limited (2 req/s, burst of 3) using the existing `RateLimiter`.

- **[LOW] Renamed `JACS_USE_SECURITY` to `JACS_ENABLE_FILESYSTEM_QUARANTINE`**: Clarifies that this setting only controls filesystem quarantine of executable files, not cryptographic verification. Old name still works with a deprecation warning.

### Migration Notes

- Keys encrypted with pre-0.5.2 PBKDF2 iterations (100k) are automatically decrypted via fallback, but new encryptions use 600k iterations. Re-encrypt existing keys for improved security.

# PLANNED
- machine fingerprinting v2
- passkey-client integration
Expand Down
2 changes: 1 addition & 1 deletion jacs/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "jacs"
version = "0.5.1"
version = "0.5.2"
edition = "2024"
rust-version = "1.93"
resolver = "3"
Expand Down
19 changes: 16 additions & 3 deletions jacs/docs/jacsbook/src/advanced/security.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Signatures provide proof of origin:
|--------|------------|
| **Tampering** | Content hashes detect modifications |
| **Impersonation** | Cryptographic signatures verify identity |
| **Replay Attacks** | Timestamps and version IDs ensure freshness; future timestamps rejected |
| **Replay Attacks** | Timestamps and version IDs ensure freshness; future timestamps rejected; optional signature expiration via `JACS_MAX_SIGNATURE_AGE_SECONDS` |
| **Man-in-the-Middle** | DNS verification via DNSSEC; TLS certificate validation |
| **Key Compromise** | Key rotation through versioning |
| **Weak Passwords** | Minimum 28-bit entropy enforcement (35-bit for single class) |
Expand Down Expand Up @@ -195,7 +195,20 @@ JACS signatures include timestamps to prevent replay attacks and ensure temporal

1. **Timestamp Inclusion**: Every signature includes a UTC timestamp recording when it was created
2. **Future Timestamp Rejection**: Signatures with timestamps more than 5 minutes in the future are rejected
3. **Validation**: Timestamp validation occurs during signature verification
3. **Optional Signature Expiration**: Configurable via `JACS_MAX_SIGNATURE_AGE_SECONDS` (disabled by default since JACS documents are designed to be eternal)
4. **Validation**: Timestamp validation occurs during signature verification

### Configuring Signature Expiration

By default, signatures do not expire. JACS documents are designed to be idempotent and eternal. For use cases that require expiration:

```bash
# Enable expiration (e.g., 90 days)
export JACS_MAX_SIGNATURE_AGE_SECONDS=7776000

# Default: no expiration (0)
export JACS_MAX_SIGNATURE_AGE_SECONDS=0
```

### Protection Against Replay Attacks

Expand Down Expand Up @@ -470,7 +483,7 @@ client = JACSMCPClient("https://localhost:8000/sse") # Good
{
"jacs_dns_strict": true,
"jacs_dns_required": true,
"jacs_use_security": "1"
"jacs_enable_filesystem_quarantine": "true"
}
```

Expand Down
2 changes: 1 addition & 1 deletion jacs/docs/jacsbook/src/reference/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@ All other JACS settings are **configuration file fields** that have sensible def
- `jacs_key_directory` - Where cryptographic keys are stored (default: `./jacs_keys`)
- `jacs_agent_key_algorithm` - Cryptographic algorithm to use (default: `RSA-PSS`)
- `jacs_default_storage` - Storage backend (default: `fs`)
- `jacs_use_security` - Enable security features (default: `false`)
- `jacs_use_security` / `JACS_ENABLE_FILESYSTEM_QUARANTINE` - Enable filesystem quarantine of executable files (default: `false`). The env var `JACS_USE_SECURITY` is deprecated; use `JACS_ENABLE_FILESYSTEM_QUARANTINE` instead.

These can be overridden by environment variables if needed, but they are primarily configured through the `jacs.config.json` file.

Expand Down
23 changes: 23 additions & 0 deletions jacs/docs/jacsbook/src/reference/migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,29 @@ JACS maintains backward compatibility for document verification:
- Documents signed with older versions can be verified with newer versions
- Older JACS versions cannot verify documents using newer cryptographic algorithms

## Migrating from 0.5.1 to 0.5.2

### Migration Notes

**PBKDF2 Iteration Count**: New key encryptions use 600,000 iterations (up from 100,000). Existing encrypted keys are decrypted automatically via fallback. To upgrade existing keys, re-encrypt them:

```bash
# Re-generate keys to use the new iteration count
jacs keygen
```

### Deprecated Environment Variables

- `JACS_USE_SECURITY` is now `JACS_ENABLE_FILESYSTEM_QUARANTINE`. The old name still works with a deprecation warning.

### New Environment Variables

| Variable | Default | Description |
|----------|---------|-------------|
| `JACS_MAX_SIGNATURE_AGE_SECONDS` | `0` (no expiration) | Maximum age of valid signatures. Set to a positive value to enable (e.g., `7776000` for 90 days). |
| `JACS_REQUIRE_EXPLICIT_ALGORITHM` | `false` | When `true`, reject verification if `signingAlgorithm` is missing. |
| `JACS_ENABLE_FILESYSTEM_QUARANTINE` | `false` | Enable filesystem quarantine (replaces `JACS_USE_SECURITY`). |

## Migrating from 0.2.x to 0.3.x

### Configuration Changes
Expand Down
50 changes: 47 additions & 3 deletions jacs/src/agent/loaders.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::agent::boilerplate::BoilerPlate;
use crate::agent::security::SecurityTraits;
use crate::crypt::aes_encrypt::{decrypt_private_key_secure, encrypt_private_key};
use crate::error::JacsError;
use crate::rate_limit::RateLimiter;
use base64::{Engine as _, engine::general_purpose::STANDARD};
use flate2::Compression;
use flate2::write::GzEncoder;
Expand All @@ -13,8 +14,16 @@ use crate::time_utils;
use std::error::Error;
use std::io::Write;
use std::path::Path;
use std::sync::OnceLock;
use tracing::{debug, error, info, warn};

/// Rate limiter for HAI key service requests (2 req/s, burst of 3).
static HAI_KEY_RATE_LIMITER: OnceLock<RateLimiter> = OnceLock::new();

fn hai_key_rate_limiter() -> &'static RateLimiter {
HAI_KEY_RATE_LIMITER.get_or_init(|| RateLimiter::new(2.0, 3))
}

/// This environment variable determine if files are saved to the filesystem at all
/// if you are building something that passing data through to a database, you'd set this flag to 0 or False
///
Expand Down Expand Up @@ -816,6 +825,20 @@ fn decode_pem_public_key(pem_data: &str) -> Result<Vec<u8>, JacsError> {
/// Set to 0 to disable retries.
#[cfg(not(target_arch = "wasm32"))]
pub fn fetch_public_key_from_hai(agent_id: &str, version: &str) -> Result<PublicKeyInfo, JacsError> {
// Validate agent_id and version are valid UUIDs to prevent URL path traversal
uuid::Uuid::parse_str(agent_id).map_err(|e| {
JacsError::ValidationError(format!(
"Invalid agent_id '{}' for HAI key fetch: must be a valid UUID. {}",
agent_id, e
))
})?;
uuid::Uuid::parse_str(version).map_err(|e| {
JacsError::ValidationError(format!(
"Invalid version '{}' for HAI key fetch: must be a valid UUID. {}",
version, e
))
})?;

// Get retry count from environment or use default of 3
let max_retries: u32 = std::env::var("HAI_KEY_FETCH_RETRIES")
.ok()
Expand All @@ -826,6 +849,15 @@ pub fn fetch_public_key_from_hai(agent_id: &str, version: &str) -> Result<Public
let base_url =
std::env::var("HAI_KEYS_BASE_URL").unwrap_or_else(|_| "https://keys.hai.ai".to_string());

// Enforce HTTPS for security (prevent MITM on key fetch)
if !base_url.starts_with("https://") && !base_url.starts_with("http://localhost") && !base_url.starts_with("http://127.0.0.1") {
return Err(JacsError::ConfigError(format!(
"HAI_KEYS_BASE_URL must use HTTPS (got '{}'). \
Only localhost URLs are allowed over HTTP for testing.",
base_url
)));
}

let url = format!("{}/jacs/v1/agents/{}/keys/{}", base_url, agent_id, version);

info!(
Expand All @@ -844,6 +876,9 @@ pub fn fetch_public_key_from_hai(agent_id: &str, version: &str) -> Result<Public
JacsError::NetworkError("No attempts made to fetch public key".to_string());

for attempt in 1..=max_retries + 1 {
// Rate limit outgoing requests to avoid overwhelming the key service
hai_key_rate_limiter().acquire();

match fetch_public_key_attempt(&client, &url, agent_id, version) {
Ok(result) => return Ok(result),
Err(err) => {
Expand Down Expand Up @@ -1144,7 +1179,10 @@ CDEF
std::env::set_var("HAI_KEY_FETCH_RETRIES", "0");
}

let result = fetch_public_key_from_hai("test-agent-id", "1");
let result = fetch_public_key_from_hai(
"550e8400-e29b-41d4-a716-446655440000",
"550e8400-e29b-41d4-a716-446655440001",
);

// Clean up first to ensure it happens even if assertions fail
unsafe {
Expand Down Expand Up @@ -1178,7 +1216,10 @@ CDEF
}

// This will fail (no server), but we can verify it attempted the right URL
let result = fetch_public_key_from_hai("nonexistent-agent", "1");
let result = fetch_public_key_from_hai(
"550e8400-e29b-41d4-a716-446655440000",
"550e8400-e29b-41d4-a716-446655440001",
);

// Clean up
unsafe {
Expand All @@ -1205,7 +1246,10 @@ CDEF
}

let start = std::time::Instant::now();
let _ = fetch_public_key_from_hai("test-agent", "1");
let _ = fetch_public_key_from_hai(
"550e8400-e29b-41d4-a716-446655440000",
"550e8400-e29b-41d4-a716-446655440001",
);
let elapsed = start.elapsed();

// Clean up
Expand Down
15 changes: 10 additions & 5 deletions jacs/src/agent/security.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,12 @@ use std::os::unix::fs::PermissionsExt;
use std::path::Path;
use walkdir::WalkDir;

/// off by default
/// /// This environment variable determine if files are saved to the filesystem at all
/// if you are building something that passing data through to a database, you'd set this flag to 0 or False
const _JACS_USE_SECURITY: &str = "JACS_USE_SECURITY";
/// Controls filesystem quarantine of executable files found in the data directory.
/// Off by default. Set `JACS_ENABLE_FILESYSTEM_QUARANTINE=true` to enable.
/// Legacy env var `JACS_USE_SECURITY` is also supported but deprecated.
///
/// NOTE: This does NOT affect cryptographic signing or verification.
const _JACS_ENABLE_FILESYSTEM_QUARANTINE: &str = "JACS_ENABLE_FILESYSTEM_QUARANTINE";

pub trait SecurityTraits {
fn use_security(&self) -> bool;
Expand All @@ -30,7 +32,10 @@ impl SecurityTraits for Agent {
/// /// it will move all exuctable documents in JACS_DATA_DIRECTORY a quarantine directory
fn check_data_directory(&self) -> Result<(), Box<dyn Error>> {
if !self.use_security() {
info!("JACS_USE_SECURITY security is off");
info!(
"Filesystem quarantine is disabled. Set JACS_ENABLE_FILESYSTEM_QUARANTINE=true to enable. \
Note: this does NOT affect cryptographic signing or verification."
);
return Ok(());
}
if !self.use_fs_security() {
Expand Down
24 changes: 23 additions & 1 deletion jacs/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,29 @@ macro_rules! env_default {

env_default!(default_storage, "JACS_DEFAULT_STORAGE", "fs");
env_default!(default_algorithm, "JACS_AGENT_KEY_ALGORITHM", "RSA-PSS");
env_default!(default_security, "JACS_USE_SECURITY", "false");
/// Check `JACS_ENABLE_FILESYSTEM_QUARANTINE` (preferred) first,
/// fall back to legacy `JACS_USE_SECURITY` with a deprecation warning.
fn default_security() -> Option<String> {
// Preferred new name
if let Ok(Some(val)) = get_env_var("JACS_ENABLE_FILESYSTEM_QUARANTINE", false) {
if !val.is_empty() {
return Some(val);
}
}
// Legacy name (backwards compatible)
if let Ok(Some(val)) = get_env_var("JACS_USE_SECURITY", false) {
if !val.is_empty() {
eprintln!(
"DEPRECATION WARNING: JACS_USE_SECURITY is deprecated. \
Use JACS_ENABLE_FILESYSTEM_QUARANTINE instead. \
This env var only controls filesystem quarantine of executable files, \
not cryptographic verification."
);
return Some(val);
}
}
Some("false".to_string())
}

/// Helper to compute a directory default with CWD resolution for filesystem storage.
/// Falls back to a relative path if CWD cannot be determined or storage is not "fs".
Expand Down
Loading
Loading