Skip to content
6 changes: 6 additions & 0 deletions common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3408,6 +3408,12 @@ pub struct TufArtifactMeta {

/// The size of the artifact in bytes.
pub size: u64,

/// Contents of the `SIGN` field of a Hubris archive caboose, i.e.,
/// an identifier for the set of valid signing keys. Currently only
/// applicable to RoT image and bootloader artifacts, where it will
/// be an LPC55 Root Key Table Hash (RKTH).
pub sign: Option<Vec<u8>>,
}

/// Data about a successful TUF repo import into Nexus.
Expand Down
3 changes: 2 additions & 1 deletion nexus/db-model/src/schema_versions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ use std::{collections::BTreeMap, sync::LazyLock};
///
/// This must be updated when you change the database schema. Refer to
/// schema/crdb/README.adoc in the root of this repository for details.
pub const SCHEMA_VERSION: Version = Version::new(173, 0, 0);
pub const SCHEMA_VERSION: Version = Version::new(174, 0, 0);

/// List of all past database schema versions, in *reverse* order
///
Expand All @@ -28,6 +28,7 @@ static KNOWN_VERSIONS: LazyLock<Vec<KnownVersion>> = LazyLock::new(|| {
// | leaving the first copy as an example for the next person.
// v
// KnownVersion::new(next_int, "unique-dirname-with-the-sql-files"),
KnownVersion::new(174, "add-tuf-rot-by-sign"),
KnownVersion::new(173, "inv-internal-dns"),
KnownVersion::new(172, "add-zones-with-mupdate-override"),
KnownVersion::new(171, "inv-clear-mupdate-override"),
Expand Down
5 changes: 5 additions & 0 deletions nexus/db-model/src/tuf_repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ pub struct TufArtifact {
pub sha256: ArtifactHash,
artifact_size: i64,
pub generation_added: Generation,
pub sign: Option<Vec<u8>>,
}

impl TufArtifact {
Expand All @@ -174,6 +175,7 @@ impl TufArtifact {
sha256: ArtifactHash,
artifact_size: u64,
generation_added: external::Generation,
sign: Option<Vec<u8>>,
) -> Self {
Self {
id: TypedUuid::new_v4().into(),
Expand All @@ -184,6 +186,7 @@ impl TufArtifact {
sha256,
artifact_size: artifact_size as i64,
generation_added: generation_added.into(),
sign,
}
}

Expand All @@ -202,6 +205,7 @@ impl TufArtifact {
artifact.hash.into(),
artifact.size,
generation_added,
artifact.sign,
)
}

Expand All @@ -215,6 +219,7 @@ impl TufArtifact {
},
hash: self.sha256.into(),
size: self.artifact_size as u64,
sign: self.sign,
}
}

Expand Down
3 changes: 3 additions & 0 deletions nexus/db-queries/src/db/datastore/deployment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3236,6 +3236,7 @@ mod tests {
},
hash: ZONE_ARTIFACT_HASH_1,
size: 0,
sign: None,
},
TufArtifactMeta {
id: ArtifactId {
Expand All @@ -3245,6 +3246,7 @@ mod tests {
},
hash: HOST_ARTIFACT_HASH_1,
size: 0,
sign: None,
},
TufArtifactMeta {
id: ArtifactId {
Expand All @@ -3254,6 +3256,7 @@ mod tests {
},
hash: HOST_ARTIFACT_HASH_2,
size: 0,
sign: None,
},
],
},
Expand Down
1 change: 1 addition & 0 deletions nexus/db-queries/src/db/datastore/target_release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ mod test {
},
hash,
size: 0,
sign: None,
}],
},
)
Expand Down
1 change: 1 addition & 0 deletions nexus/db-schema/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1415,6 +1415,7 @@ table! {
sha256 -> Text,
artifact_size -> Int8,
generation_added -> Int8,
sign -> Nullable<Binary>,
}
}

Expand Down
3 changes: 2 additions & 1 deletion nexus/reconfigurator/planning/src/mgs_updates/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -661,7 +661,8 @@ mod test {
kind: kind.into(),
},
hash,
size: 0, // unused here
size: 0, // unused here
sign: None, // unused here
}
}

