1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//! Explicit bounds for certificate admission and far service projections.
use std::num::{NonZeroU64, NonZeroUsize};
use std::path::PathBuf;
/// Peer security is opt-in so recorded-key configurations retain their shape.
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PeersConfig {
/// Concrete subject prefixes whose service contracts and calls may cross.
/// The pass must still grant every act; this list adds no authority.
pub service_prefixes: Vec<String>,
/// The issued capability certificate, DER, read before each open so rotation
/// needs no credential copied into a configuration file.
pub certificate_path: Option<PathBuf>,
/// Largest certificate accepted before parsing or allocating its claims.
/// This confines a certified peer's X.509 parsing cost.
pub max_certificate_bytes: NonZeroUsize,
/// Largest response from the unauthenticated service-key route.
/// This bounds allocation even if the configured issuer is compromised.
pub max_key_response_bytes: NonZeroUsize,
/// Largest signed proof header accepted before hex/COSE decoding.
/// This bounds parser cost independently of certificate DER.
pub max_proof_bytes: NonZeroUsize,
/// Largest response to the single certificate-standing pass request.
/// A compromised issuer cannot allocate an unbounded token response.
pub max_pass_response_bytes: NonZeroUsize,
/// Largest current certificate-log view returned by the signed read.
/// This bounds retained history while checking one exact withdrawal.
pub max_certificate_status_bytes: NonZeroUsize,
/// Largest signing-time skew accepted for the nonce-bearing proof.
/// At most Lys's own sixty-second request-signature window.
pub proof_window_ms: NonZeroU64,
/// Longest fresh pass accepted as certificate-standing evidence.
/// This bounds withdrawal exposure when the grant feed cannot name certificates.
pub max_certificate_pass_lifetime_secs: NonZeroU64,
/// Largest complete far topology snapshot before JSON decoding.
/// A peer cannot exhaust this exchange's heap with discovery metadata.
pub max_topology_bytes: NonZeroUsize,
/// Most stations retained from one snapshot, including unexported stations.
/// Filtering cannot hide the cost of parsing a malicious discovery record.
pub max_stations: NonZeroUsize,
/// Most simultaneous discovered peers, so discovery memory is bounded.
pub max_peers: NonZeroUsize,
/// Most queued or in-flight calls per link; checked before dispatch.
/// A slow far worker must not consume unbounded local reservations.
pub max_pending_calls: NonZeroUsize,
/// Most bytes in a call or reply, including its signed caller credential.
/// This caps retained call data independently of the pending-entry count.
pub max_call_bytes: NonZeroUsize,
}
impl PeersConfig {
/// Checks the configured allocation bounds against the transport ceiling.
///
/// # Errors
/// A certificate/call cannot fit in a single sealed Noise message, or an
/// optional certificate path is empty.
pub fn validate(&self) -> Result<(), &'static str> {
let ceiling =
crate::link_handshake::NOISE_MAX_MESSAGE - crate::link_handshake::NOISE_OVERHEAD;
if self.max_certificate_bytes.get() > ceiling || self.max_call_bytes.get() > ceiling {
return Err("peer_bound_exceeds_noise_frame");
}
if self.proof_window_ms.get() > 60_000 {
return Err("certificate_proof_window_exceeds_issuer");
}
if self
.certificate_path
.as_ref()
.is_some_and(|path| path.as_os_str().is_empty())
{
return Err("certificate_path_empty");
}
for prefix in &self.service_prefixes {
crate::auth_lys::SubjectBinding::new("wire".into(), prefix.clone(), prefix.clone())
.map_err(|_| "peer_service_prefix_invalid")?;
if prefix == "liminal"
|| prefix.starts_with("liminal.")
|| prefix == "console"
|| prefix.starts_with("console.")
{
return Err("peer_service_prefix_reserved");
}
}
Ok(())
}
}
/// Collects peer configuration errors without network access or credentials.
pub(crate) fn collect_errors(config: &super::types::ServerConfig, errors: &mut Vec<String>) {
let Some(lys) = config.auth.as_ref().and_then(|auth| auth.lys.as_ref()) else {
return;
};
let Some(machine) = &lys.machine else {
return;
};
let Some(peers) = &machine.carrier_choice.peers else {
return;
};
if let Err(error) = peers.validate() {
errors.push(format!("auth.lys.machine.carrier_choice.peers: {error}"));
}
if !config.listen_address.ip().is_loopback()
|| config
.extra_listen_addresses
.iter()
.any(|address| !address.ip().is_loopback())
{
errors.push("own-device client listeners must be loopback; peers use Noise links".into());
}
if config
.websocket
.as_ref()
.is_some_and(|websocket| !websocket.listen_address.ip().is_loopback())
{
errors.push("own-device websocket client listener must be loopback".into());
}
if lys
.api_url
.as_deref()
.is_none_or(|base| crate::link::certificate::service_key::endpoint(base).is_err())
{
errors.push(
"auth.lys.api_url: peer certificates require a trusted service-key API base".into(),
);
}
if !lys.live {
errors.push(
"auth.lys.live: peer certificate standing requires the issuer's live route".into(),
);
}
if lys
.grant_stream
.as_ref()
.is_none_or(|stream| !stream.enforce_revocation)
{
errors.push("auth.lys.grant_stream: own-device peers require enforce_revocation".into());
}
if peers.certificate_path.is_some() && machine.agent_id.is_empty() {
errors.push(
"auth.lys.machine.agent_id: a certificate offer must name its certified agent".into(),
);
}
}