Skip to content

Commit 139c01c

Browse files
authored
Enforce sprockets measurements (#9938)
Sprockets is currently in warning only mode for measurements. This change makes sprockets reject connections if the references measurements aren't as expected. Being able to turn this on/off with a sled-agent config is still a security gap as tech port access can defeat this. It's still useful to have this as a backup option for a little while longer since a failure here will prevent the control plane from coming up. The eventual plan will to change sprockets to be always enforcing regardless of the config. When measurements are enforced we need a (relatively) easy way to allow testing of engineering builds of the SP that need the full control plane. A good example would be a change in the SP to collect sensor data in the control plane. This also adds a tool to take care of adusting the measurement manifest on the `install` dataset. This restricts testing to MUPdate cases but if you need to test reconfigurator with an engineering SP build you are better off making a full TUF repo.
1 parent 2e78900 commit 139c01c

7 files changed

Lines changed: 229 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 16 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ members = [
3737
"dev-tools/clickhouse-cluster-dev",
3838
"dev-tools/ch-dev",
3939
"dev-tools/common",
40+
"dev-tools/corpus-adjust",
4041
"dev-tools/crdb-seed",
4142
"dev-tools/db-dev",
4243
"dev-tools/downloader",
@@ -227,6 +228,7 @@ default-members = [
227228
"dev-tools/clickhouse-cluster-dev",
228229
"dev-tools/ch-dev",
229230
"dev-tools/common",
231+
"dev-tools/corpus-adjust",
230232
"dev-tools/crdb-seed",
231233
"dev-tools/db-dev",
232234
"dev-tools/downloader",

dev-tools/corpus-adjust/Cargo.toml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
[package]
2+
name = "corpus-adjust"
3+
version = "0.1.0"
4+
edition.workspace = true
5+
license = "MPL-2.0"
6+
7+
[lints]
8+
workspace = true
9+
10+
[dependencies]
11+
anyhow.workspace = true
12+
omicron-workspace-hack.workspace = true
13+
omicron-common.workspace = true
14+
tufaceous-artifact.workspace = true
15+
sha2.workspace = true
16+
serde_json.workspace = true
17+
clap.workspace = true
18+
camino.workspace = true
19+
uuid.workspace = true
20+
iddqd.workspace = true
21+
22+
[[bin]]
23+
name = "corpus-adjust"
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4+
5+
use anyhow::{Context, bail};
6+
use camino::{Utf8DirEntry, Utf8Path};
7+
use clap::{Parser, Subcommand};
8+
use iddqd::IdOrdMap;
9+
use omicron_common::update::{OmicronInstallManifest, OmicronInstallMetadata};
10+
use sha2::{Digest, Sha256};
11+
use std::fs::File;
12+
use tufaceous_artifact::ArtifactHash;
13+
use uuid::Uuid;
14+
15+
/// Return a UUID if the `DirEntry` contains a directory that parses into a UUID.
16+
fn get_uuid_dir(result: std::io::Result<Utf8DirEntry>) -> Option<Uuid> {
17+
let Ok(entry) = result else {
18+
return None;
19+
};
20+
let Ok(file_type) = entry.file_type() else {
21+
return None;
22+
};
23+
if !file_type.is_dir() {
24+
return None;
25+
}
26+
let file_name = entry.file_name();
27+
file_name.parse().ok()
28+
}
29+
30+
#[derive(Debug)]
31+
pub struct Pools {
32+
pub internal: Vec<Uuid>,
33+
pub external: Vec<Uuid>,
34+
}
35+
36+
impl Pools {
37+
pub fn read() -> anyhow::Result<Pools> {
38+
let internal = Utf8Path::new("/pool/int/")
39+
.read_dir_utf8()
40+
.context("Failed to read /pool/int")?
41+
.filter_map(get_uuid_dir)
42+
.collect();
43+
let external = Utf8Path::new("/pool/ext/")
44+
.read_dir_utf8()
45+
.context("Failed to read /pool/ext")?
46+
.filter_map(get_uuid_dir)
47+
.collect();
48+
Ok(Pools { internal, external })
49+
}
50+
}
51+
52+
#[derive(Subcommand)]
53+
enum CorpusCommand {
54+
/// Verify that all corpus files are present and accounted for
55+
/// on all disks
56+
Check,
57+
/// Regenerate the `measurements.json` file on all disks. This will
58+
/// be saved as `measurements-regenerated.json`. It is up to you to
59+
/// do the replacement!
60+
Regenerate,
61+
}
62+
63+
/// Adjust the `measurement.json` in an install dataset
64+
#[derive(Parser)]
65+
struct CorpusAdjust {
66+
#[clap(subcommand)]
67+
command: CorpusCommand,
68+
}
69+
70+
fn measurement_names(
71+
dir: &Utf8Path,
72+
) -> anyhow::Result<IdOrdMap<OmicronInstallMetadata>> {
73+
if !dir.is_dir() {
74+
bail!("Not a directory");
75+
}
76+
77+
let mut files = IdOrdMap::new();
78+
79+
for entry in dir.read_dir_utf8()? {
80+
let entry = entry?;
81+
82+
if !entry.file_name().contains("measurements.json") {
83+
continue;
84+
}
85+
86+
let mut hasher = Sha256::new();
87+
std::io::copy(&mut File::open(&entry.path())?, &mut hasher)?;
88+
89+
let meta = entry.metadata()?;
90+
91+
let entry = OmicronInstallMetadata {
92+
file_name: entry.file_name().to_owned(),
93+
file_size: meta.len(),
94+
hash: ArtifactHash(hasher.finalize().into()),
95+
};
96+
97+
files
98+
.insert_unique(entry)
99+
.map_err(|_| anyhow::anyhow!("duplicate?"))?;
100+
}
101+
102+
Ok(files)
103+
}
104+
105+
fn main() -> anyhow::Result<()> {
106+
let arg = CorpusAdjust::parse();
107+
108+
match arg.command {
109+
CorpusCommand::Check => {
110+
let pools = Pools::read()?;
111+
112+
let all = pools.internal.into_iter().map(|u| {
113+
Utf8Path::new("/pool/int")
114+
.join(format!("{u}"))
115+
.join("install")
116+
.join("measurements")
117+
});
118+
for p in all {
119+
let mut names = measurement_names(&p)?;
120+
121+
let f = p.join("measurements.json");
122+
let bytes = std::fs::read(f)?;
123+
124+
let manifest =
125+
serde_json::from_slice::<OmicronInstallManifest>(&bytes)?;
126+
127+
for f in manifest.files {
128+
if let Some(entry) = names.remove(f.file_name.as_str()) {
129+
if entry != f {
130+
bail!(
131+
"file {} in manifest does not match disk {p}",
132+
f.file_name
133+
);
134+
}
135+
} else {
136+
bail!(
137+
"file {} in manifest but not found on disk {p}",
138+
f.file_name
139+
);
140+
}
141+
}
142+
143+
if !names.is_empty() {
144+
println!("some files were not in the manifest {p}:");
145+
for n in names {
146+
println!("{}", n.file_name);
147+
}
148+
bail!("Fix your manifest");
149+
}
150+
}
151+
}
152+
CorpusCommand::Regenerate => {
153+
let pools = Pools::read()?;
154+
155+
let all = pools.internal.into_iter().map(|u| {
156+
Utf8Path::new("/pool/int")
157+
.join(format!("{u}"))
158+
.join("install")
159+
.join("measurements")
160+
});
161+
for p in all {
162+
let names = measurement_names(&p)?;
163+
164+
let f = p.join("measurements.json");
165+
let bytes = std::fs::read(f)?;
166+
167+
let manifest =
168+
serde_json::from_slice::<OmicronInstallManifest>(&bytes)?;
169+
170+
let updated = OmicronInstallManifest {
171+
source: manifest.source,
172+
files: names,
173+
};
174+
175+
let json = serde_json::to_vec(&updated)?;
176+
177+
std::fs::write(p.join("measurements-regenerated.json"), &json)?;
178+
}
179+
180+
println!("done");
181+
}
182+
}
183+
184+
Ok(())
185+
}

smf/sled-agent/gimlet-standalone/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,4 @@ if_exists = "append"
8080
resolve = { which = "ipcc" }
8181
attest = { which = "ipcc" }
8282
roots = ["/usr/share/oxide/idcerts/staging.pem", "/usr/share/oxide/idcerts/production.pem"]
83+
enforce = "Enforced"

smf/sled-agent/gimlet/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,3 +75,4 @@ if_exists = "append"
7575
resolve = { which = "ipcc" }
7676
attest = { which = "ipcc" }
7777
roots = ["/usr/share/oxide/idcerts/staging.pem", "/usr/share/oxide/idcerts/production.pem"]
78+
enforce = "Enforced"

smf/sled-agent/non-gimlet/config.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,4 @@ if_exists = "append"
125125
resolve = { which = "local", priv_key = "/opt/oxide/sled-agent/pkg/test-sprockets-auth-1.key.pem", cert_chain = "/opt/oxide/sled-agent/pkg/test-sprockets-auth-1.certlist.pem" }
126126
attest = { which = "local", priv_key = "/opt/oxide/sled-agent/pkg/test-alias-1.key.pem", cert_chain = "/opt/oxide/sled-agent/pkg/test-alias-1.certlist.pem", log = "/opt/oxide/sled-agent/pkg/sprockets-log.bin", test_corpus = [ "/opt/oxide/sled-agent/pkg/testing-measurements/corim-rot.cbor", "/opt/oxide/sled-agent/pkg/testing-measurements/corim-sp.cbor"] }
127127
roots = ["/opt/oxide/sled-agent/pkg/test-root-a.cert.pem"]
128+
enforce = "Enforced"

0 commit comments

Comments
 (0)