Skip to main content

sim_platform_ubuntu_pc/
lib.rs

1#![forbid(unsafe_code)]
2//! Ubuntu PC capsule registration and sanitized evidence contracts.
3use serde::{Deserialize, Serialize};
4use sim_platform_core::{
5    ContractProvenance, EvidenceLevel, FactPort, OpenSymbol, PlatformCard, ServiceOffer,
6    stable_digest,
7};
8pub use sim_platform_core::{
9    UbuntuArchitecture as Architecture, UbuntuProfile as UbuntuPcProfile,
10    UbuntuProfileKind as ProfileKind,
11};
12use std::{ffi::OsString, path::PathBuf};
13
14pub use sim_platform_linux as linux;
15mod compute;
16mod loader;
17pub use compute::UbuntuComputeProbe;
18pub use loader::UbuntuLoaderPort;
19mod process;
20mod sandbox;
21pub use process::UbuntuProcess;
22pub use sandbox::BwrapLauncher;
23
24/// Owned process-entry facts captured by the Ubuntu capsule.
25///
26/// Portable bootloaders consume this value and never inspect argv, the current
27/// directory, or environment variables themselves.
28#[derive(Clone, Debug, Eq, PartialEq)]
29pub struct UbuntuProcessEnvelope {
30    /// Complete process argument vector.
31    pub argv: Vec<OsString>,
32    /// Explicit working root supplied to configuration and cache policy.
33    pub work_root: PathBuf,
34    /// Explicit cache root override, when supplied by the host.
35    pub cache_root: Option<PathBuf>,
36    /// Explicit registry artifact endpoint, when supplied by the host.
37    pub registry_endpoint: Option<String>,
38    /// Whether the host admits unauthenticated non-loopback registry access.
39    pub allow_insecure_registry: bool,
40}
41
42impl UbuntuProcessEnvelope {
43    /// Captures the bounded process facts owned by this concrete capsule.
44    ///
45    /// # Errors
46    /// Returns an error when the host does not expose a current working root.
47    pub fn capture() -> std::io::Result<Self> {
48        Ok(Self {
49            argv: std::env::args_os().collect(),
50            work_root: std::env::current_dir()?,
51            cache_root: std::env::var_os("SIM_CLI_CACHE_DIR").map(PathBuf::from),
52            registry_endpoint: std::env::var_os("SIM_GIT_REGISTRY_ENDPOINT")
53                .map(|value| value.to_string_lossy().into_owned()),
54            allow_insecure_registry: std::env::var_os("SIM_GIT_REGISTRY_ALLOW_INSECURE")
55                .is_some_and(|value| !value.is_empty()),
56        })
57    }
58}
59pub const BUNDLE_DESCRIPTOR: &str = "sim.platform-bundle.toml";
60/// Ubuntu PC distribution bundle. The one row admits only this capsule.
61pub const UBUNTU_BUNDLE_MANIFEST: &str = r#"
62schema = "sim.platform-bundle/v1"
63capsule = "platform/site/ubuntu-pc"
64artifact = "sim-platform-ubuntu-pc.so"
65loader = "loader/native-v1"
66artifact_content = "sha256:ubuntu-pc-release-content"
67entry = "sim_native_abi_v1"
68"#;
69
70/// Ubuntu PC capsule card paired with [`UBUNTU_BUNDLE_MANIFEST`].
71pub const UBUNTU_CAPSULE_MANIFEST: &str = r#"
72schema = "sim.platform-capsule/v1"
73provider = "platform/site/ubuntu-pc"
74services = ["platform/monotonic", "platform/wall-clock", "platform/entropy"]
75shells = []
76loader_kinds = ["loader/native-v1", "loader/wasm-v1", "loader/source-v1", "loader/static-v1"]
77"#;
78#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
79pub struct RegisteredCard {
80    pub profile: UbuntuPcProfile,
81    pub card: PlatformCard,
82}
83
84const FACTS: &[(&str, FactPort)] = &[
85    ("wall-clock", FactPort::WallClock),
86    ("monotonic-clock", FactPort::MonotonicClock),
87    ("timer", FactPort::Timer),
88    ("entropy", FactPort::Entropy),
89    ("locale", FactPort::Locale),
90    ("timezone", FactPort::Timezone),
91    ("pressure", FactPort::LifecyclePressure),
92    ("limits", FactPort::MachineLimits),
93];
94const DESKTOP: &[&str] = &[
95    "open",
96    "share",
97    "notify",
98    "clipboard",
99    "permission",
100    "keep-awake",
101    "activation",
102];
103/// Construct the exact Card for a supported Ubuntu profile.
104///
105/// # Panics
106/// Panics only if serialization of the closed profile enum fails.
107#[must_use]
108pub fn register(profile: UbuntuPcProfile) -> RegisteredCard {
109    let kind = profile.kind;
110    let mut services = FACTS
111        .iter()
112        .map(|(name, port)| ServiceOffer {
113            service: OpenSymbol(format!("platform/{name}")),
114            port: *port,
115            evidence: EvidenceLevel::Attested,
116        })
117        .collect::<Vec<_>>();
118    services.extend(
119        ["xdg-config", "xdg-cache", "xdg-data", "xdg-state", "temp"]
120            .into_iter()
121            .map(|name| ServiceOffer {
122                service: OpenSymbol(format!("platform/mount/{name}")),
123                port: FactPort::MachineLimits,
124                evidence: EvidenceLevel::Attested,
125            }),
126    );
127    if kind == ProfileKind::Desktop {
128        services.extend(DESKTOP.iter().map(|name| ServiceOffer {
129            service: OpenSymbol(format!("platform/{name}")),
130            port: FactPort::LifecyclePressure,
131            evidence: EvidenceLevel::Attested,
132        }));
133    }
134    let profile_bytes = serde_json::to_vec(&profile).expect("closed profile serializes");
135    RegisteredCard {
136        profile,
137        card: PlatformCard {
138            schema: OpenSymbol("platform/card-v1".into()),
139            site: OpenSymbol(
140                match kind {
141                    ProfileKind::Desktop => "platform/site/ubuntu-pc-desktop",
142                    ProfileKind::Headless => "platform/site/ubuntu-pc-headless",
143                }
144                .into(),
145            ),
146            services,
147            provenance: ContractProvenance {
148                contract: OpenSymbol("contract/ubuntu-pc-v1".into()),
149                content_digest: stable_digest(&profile_bytes),
150                issuer: OpenSymbol("issuer/sim-platform".into()),
151            },
152        },
153    }
154}
155
156#[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)]
157pub struct PhysicalAttestation {
158    pub schema: String,
159    pub provider: String,
160    pub registered_capability: String,
161    pub source_content: String,
162    pub artifact_content: String,
163    pub card_content: String,
164    pub result_content: String,
165    pub source: String,
166    pub artifact: String,
167    pub card: String,
168    pub result: String,
169    pub checks: Vec<String>,
170}
171impl PhysicalAttestation {
172    /// Verify the sanitized, content-bound offline evidence envelope.
173    ///
174    /// # Errors
175    /// Refuses wrong identity, missing content identities, or leaking checks.
176    pub fn validate(&self) -> Result<(), &'static str> {
177        if self.schema != "sim.platform-physical-attestation/v1"
178            || self.provider != "ubuntu-pc"
179            || self.registered_capability != "linux-x86_64"
180        {
181            return Err("wrong attestation identity");
182        }
183        for (claimed, payload) in [
184            (&self.source_content, &self.source),
185            (&self.artifact_content, &self.artifact),
186            (&self.card_content, &self.card),
187            (&self.result_content, &self.result),
188        ] {
189            if *claimed != stable_digest(payload.as_bytes()) {
190                return Err("content identity mismatch");
191            }
192        }
193        if self.checks.len() < 4
194            || self
195                .checks
196                .iter()
197                .any(|v| v.contains('/') || v.contains('@'))
198        {
199            return Err("unsanitized or incomplete evidence");
200        }
201        Ok(())
202    }
203}