Skip to main content

active_rpc/
security.rs

1//! Security helpers shared by providers and transports.
2//!
3//! Three rules, enforced here so no provider has to remember them:
4//!
5//! 1. **Secrets never cross the boundary as content.** An evaluated secret
6//!    becomes [`Value::Secret`], a marker; there is no variant that carries the
7//!    bytes.
8//! 2. **Redaction is monotone.** A redacting capability sees paths but not
9//!    secret-cone labels; a non-redacting one still never sees values.
10//! 3. **Diagnostics are value-free.** Provider errors carry stable codes.
11
12use std::collections::BTreeSet;
13
14use crate::error::{Error, Result};
15use crate::value::Value;
16
17/// Marker used wherever a secret would otherwise appear.
18pub const SECRET_MARKER: &str = "***SECRET***";
19
20/// Convert an evaluated Active value into an RPC value.
21///
22/// Secrets and tainted blobs collapse to [`Value::Secret`]: the RPC layer never
23/// carries secret material, only the fact that something is secret.
24pub fn from_active_value(value: &active_core::Value) -> Value {
25    use active_core::Value as ActiveValue;
26    match value {
27        ActiveValue::Null => Value::Null,
28        ActiveValue::Bool(flag) => Value::Bool(*flag),
29        ActiveValue::Int(number) => Value::Int(*number),
30        ActiveValue::Float(number) => Value::Float(*number),
31        ActiveValue::String(text) => Value::Text(text.clone()),
32        ActiveValue::DurationMs(ms) => Value::Map(
33            [("ms".to_string(), Value::Int(*ms as i64))]
34                .into_iter()
35                .collect(),
36        ),
37        ActiveValue::Quantity(amount, unit) => Value::Map(
38            [
39                ("value".to_string(), Value::Float(*amount)),
40                ("unit".to_string(), Value::Text(unit.suffix())),
41            ]
42            .into_iter()
43            .collect(),
44        ),
45        ActiveValue::Array(items) => Value::List(items.iter().map(from_active_value).collect()),
46        ActiveValue::Table(entries) => Value::Map(
47            entries
48                .iter()
49                .map(|(key, value)| (key.clone(), from_active_value(value)))
50                .collect(),
51        ),
52        ActiveValue::Secret(_) => Value::Secret,
53    }
54}
55
56/// Redact labels that name nodes in the secret cone.
57///
58/// A redacting capability still learns that a path is tainted, but not which
59/// path or how many: the answer collapses to a single marker.
60pub fn redact_cone_labels(labels: Vec<String>, tainted: &BTreeSet<String>) -> Vec<String> {
61    if labels.iter().any(|label| tainted.contains(label)) {
62        vec![SECRET_MARKER.to_string()]
63    } else {
64        labels
65    }
66}
67
68/// Redact a value tree that may contain secret markers because a path is
69/// tainted (used when a capability is redacting but a provider already built
70/// labels).
71pub fn redact_value(value: Value) -> Value {
72    match value {
73        Value::Secret => Value::Secret,
74        Value::List(items) => Value::List(items.into_iter().map(redact_value).collect()),
75        Value::Map(entries) => Value::Map(
76            entries
77                .into_iter()
78                .map(|(key, value)| (key, redact_value(value)))
79                .collect(),
80        ),
81        Value::Text(text) if text.contains(SECRET_MARKER) => Value::Secret,
82        other => other,
83    }
84}
85
86/// Validate an outgoing module payload before it is handed to a transport or a
87/// tool.
88pub fn check_module_payload(bytes: &[u8]) -> Result<()> {
89    active_infinity::validate(bytes)
90        .map(|_| ())
91        .map_err(|error| Error::Payload(error.code().to_string()))
92}
93
94/// Ensure a response carries no secret material.
95///
96/// The RPC value model cannot represent secret bytes, but a provider might hand
97/// back text that contains a marker or a module that a caller does not expect to
98/// be secret-bearing. Modules are checked for structural secrets before they go
99/// out; a module with secrets is allowed only inside a redacting capability,
100/// which the provider already enforces.
101pub fn assert_no_secret_material(value: &Value) -> Result<()> {
102    match value {
103        Value::Secret => Err(Error::Provider {
104            code: "rpc/secret-material".to_string(),
105        }),
106        Value::List(items) => items.iter().try_for_each(assert_no_secret_material),
107        Value::Map(entries) => entries.values().try_for_each(assert_no_secret_material),
108        _ => Ok(()),
109    }
110}