Skip to content

Commit e918cbf

Browse files
committed
fix: enforce message nonce TTL + bundle hash
1 parent d8b5711 commit e918cbf

File tree

3 files changed

+62
-2
lines changed

3 files changed

+62
-2
lines changed

crates/hush-multi-agent/src/message.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,8 @@ impl SignedMessage {
143143

144144
// Replay protection: per (iss, sub).
145145
let scope = format!("msg:{}:{}", self.claims.iss, self.claims.sub);
146-
revocations.check_and_mark_nonce(&scope, &self.claims.nonce, now_unix, 300)?;
146+
let ttl_secs = (self.claims.exp - now_unix).max(1);
147+
revocations.check_and_mark_nonce(&scope, &self.claims.nonce, now_unix, ttl_secs)?;
147148

148149
// Optional delegation token.
149150
if let Some(token) = &self.claims.delegation {

crates/hushd/src/api/policy.rs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ use axum::{extract::State, http::StatusCode, Json};
44
use serde::{Deserialize, Serialize};
55

66
use clawdstrike::{HushEngine, Policy, PolicyBundle, SignedPolicyBundle};
7-
use hush_core::Keypair;
7+
use hush_core::canonical::canonicalize;
8+
use hush_core::{sha256, Keypair};
89

910
use crate::audit::AuditEvent;
1011
use crate::state::AppState;
@@ -110,6 +111,28 @@ pub async fn update_policy_bundle(
110111
.validate()
111112
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid policy: {}", e)))?;
112113

114+
// Ensure policy_hash is correctly derived from the policy itself.
115+
//
116+
// The bundle is signed, but we still treat policy_hash as a derived field (it must not be
117+
// allowed to lie).
118+
let computed_policy_hash = {
119+
let value = serde_json::to_value(&signed.bundle.policy)
120+
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid policy: {}", e)))?;
121+
let canonical = canonicalize(&value)
122+
.map_err(|e| (StatusCode::BAD_REQUEST, format!("Invalid policy: {}", e)))?;
123+
sha256(canonical.as_bytes())
124+
};
125+
if computed_policy_hash != signed.bundle.policy_hash {
126+
return Err((
127+
StatusCode::UNPROCESSABLE_ENTITY,
128+
format!(
129+
"Policy bundle policy_hash mismatch (expected {}, got {})",
130+
computed_policy_hash.to_hex_prefixed(),
131+
signed.bundle.policy_hash.to_hex_prefixed(),
132+
),
133+
));
134+
}
135+
113136
// Update the engine (preserve signing keypair so receipts remain verifiable).
114137
let mut engine = state.engine.write().await;
115138
let keypair = if let Some(ref key_path) = state.config.signing_key {

crates/hushd/tests/integration.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,42 @@ async fn test_get_policy() {
106106
assert!(policy["policy_hash"].is_string());
107107
}
108108

109+
#[tokio::test]
110+
async fn test_update_policy_bundle_rejects_policy_hash_mismatch() {
111+
let (client, url) = test_setup();
112+
113+
let signer = hush_core::Keypair::generate();
114+
let policy = clawdstrike::Policy::new();
115+
let correct_hash = clawdstrike::PolicyBundle::new(policy.clone())
116+
.unwrap()
117+
.policy_hash;
118+
let bad_hash = {
119+
let mut bytes = *correct_hash.as_bytes();
120+
bytes[0] ^= 0x01;
121+
hush_core::Hash::from_bytes(bytes)
122+
};
123+
124+
let bundle = clawdstrike::PolicyBundle {
125+
version: clawdstrike::POLICY_BUNDLE_SCHEMA_VERSION.to_string(),
126+
bundle_id: "test-bundle".to_string(),
127+
compiled_at: chrono::Utc::now().to_rfc3339(),
128+
policy,
129+
policy_hash: bad_hash,
130+
sources: Vec::new(),
131+
metadata: None,
132+
};
133+
let signed = clawdstrike::SignedPolicyBundle::sign_with_public_key(bundle, &signer).unwrap();
134+
135+
let resp = client
136+
.put(format!("{}/api/v1/policy/bundle", url))
137+
.json(&signed)
138+
.send()
139+
.await
140+
.expect("Failed to connect to daemon");
141+
142+
assert_eq!(resp.status(), reqwest::StatusCode::UNPROCESSABLE_ENTITY);
143+
}
144+
109145
#[tokio::test]
110146
async fn test_audit_query() {
111147
let (client, url) = test_setup();

0 commit comments

Comments
 (0)