Skip to main content

scosh_core/
bootstrap.rs

1//! Semantic bootstrap boundary for host-owned SSH adapters.
2//!
3//! The core verifies the binding and consumes the capability. It neither
4//! invokes an SSH executable nor accepts private-key bytes or wire frames.
5
6use std::{
7    fmt,
8    time::{Duration, Instant},
9};
10
11use crate::errors::SdkError;
12
13const CAPABILITY_LEN: usize = 32;
14const SPKI_DIGEST_LEN: usize = 32;
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
17pub enum BootstrapPurpose {
18    Attach,
19    Recover,
20}
21
22/// An opaque process owner. The value is intentionally not serializable.
23#[derive(Clone, Copy, Eq, Hash, PartialEq)]
24pub struct ProcessOwner([u8; 16]);
25
26impl fmt::Debug for ProcessOwner {
27    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
28        f.write_str("ProcessOwner(<opaque>)")
29    }
30}
31
32impl ProcessOwner {
33    /// Create a fresh owner nonce for one live client process.
34    pub fn generate() -> Result<Self, SdkError> {
35        let mut bytes = [0_u8; 16];
36        getrandom::fill(&mut bytes).map_err(|_| SdkError::Authentication)?;
37        Ok(Self(bytes))
38    }
39
40    pub(crate) const fn from_test_bytes(bytes: [u8; 16]) -> Self {
41        Self(bytes)
42    }
43}
44
45/// A verified data-plane identity. Only a host adapter that completed its
46/// OpenSSH host-key check may construct one through the crate-private helper.
47#[derive(Clone, Copy, Eq, PartialEq)]
48pub struct VerifiedServer([u8; SPKI_DIGEST_LEN]);
49
50impl fmt::Debug for VerifiedServer {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52        f.write_str("VerifiedServer(<spki-redacted>)")
53    }
54}
55
56impl VerifiedServer {
57    pub(crate) const fn from_test_digest(digest: [u8; SPKI_DIGEST_LEN]) -> Self {
58        Self(digest)
59    }
60}
61
62#[derive(Clone, Copy, Eq, PartialEq)]
63pub struct BootstrapCapability([u8; CAPABILITY_LEN]);
64
65impl fmt::Debug for BootstrapCapability {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        f.write_str("BootstrapCapability(<redacted>)")
68    }
69}
70
71impl BootstrapCapability {
72    pub(crate) const fn from_test_bytes(bytes: [u8; CAPABILITY_LEN]) -> Self {
73        Self(bytes)
74    }
75}
76
77/// Result returned by the host-owned SSH adapter after authentication and
78/// strict host verification. No bearer material is exposed to the public API.
79#[derive(Clone, Copy, Debug, Eq, PartialEq)]
80pub struct BootstrapGrant {
81    owner: ProcessOwner,
82    epoch: u64,
83    purpose: BootstrapPurpose,
84    server: VerifiedServer,
85    capability: BootstrapCapability,
86    expires_at: Instant,
87}
88
89impl BootstrapGrant {
90    pub(crate) const fn from_verified(
91        owner: ProcessOwner,
92        epoch: u64,
93        purpose: BootstrapPurpose,
94        server: VerifiedServer,
95        capability: BootstrapCapability,
96        expires_at: Instant,
97    ) -> Self {
98        Self {
99            owner,
100            epoch,
101            purpose,
102            server,
103            capability,
104            expires_at,
105        }
106    }
107}
108
109/// One-use gate for a verified bootstrap result.
110#[derive(Clone, Copy, Debug, Eq, PartialEq)]
111pub struct BootstrapGate {
112    owner: ProcessOwner,
113    epoch: u64,
114    purpose: BootstrapPurpose,
115    expected_server: VerifiedServer,
116    consumed: bool,
117    zero_rtt: bool,
118}
119
120impl BootstrapGate {
121    pub(crate) const fn new(
122        owner: ProcessOwner,
123        epoch: u64,
124        purpose: BootstrapPurpose,
125        expected_server: VerifiedServer,
126    ) -> Self {
127        Self {
128            owner,
129            epoch,
130            purpose,
131            expected_server,
132            consumed: false,
133            zero_rtt: false,
134        }
135    }
136
137    /// QUIC early data is deliberately disabled for capability delivery.
138    pub const fn zero_rtt_enabled(self) -> bool {
139        self.zero_rtt
140    }
141
142    pub fn consume(&mut self, grant: BootstrapGrant, now: Instant) -> Result<(), SdkError> {
143        if self.consumed {
144            return Err(SdkError::CapabilityReplayed);
145        }
146        if grant.expires_at <= now {
147            return Err(SdkError::CapabilityExpired);
148        }
149        if grant.owner != self.owner || grant.epoch != self.epoch || grant.purpose != self.purpose {
150            return Err(SdkError::CapabilityBindingMismatch);
151        }
152        if grant.server != self.expected_server {
153            return Err(SdkError::HostKeyRejected);
154        }
155        // Keep the capability in the core-owned gate only for the duration of
156        // this check. The transport adapter receives no raw bytes here.
157        let _ = grant.capability;
158        self.consumed = true;
159        Ok(())
160    }
161}
162
163/// Bounded semantic request sent to a host bootstrap adapter.
164#[derive(Clone, Copy, Debug, Eq, PartialEq)]
165pub struct BootstrapRequest {
166    pub purpose: BootstrapPurpose,
167    pub epoch: u64,
168    pub timeout: Duration,
169}
170
171impl BootstrapRequest {
172    pub const fn new(purpose: BootstrapPurpose, epoch: u64, timeout: Duration) -> Self {
173        Self {
174            purpose,
175            epoch,
176            timeout,
177        }
178    }
179}
180
181/// Deterministic constructors used by conformance tests. Hidden from normal
182/// API documentation; production adapters receive grants from their host
183/// bootstrap implementation instead.
184#[doc(hidden)]
185pub mod test_support {
186    use super::*;
187
188    pub fn owner(value: u8) -> ProcessOwner {
189        ProcessOwner::from_test_bytes([value; 16])
190    }
191
192    pub fn server(value: u8) -> VerifiedServer {
193        VerifiedServer::from_test_digest([value; SPKI_DIGEST_LEN])
194    }
195
196    pub fn grant(
197        owner: ProcessOwner,
198        epoch: u64,
199        purpose: BootstrapPurpose,
200        server: VerifiedServer,
201        value: u8,
202        expires_at: Instant,
203    ) -> BootstrapGrant {
204        BootstrapGrant::from_verified(
205            owner,
206            epoch,
207            purpose,
208            server,
209            BootstrapCapability::from_test_bytes([value; CAPABILITY_LEN]),
210            expires_at,
211        )
212    }
213
214    pub fn gate(
215        owner: ProcessOwner,
216        epoch: u64,
217        purpose: BootstrapPurpose,
218        server: VerifiedServer,
219    ) -> BootstrapGate {
220        BootstrapGate::new(owner, epoch, purpose, server)
221    }
222}