Skip to content

Commit c1a545b

Browse files
Harden cache secret residency and CPU side-channel posture (#335)
* fix(core): make key schedule caching optional * fix(memory): use guarded hardware-enclave slab * fix(init): harden process before factory setup * fix(security): warn on CPU side-channel exposure * Harden FFI and binding security boundaries (#336) * fix(security): harden FFI and binding boundaries * fix(security): require explicit KMS configuration * fix(security): enforce data row record limits * fix(security): harden native binding boundaries * fix(security): zeroize Vault Transit plaintext staging * docs: add configuration drift guard TODO * docs: clarify drift guard TOFU adoption * fix(security): add metastore config drift guard * Fix CI regressions on cache hardening branch * Fix review and binding CI regressions * Move drift guard override logs out of sensitive paths * Fix sanitizer Miri hardening tests * Clamp key timestamp precision in factories
1 parent d9ae650 commit c1a545b

57 files changed

Lines changed: 2614 additions & 321 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1790,7 +1790,7 @@ jobs:
17901790
composer config repositories.asherah path /work/asherah-php
17911791
composer config minimum-stability dev
17921792
composer config prefer-stable true
1793-
composer require godaddy/asherah:* --no-interaction --no-progress >/dev/null
1793+
bash /work/scripts/composer-retry.sh require godaddy/asherah:* --no-interaction --no-progress >/dev/null
17941794
php vendor/godaddy/asherah/tests/consumer_smoke.php
17951795
'
17961796
- name: Consumer source install smoke (musl)
@@ -1806,7 +1806,7 @@ jobs:
18061806
composer config repositories.asherah path /work/asherah-php
18071807
composer config minimum-stability dev
18081808
composer config prefer-stable true
1809-
composer require godaddy/asherah:* --no-interaction --no-progress >/dev/null
1809+
bash /work/scripts/composer-retry.sh require godaddy/asherah:* --no-interaction --no-progress >/dev/null
18101810
php vendor/godaddy/asherah/tests/consumer_smoke.php
18111811
'
18121812

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ repos:
1717

1818
- id: cargo-test
1919
name: cargo test
20-
entry: cargo test --workspace --lib
20+
entry: cargo test --workspace --lib --exclude asherah-node --exclude asherah-py --exclude asherah-bench
2121
language: system
2222
types: [rust]
2323
pass_filenames: false

Cargo.lock

Lines changed: 5 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,4 +82,3 @@ unimplemented = "warn"
8282
unneeded_field_pattern = "warn"
8383
unseparated_literal_suffix = "warn"
8484
used_underscore_binding = "warn"
85-

README.md

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -161,8 +161,8 @@ TIER 1: mlock'd Slab (hot cache, ~400ns access)
161161
+----------------------------------------------------+
162162
Guard Page [PROT_NONE -- segfaults on access]
163163
164-
Canary bytes between guard pages and data detect
165-
buffer overflows at runtime.
164+
Guard pages fault on page-boundary overflows and underflows.
165+
Standalone secure buffers also use canaries for corruption detection.
166166
167167
TIER 2: Encrypted Enclaves (cold cache, ~1us access)
168168
========================================================
@@ -298,6 +298,21 @@ the key
298298
- **AES-256-GCM Enclaves**: Keys at rest in regular memory are encrypted
299299
with authenticated encryption; only the mlock'd Coffer can decrypt them
300300

301+
### Microarchitectural side channels
302+
303+
Asherah's memory hardening reduces normal process-memory exposure, but it does
304+
not by itself mitigate Spectre, Meltdown, L1TF, MDS, or related CPU side-channel
305+
classes. Guard pages and `mprotect` catch architectural out-of-bounds access;
306+
they do not make secrets invisible to speculative-execution gadgets or hostile
307+
co-tenants on an unmitigated host.
308+
309+
Production deployments that handle high-value plaintext or keys should run on
310+
hosts with current CPU microcode and kernel mitigations, avoid untrusted
311+
co-tenants on the same physical core, and consider disabling SMT or using core
312+
scheduling where the platform requires it. On Linux, `asherah-server` reads
313+
`/sys/devices/system/cpu/vulnerabilities/*` at startup and warns when the host
314+
reports a vulnerable or residual-vulnerable status.
315+
301316
## License
302317

303318
[Apache-2.0](LICENSE)

asherah-config/src/lib.rs

Lines changed: 72 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,19 @@ pub struct ConfigOptions {
6666
/// decryptors that lack metastore write permission.
6767
#[serde(rename = "SelfHealRecoveredKeys")]
6868
pub self_heal_recovered_keys: Option<bool>,
69+
/// Emergency escape hatch: run even if the persisted TOFU configuration
70+
/// drift guard conflicts with this resolved config. The guard is not
71+
/// rewritten. Env override: `ASHERAH_CONFIG_DRIFT_FORCE_RUN`.
72+
#[serde(rename = "ConfigDriftForceRun", alias = "ForceRunWithConfigDrift")]
73+
pub config_drift_force_run: Option<bool>,
74+
/// Repair escape hatch: replace the persisted TOFU configuration drift
75+
/// guard with this resolved config. Env override:
76+
/// `ASHERAH_CONFIG_DRIFT_FORCE_UPDATE`.
77+
#[serde(
78+
rename = "ConfigDriftForceUpdate",
79+
alias = "ForceUpdateConfigDriftGuard"
80+
)]
81+
pub config_drift_force_update: Option<bool>,
6982
#[serde(rename = "EnableSessionCaching")]
7083
pub enable_session_caching: Option<bool>,
7184
#[serde(rename = "Verbose")]
@@ -169,7 +182,7 @@ pub struct AppliedConfig {
169182
}
170183

171184
use asherah::builders::{
172-
KmsConfig, MetastoreConfig, PolicyConfig, PoolConfig, ResolvedConfig,
185+
ConfigDriftGuardOptions, KmsConfig, MetastoreConfig, PolicyConfig, PoolConfig, ResolvedConfig,
173186
TEST_DEBUG_STATIC_MASTER_KEY_HEX,
174187
};
175188

@@ -179,23 +192,23 @@ impl ConfigOptions {
179192
Ok(cfg)
180193
}
181194

195+
pub fn config_drift_guard_options(&self) -> ConfigDriftGuardOptions {
196+
ConfigDriftGuardOptions {
197+
allow_mismatch: self.config_drift_force_run.unwrap_or(false),
198+
force_update: self.config_drift_force_update.unwrap_or(false),
199+
}
200+
}
201+
182202
/// Resolve into a structured config — no env var reads or writes.
183203
pub fn resolve(&self) -> Result<(ResolvedConfig, AppliedConfig)> {
184-
// KMS aliases: `static` and `test-debug-static` are synonyms.
185-
// `test-debug-static` is the preferred identifier because it
186-
// makes the non-production nature obvious, but both must behave
187-
// identically to preserve interop with the canonical Go
188-
// implementation of Asherah (which accepts `static` and falls
189-
// back to the well-known test key when no hex is provided).
190-
// The fail-fast on empty `StaticMasterKeyHex` previously here
191-
// diverged from Go-canonical behavior; the static-KMS path
192-
// already logs a loud warning and tagged the test key as
193-
// public, which is the right safety net.
204+
// `test-debug-static` is an explicit test-only alias that may use the
205+
// public test key. Legacy `static` remains supported, but it must
206+
// provide an explicit key so a missing KMS config cannot silently fall
207+
// back to static master-key material.
194208
fn normalize_alias(value: &str) -> String {
195209
match value.to_lowercase().as_str() {
196210
"test-debug-memory" => "memory".to_string(),
197211
"test-debug-sqlite" => "sqlite".to_string(),
198-
"test-debug-static" => "static".to_string(),
199212
other => other.to_string(),
200213
}
201214
}
@@ -272,18 +285,24 @@ impl ConfigOptions {
272285

273286
let aws_profile_name = self.aws_profile_name.clone();
274287

275-
let kms_raw = self.kms.as_deref().unwrap_or("static");
288+
let kms_raw = self
289+
.kms
290+
.as_deref()
291+
.ok_or_else(|| anyhow!("KMS is required"))?;
276292
let kms_kind = normalize_alias(kms_raw);
277293
let kms = match kms_kind.as_str() {
278-
// `static` and `test-debug-static` collapse to the same
279-
// arm via `normalize_alias` above. Falls back to the
280-
// publicly-known test key when no hex is supplied (this
281-
// matches Go-canonical asherah behavior); the static-KMS
282-
// builder log-warns loudly that the key is non-production.
283294
"static" => KmsConfig::Static {
284295
key_hex: self
285296
.static_master_key_hex
286297
.clone()
298+
.filter(|key_hex| !key_hex.is_empty())
299+
.ok_or_else(|| anyhow!("StaticMasterKeyHex is required when KMS=static"))?,
300+
},
301+
"test-debug-static" => KmsConfig::Static {
302+
key_hex: self
303+
.static_master_key_hex
304+
.clone()
305+
.filter(|key_hex| !key_hex.is_empty())
287306
.unwrap_or_else(|| TEST_DEBUG_STATIC_MASTER_KEY_HEX.to_string()),
288307
},
289308
"aws" => KmsConfig::Aws {
@@ -454,16 +473,25 @@ fn resolve_rdbms_connection(
454473
/// Build a factory from structured config — no env var side effects.
455474
pub fn factory_from_config(config: &ConfigOptions) -> Result<(Factory, AppliedConfig)> {
456475
let (mut resolved, applied) = config.resolve()?;
457-
merge_recovery_region_suffixes_from_env(&mut resolved);
458-
let factory = asherah::builders::factory_from_resolved(&resolved)?;
476+
let mut config_drift_guard = config.config_drift_guard_options();
477+
merge_runtime_env_overrides(&mut resolved, &mut config_drift_guard);
478+
let factory = asherah::builders::factory_from_resolved_with_config_drift_guard(
479+
&resolved,
480+
config_drift_guard,
481+
)?;
459482
Ok((factory, applied))
460483
}
461484

462485
/// Async variant — safe for concurrent use since no env vars are written.
463486
pub async fn factory_from_config_async(config: &ConfigOptions) -> Result<(Factory, AppliedConfig)> {
464487
let (mut resolved, applied) = config.resolve()?;
465-
merge_recovery_region_suffixes_from_env(&mut resolved);
466-
let factory = asherah::builders::factory_from_resolved_async(&resolved).await?;
488+
let mut config_drift_guard = config.config_drift_guard_options();
489+
merge_runtime_env_overrides(&mut resolved, &mut config_drift_guard);
490+
let factory = asherah::builders::factory_from_resolved_with_config_drift_guard_async(
491+
&resolved,
492+
config_drift_guard,
493+
)
494+
.await?;
467495
Ok((factory, applied))
468496
}
469497

@@ -472,7 +500,18 @@ pub async fn factory_from_config_async(config: &ConfigOptions) -> Result<(Factor
472500
/// while preserving order. This gives every language binding the env-based
473501
/// recovery lever even when it has no first-class config field for it yet.
474502
/// Only reads env (never writes), so it is safe for concurrent use.
475-
fn merge_recovery_region_suffixes_from_env(resolved: &mut ResolvedConfig) {
503+
fn merge_runtime_env_overrides(
504+
resolved: &mut ResolvedConfig,
505+
config_drift_guard: &mut ConfigDriftGuardOptions,
506+
) {
507+
fn parse_bool(raw: &str) -> Option<bool> {
508+
match raw.trim().to_lowercase().as_str() {
509+
"1" | "true" | "yes" | "on" => Some(true),
510+
"0" | "false" | "no" | "off" => Some(false),
511+
_ => None,
512+
}
513+
}
514+
476515
if let Ok(raw) = std::env::var("RECOVERY_REGION_SUFFIXES") {
477516
for entry in raw.split(',').map(str::trim).filter(|s| !s.is_empty()) {
478517
if !resolved.recovery_region_suffixes.iter().any(|s| s == entry) {
@@ -488,6 +527,16 @@ fn merge_recovery_region_suffixes_from_env(resolved: &mut ResolvedConfig) {
488527
"0" | "false" | "no" | "off"
489528
);
490529
}
530+
if let Ok(raw) = std::env::var("ASHERAH_CONFIG_DRIFT_FORCE_RUN") {
531+
if let Some(force_run) = parse_bool(&raw) {
532+
config_drift_guard.allow_mismatch = force_run;
533+
}
534+
}
535+
if let Ok(raw) = std::env::var("ASHERAH_CONFIG_DRIFT_FORCE_UPDATE") {
536+
if let Some(force_update) = parse_bool(&raw) {
537+
config_drift_guard.force_update = force_update;
538+
}
539+
}
491540
}
492541

493542
/// Resolve the DynamoDB endpoint URL given the user-supplied

0 commit comments

Comments
 (0)