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
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
//! A link is a machine's grant (ACCESS-002 R1).
//!
//! A link is a resource placed under the network, between an exporting and
//! an importing machine. Opening it is deliberate and asks Lys through the
//! live seam. Nothing crosses without a grant on both ends: the exporting
//! machine's pass must hold `link_export` on the link resource and
//! the importing machine's pass `link_import`, both decided offline from the
//! passes, and a message refused on either end is refused by name saying
//! which end. The handshake that keys the link lives in
//! [`crate::link_handshake`].
use crate::auth_lys::{
AccessAction, AccessDecisionError, AccessResource, AdmittedLysPass, DeliberateDecision,
GrantReceipt, LysDeliberateConsumer, ResourceKind, SubjectBinding, SubjectBindingError,
decide_deliberate,
};
use crate::link_handshake::{LocalMachine, MachineIdentity};
use std::collections::BTreeMap;
#[path = "link_route_call.rs"]
pub mod calls;
#[path = "link_certificate.rs"]
pub mod certificate;
#[path = "link_federated.rs"]
pub mod federated;
mod life;
pub use life::LinkLifeCounters;
pub(crate) use life::{CloseLinks, LinkLife, LinkRegistry};
/// The carrier event that ended a keyed link.
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum LinkEnd {
/// The remote carrier disappeared.
#[error("peer_gone")]
PeerGone,
/// The admitted pass lost its grant.
#[error("revoked")]
Revoked,
/// The link was ended by a named refusal.
#[error("refused: {0}")]
Refused(String),
/// Another link took this link's place.
#[error("replaced")]
Replaced,
/// The owner shut the link down.
#[error("shutdown")]
Shutdown,
}
/// The two grants a crossing needs, one per end.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum LinkAction {
/// Held by the exporting machine on the link resource.
Export,
/// Held by the importing machine on the link resource.
Import,
}
impl LinkAction {
/// The schema action this end's grant names.
#[must_use]
pub const fn access_action(self) -> AccessAction {
match self {
Self::Export => AccessAction::LinkExport,
Self::Import => AccessAction::LinkImport,
}
}
/// The other end of the same link.
#[must_use]
pub const fn other(self) -> Self {
match self {
Self::Export => Self::Import,
Self::Import => Self::Export,
}
}
}
impl From<liminal::protocol::LinkEnd> for LinkAction {
fn from(end: liminal::protocol::LinkEnd) -> Self {
match end {
liminal::protocol::LinkEnd::Export => Self::Export,
liminal::protocol::LinkEnd::Import => Self::Import,
}
}
}
/// This liminal as a machine Lys knows (ACCESS-002 R1): the identity its
/// links are keyed from, its own pass's link rights, and the machine keys
/// Lys recorded for the holders that may open links to it.
///
/// The recorded keys are the operator's mirror of Lys's directory
/// (`[auth.lys.machine] machines`) until liminal reads the directory itself;
/// a holder with no recorded key cannot open a link, and an offered key that
/// is not the recorded one is refused by name.
#[derive(Debug)]
pub struct LinkHost {
local: LocalMachine,
rights: Box<dyn LinkRights>,
recorded: BTreeMap<String, MachineIdentity>,
peers: Option<crate::config::peers::PeersConfig>,
service_key: Option<[u8; 32]>,
certificate_standing: Option<std::sync::Arc<certificate::standing::Standing>>,
far: Option<std::sync::Arc<federated::FarTopology>>,
executor: std::sync::OnceLock<std::sync::Weak<dyn calls::CallExecutor>>,
}
impl LinkHost {
/// This machine, its own link rights, and the recorded keys by holder.
#[must_use]
pub const fn new(
local: LocalMachine,
rights: Box<dyn LinkRights>,
recorded: BTreeMap<String, MachineIdentity>,
) -> Self {
Self {
local,
rights,
recorded,
peers: None,
service_key: None,
certificate_standing: None,
far: None,
executor: std::sync::OnceLock::new(),
}
}
/// The identity this machine's links are keyed from.
#[must_use]
pub const fn local(&self) -> &LocalMachine {
&self.local
}
/// This machine's own pass answering its end of every crossing.
#[must_use]
pub fn rights(&self) -> &dyn LinkRights {
self.rights.as_ref()
}
/// The machine key Lys recorded for `holder`, if one is recorded.
#[must_use]
pub fn recorded_machine(&self, holder: &str) -> Option<MachineIdentity> {
self.recorded.get(holder).copied()
}
/// Installs the service-key trust anchor and explicit device-link bounds.
///
/// # Errors
/// A configured allocation bound or subject prefix is invalid.
pub fn with_peer_security(
mut self,
peers: crate::config::peers::PeersConfig,
service_key: [u8; 32],
) -> Result<Self, &'static str> {
peers.validate()?;
let verifying = ed25519_dalek::VerifyingKey::from_bytes(&service_key)
.map_err(|_| "certificate_service_key_refused")?;
if verifying.is_weak() {
return Err("certificate_service_key_refused");
}
self.far = Some(std::sync::Arc::new(federated::FarTopology::new(
federated::ProjectionLimits {
bytes: peers.max_topology_bytes,
stations: peers.max_stations,
peers: peers.max_peers,
},
)));
self.peers = Some(peers);
self.service_key = Some(service_key);
Ok(self)
}
/// Configured bounds; absence preserves the original stream-only path.
#[must_use]
pub const fn peers(&self) -> Option<&crate::config::peers::PeersConfig> {
self.peers.as_ref()
}
/// The generation-bound far index shared by all views.
#[must_use]
pub const fn far(&self) -> Option<&std::sync::Arc<federated::FarTopology>> {
self.far.as_ref()
}
/// The service key fetched from the issuer API, never the pass JWKS.
#[must_use]
pub const fn service_key(&self) -> Option<&[u8; 32]> {
self.service_key.as_ref()
}
/// The issuer's existing certified-agent route supplies current standing.
#[must_use]
pub const fn standing(&self) -> Option<&std::sync::Arc<certificate::standing::Standing>> {
self.certificate_standing.as_ref()
}
/// Installs the issuer's standing verifier built from this host's pass adapter.
#[must_use]
pub fn with_standing(
mut self,
standing: std::sync::Arc<certificate::standing::Standing>,
) -> Self {
self.certificate_standing = Some(standing);
self
}
/// Installs the full far connection executor once, after its supervisor exists.
///
/// # Errors
/// A second executor would split the ownership of pending calls.
pub fn install_call_executor(
&self,
executor: &std::sync::Arc<dyn calls::CallExecutor>,
) -> Result<(), calls::CallError> {
self.executor
.set(std::sync::Arc::downgrade(executor))
.map_err(|_| calls::CallError::Unavailable)
}
/// The full far admission route, when its startup hook has installed it.
#[must_use]
pub fn call_executor(&self) -> Option<std::sync::Arc<dyn calls::CallExecutor>> {
self.executor.get().and_then(std::sync::Weak::upgrade)
}
}
/// A machine's pass answering, offline, whether it holds a link grant on a
/// bound link. The real answer comes from the verified pass; a fixture
/// answers from a table.
pub trait LinkRights: Send + Sync + std::fmt::Debug {
/// Whether this machine's pass holds `action` on the bound link at `now`.
///
/// # Errors
/// The one refusal shape with its cause.
fn holds(
&self,
action: LinkAction,
subject: &SubjectBinding,
now: u64,
) -> Result<GrantReceipt, AccessDecisionError>;
}
/// A link between two machines under one network.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Link {
network_id: String,
id: String,
exporter: MachineIdentity,
importer: MachineIdentity,
}
/// Why a link was not described.
#[derive(Debug, PartialEq, Eq)]
pub enum LinkError {
/// An empty or whitespace-bearing identifier.
Identifier(&'static str),
/// The same machine on both ends.
SameMachine,
}
impl std::fmt::Display for LinkError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Identifier(member) => write!(formatter, "link {member} is not an identifier"),
Self::SameMachine => formatter.write_str("a link joins two different machines"),
}
}
}
impl std::error::Error for LinkError {}
/// Why a message did not cross, naming the end that refused it.
#[derive(Debug, PartialEq, Eq)]
pub enum LinkRefusal {
/// The subject could not be bound (empty, whitespace, or a pattern).
Binding(SubjectBindingError),
/// The exporting machine's pass holds no reaching `link_export`.
Export(AccessDecisionError),
/// The importing machine's pass holds no reaching `link_import`.
Import(AccessDecisionError),
}
impl std::fmt::Display for LinkRefusal {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Binding(SubjectBindingError(member)) => {
write!(formatter, "crossing refused: subject {member}")
}
Self::Export(error) => write!(formatter, "crossing refused at export: {error}"),
Self::Import(error) => write!(formatter, "crossing refused at import: {error}"),
}
}
}
impl std::error::Error for LinkRefusal {}
/// The two receipts a crossing was decided on.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Crossing {
/// The exporting machine's grant.
pub export: GrantReceipt,
/// The importing machine's grant.
pub import: GrantReceipt,
}
impl Link {
/// Describes a link under `network_id` between two different machines.
///
/// # Errors
/// An identifier that is empty or carries whitespace, or one machine on
/// both ends.
pub fn new(
network_id: String,
id: String,
exporter: MachineIdentity,
importer: MachineIdentity,
) -> Result<Self, LinkError> {
for (member, value) in [("network", &network_id), ("id", &id)] {
if value.is_empty() || value.chars().any(char::is_whitespace) {
return Err(LinkError::Identifier(member));
}
}
if exporter == importer {
return Err(LinkError::SameMachine);
}
Ok(Self {
network_id,
id,
exporter,
importer,
})
}
/// The network the link is placed under.
#[must_use]
pub fn network_id(&self) -> &str {
&self.network_id
}
/// The link as a placed resource of kind `link`.
#[must_use]
pub fn resource(&self) -> AccessResource {
AccessResource {
kind: ResourceKind::Link,
id: self.id.clone(),
}
}
/// The machine messages leave.
#[must_use]
pub const fn exporter(&self) -> &MachineIdentity {
&self.exporter
}
/// The machine messages arrive at.
#[must_use]
pub const fn importer(&self) -> &MachineIdentity {
&self.importer
}
/// Asks Lys, live, whether this end may open the link: the exporting
/// end asks for `link_export` on the link, the importing end for
/// `link_import`. Never decided from the pass alone; the seam's absence is
/// the `deliberate_unavailable` refusal.
///
/// # Errors
/// The one refusal shape with its cause.
pub fn open<C: LysDeliberateConsumer>(
&self,
end: LinkAction,
consumer: &C,
pass: &AdmittedLysPass<C::Pass>,
now: u64,
) -> Result<DeliberateDecision, AccessDecisionError> {
decide_deliberate(consumer, pass, &self.resource(), end.access_action(), now)
}
/// Decides one message crossing on this link resource: the exporter's pass first,
/// then the importer's, both offline; either refusal names its end.
///
/// # Errors
/// The subject could not be bound, or an end holds no reaching grant.
pub fn crossing(
&self,
exporter: &dyn LinkRights,
importer: &dyn LinkRights,
subject: &str,
now: u64,
) -> Result<Crossing, LinkRefusal> {
let bound =
SubjectBinding::link(self.network_id.clone(), self.id.clone(), subject.to_owned())
.map_err(LinkRefusal::Binding)?;
let export = exporter
.holds(LinkAction::Export, &bound, now)
.map_err(LinkRefusal::Export)?;
let import = importer
.holds(LinkAction::Import, &bound, now)
.map_err(LinkRefusal::Import)?;
Ok(Crossing { export, import })
}
}