Expand Down
1 change: 1 addition & 0 deletions nexus/reconfigurator/planning/src/planner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5512,6 +5512,7 @@ pub(crate) mod test {
},
hash: ArtifactHash([0; 32]),
size: 0,
sign: None,
}
};
}
Expand Down
10 changes: 10 additions & 0 deletions openapi/nexus.json
Original file line number Diff line number Diff line change
Expand Up @@ -25384,6 +25384,16 @@
}
]
},
"sign": {
"nullable": true,
"description": "Sign (root key hash table) hash of a signed image. Only applicable to RoT or RoT bootloader artifacts.",
"type": "array",
"items": {
"type": "integer",
"format": "uint8",
"minimum": 0
}
},
"size": {
"description": "The size of the artifact in bytes.",
"type": "integer",
Expand Down
1 change: 1 addition & 0 deletions schema/crdb/add-tuf-rot-by-sign/up1.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE omicron.public.tuf_artifact ADD COLUMN IF NOT EXISTS sign BYTES;
5 changes: 4 additions & 1 deletion schema/crdb/dbinit.sql
Original file line number Diff line number Diff line change
Expand Up @@ -2513,6 +2513,9 @@ CREATE TABLE IF NOT EXISTS omicron.public.tuf_artifact (
-- The generation number this artifact was added for.
generation_added INT8 NOT NULL,

-- Sign (root key hash table) hash of a signed RoT or RoT bootloader image.
sign BYTES, -- nullable

CONSTRAINT unique_name_version_kind UNIQUE (name, version, kind)
);

Expand Down Expand Up @@ -6347,7 +6350,7 @@ INSERT INTO omicron.public.db_metadata (
version,
target_version
) VALUES
(TRUE, NOW(), NOW(), '173.0.0', NULL)
(TRUE, NOW(), NOW(), '174.0.0', NULL)
ON CONFLICT DO NOTHING;

COMMIT;
55 changes: 45 additions & 10 deletions update-common/src/artifacts/update_plan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ impl<'a> UpdatePlanBuilder<'a> {
artifact_id,
data,
artifact_kind.into(),
None,
self.log,
)?;

Expand Down Expand Up @@ -419,11 +420,20 @@ impl<'a> UpdatePlanBuilder<'a> {
let (artifact_id, bootloader_caboose) =
read_hubris_caboose_from_archive(artifact_id, data.clone())?;

let sign = match bootloader_caboose.sign {
Some(sign) => sign,
None => {
return Err(RepositoryError::MissingHubrisCabooseSign(
artifact_id,
));
}
};

// We restrict the bootloader to exactly one entry per (kind, signature)
match self.rot_by_sign.entry(RotSignData {
kind: artifact_kind,
sign: bootloader_caboose.sign.expect("required SIGN in caboose"),
}) {
match self
.rot_by_sign
.entry(RotSignData { kind: artifact_kind, sign: sign.clone() })
{
hash_map::Entry::Occupied(_) => {
return Err(RepositoryError::DuplicateBoardEntry {
board: bootloader_caboose.board,
Expand Down Expand Up @@ -454,6 +464,7 @@ impl<'a> UpdatePlanBuilder<'a> {
artifact_id,
data,
bootloader_kind,
Some(sign),
self.log,
)?;

Expand Down Expand Up @@ -555,11 +566,17 @@ impl<'a> UpdatePlanBuilder<'a> {
});
}

let entry_a = RotSignData {
kind: artifact_kind,
sign: image_a_caboose.sign.expect("required SIGN in caboose"),
let sign_a = match image_a_caboose.sign {
Some(sign) => sign,
None => {
return Err(RepositoryError::MissingHubrisCabooseSign(
artifact_id,
));
}
};

let entry_a = RotSignData { kind: artifact_kind, sign: sign_a.clone() };

let target_a = RotSignTarget {
id: artifact_id.clone(),
bord: image_a_caboose.board,
Expand All @@ -583,11 +600,17 @@ impl<'a> UpdatePlanBuilder<'a> {
}
};

let entry_b = RotSignData {
kind: artifact_kind,
sign: image_b_caboose.sign.expect("required SIGN in caboose"),
let sign_b = match image_b_caboose.sign {
Some(sign) => sign,
None => {
return Err(RepositoryError::MissingHubrisCabooseSign(
artifact_id,
));
}
};

let entry_b = RotSignData { kind: artifact_kind, sign: sign_b.clone() };

let target_b = RotSignTarget {
id: artifact_id.clone(),
bord: image_b_caboose.board,
Expand Down Expand Up @@ -620,12 +643,14 @@ impl<'a> UpdatePlanBuilder<'a> {
artifact_id.clone(),
rot_a_data,
rot_a_kind,
Some(sign_a),
self.log,
)?;
self.record_extracted_artifact(
artifact_id,
rot_b_data,
rot_b_kind,
Some(sign_b),
self.log,
)?;

Expand Down Expand Up @@ -668,12 +693,14 @@ impl<'a> UpdatePlanBuilder<'a> {
artifact_id.clone(),
phase_1_data,
ArtifactKind::HOST_PHASE_1,
None,
self.log,
)?;
self.record_extracted_artifact(
artifact_id,
phase_2_data,
ArtifactKind::HOST_PHASE_2,
None,
self.log,
)?;

Expand Down Expand Up @@ -725,12 +752,14 @@ impl<'a> UpdatePlanBuilder<'a> {
artifact_id.clone(),
phase_1_data,
ArtifactKind::TRAMPOLINE_PHASE_1,
None,
self.log,
)?;
self.record_extracted_artifact(
artifact_id,
phase_2_data,
ArtifactKind::TRAMPOLINE_PHASE_2,
None,
self.log,
)?;

Expand Down Expand Up @@ -761,6 +790,7 @@ impl<'a> UpdatePlanBuilder<'a> {
artifact_id,
data,
artifact_kind,
None,
self.log,
)?;

Expand Down Expand Up @@ -796,6 +826,7 @@ impl<'a> UpdatePlanBuilder<'a> {
artifact_id,
data,
KnownArtifactKind::ControlPlane.into(),
None,
self.log,
)?;
}
Expand Down Expand Up @@ -830,6 +861,7 @@ impl<'a> UpdatePlanBuilder<'a> {
artifact_id,
data,
artifact_kind,
None,
self.log,
)?;

Expand Down Expand Up @@ -994,6 +1026,7 @@ impl<'a> UpdatePlanBuilder<'a> {
artifact_id,
data,
KnownArtifactKind::Zone.into(),
None,
self.log,
)?;
Ok(())
Expand All @@ -1017,6 +1050,7 @@ impl<'a> UpdatePlanBuilder<'a> {
tuf_repo_artifact_id: ArtifactId,
data: ExtractedArtifactDataHandle,
data_kind: ArtifactKind,
sign: Option<Vec<u8>>,
log: &Logger,
) -> Result<(), RepositoryError> {
use std::collections::hash_map::Entry;
Expand Down Expand Up @@ -1059,6 +1093,7 @@ impl<'a> UpdatePlanBuilder<'a> {
id: artifacts_meta_id,
hash: data.hash(),
size: data.file_size() as u64,
sign,
});
by_hash_slot.insert(data);

Expand Down
4 changes: 4 additions & 0 deletions update-common/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,9 @@ pub enum RepositoryError {
#[error("error reading name from hubris caboose of {0:?}: non-utf8 value")]
ReadHubrisCabooseNameUtf8(ArtifactId),

#[error("missing sign from hubris caboose of {0:?}")]
MissingHubrisCabooseSign(ArtifactId),

#[error("missing artifact of kind `{0:?}`")]
MissingArtifactKind(KnownArtifactKind),

Expand Down Expand Up @@ -203,6 +206,7 @@ impl RepositoryError {
| RepositoryError::LocateTarget { .. }
| RepositoryError::TargetHashLength(_)
| RepositoryError::MissingArtifactKind(_)
| RepositoryError::MissingHubrisCabooseSign(_)
| RepositoryError::MissingTarget(_)
| RepositoryError::DuplicateHashEntry(_)
| RepositoryError::DuplicateBoardEntry { .. }
Expand Down
Loading