Skip to content
21 changes: 21 additions & 0 deletions common/src/api/external/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3357,13 +3357,22 @@ pub struct TufRepoDescription {

/// Information about the artifacts present in the repository.
pub artifacts: Vec<TufArtifactMeta>,

// Association between an artifact and its RoT signing information.
// This also covers the RoT bootloader artifacts which are signed as well.
pub rots_by_sign: Vec<TufRotBySign>,
}

impl TufRepoDescription {
/// Sorts the artifacts so that descriptions can be compared.
pub fn sort_artifacts(&mut self) {
self.artifacts.sort_by(|a, b| a.id.cmp(&b.id));
}

/// Sorts the RoTs by sign so that descriptions can be compared.
pub fn sort_rots_by_sign(&mut self) {
self.rots_by_sign.sort_by(|a, b| a.id.cmp(&b.id));
}
}

/// Metadata about a TUF repository.
Expand Down Expand Up @@ -3410,6 +3419,18 @@ pub struct TufArtifactMeta {
pub size: u64,
}

/// Mapping for RoT signing information in RoT and RoT bootloader artifacts
///
/// Found within a `TufRepoDescription`.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
pub struct TufRotBySign {
/// The artifact ID.
pub id: ArtifactId,

/// The sign (RKTH value) of the artifact.
pub sign: Vec<u8>,
}

/// Data about a successful TUF repo import into Nexus.
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[serde(rename_all = "snake_case")]
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
63 changes: 62 additions & 1 deletion nexus/db-model/src/tuf_repo.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use chrono::{DateTime, Utc};
use diesel::sql_types::{Jsonb, Text};
use diesel::{deserialize::FromSql, serialize::ToSql};
use nexus_db_schema::schema::{
tuf_artifact, tuf_repo, tuf_repo_artifact, tuf_trust_root,
tuf_artifact, tuf_repo, tuf_repo_artifact, tuf_rot_by_sign, tuf_trust_root,
};
use nexus_types::external_api::shared::TufSignedRootRole;
use nexus_types::external_api::views;
Expand Down Expand Up @@ -39,6 +39,9 @@ pub struct TufRepoDescription {

/// The artifacts.
pub artifacts: Vec<TufArtifact>,

/// The RoT and RoT booloader artifacts with sign values
pub rots_by_sign: Vec<TufRotBySign>,
}

impl TufRepoDescription {
Expand All @@ -61,6 +64,11 @@ impl TufRepoDescription {
TufArtifact::from_external(artifact, generation_added)
})
.collect(),
rots_by_sign: description
.rots_by_sign
.into_iter()
.map(|artifact| TufRotBySign::from_external(artifact))
.collect(),
}
}

Expand All @@ -73,6 +81,11 @@ impl TufRepoDescription {
.into_iter()
.map(TufArtifact::into_external)
.collect(),
rots_by_sign: self
.rots_by_sign
.into_iter()
.map(TufRotBySign::into_external)
.collect(),
}
}
}
Expand Down Expand Up @@ -154,6 +167,54 @@ impl TufRepo {
}
}

#[derive(Queryable, Insertable, Clone, Debug, Selectable, AsChangeset)]
#[diesel(table_name = tuf_rot_by_sign)]
pub struct TufRotBySign {
pub id: DbTypedUuid<TufArtifactKind>,
pub name: String,
pub version: DbArtifactVersion,
pub kind: String,
pub sign: Vec<u8>,
pub time_created: DateTime<Utc>,
// TODO-K: Do I need generation?
}

impl TufRotBySign {
/// Creates a new `TufRotBySign` ready for insertion.
pub fn new(artifact_id: ArtifactId, sign: Vec<u8>) -> Self {
Self {
id: TypedUuid::new_v4().into(),
name: artifact_id.name,
version: artifact_id.version.into(),
kind: artifact_id.kind.as_str().to_owned(),
sign,
time_created: Utc::now(),
}
}

/// Creates a new `TufRotBySign` ready for insertion from an external
/// `TufRotBySign`.
///
/// This is not implemented as a `From` impl because we insert new fields
/// as part of the process, which `From` doesn't necessarily communicate
/// and can be surprising.
pub fn from_external(artifact: external::TufRotBySign) -> Self {
Self::new(artifact.id, artifact.sign)
}

/// Converts self into [`external::TufRotBySign`].
pub fn into_external(self) -> external::TufRotBySign {
external::TufRotBySign {
id: ArtifactId {
name: self.name,
version: self.version.into(),
kind: ArtifactKind::new(self.kind),
},
sign: self.sign,
}
}
}

#[derive(Queryable, Insertable, Clone, Debug, Selectable, AsChangeset)]
#[diesel(table_name = tuf_artifact)]
pub struct TufArtifact {
Expand Down
1 change: 1 addition & 0 deletions nexus/db-queries/src/db/datastore/deployment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3256,6 +3256,7 @@ mod tests {
size: 0,
},
],
rots_by_sign: vec![],
},
)
.await
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 @@ -229,6 +229,7 @@ mod test {
hash,
size: 0,
}],
rots_by_sign: vec![],
},
)
.await
Expand Down
138 changes: 126 additions & 12 deletions nexus/db-queries/src/db/datastore/update.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ use nexus_db_errors::OptionalError;
use nexus_db_errors::{ErrorHandler, public_error_from_diesel};
use nexus_db_lookup::DbConnection;
use nexus_db_model::{
ArtifactHash, TufArtifact, TufRepo, TufRepoDescription, TufTrustRoot,
to_db_typed_uuid,
ArtifactHash, TufArtifact, TufRepo, TufRepoDescription, TufRotBySign,
TufTrustRoot, to_db_typed_uuid,
};
use omicron_common::api::external::{
self, CreateResult, DataPageParams, DeleteResult, Generation,
Expand All @@ -28,6 +28,7 @@ use omicron_common::api::external::{
use omicron_uuid_kinds::GenericUuid;
use omicron_uuid_kinds::TufRepoKind;
use omicron_uuid_kinds::TypedUuid;
use std::collections::HashSet;
use swrite::{SWrite, swrite};
use tufaceous_artifact::ArtifactVersion;
use uuid::Uuid;
Expand Down Expand Up @@ -72,6 +73,28 @@ async fn artifacts_for_repo(
.await
}

async fn rots_by_sign_for_repo(
repo_id: TypedUuid<TufRepoKind>,
conn: &async_bb8_diesel::Connection<DbConnection>,
) -> Result<Vec<TufRotBySign>, DieselError> {
use nexus_db_schema::schema::tuf_repo_rot_by_sign::dsl as tuf_repo_rot_by_sign_dsl;
use nexus_db_schema::schema::tuf_rot_by_sign::dsl as tuf_rot_by_sign_dsl;

let join_on_dsl = tuf_rot_by_sign_dsl::id
.eq(tuf_repo_rot_by_sign_dsl::tuf_rot_by_sign_id);
// Don't bother paginating because each repo should only have a few (under
// 20) artifacts.
tuf_repo_rot_by_sign_dsl::tuf_repo_rot_by_sign
.filter(
tuf_repo_rot_by_sign_dsl::tuf_repo_id
.eq(nexus_db_model::to_db_typed_uuid(repo_id)),
)
.inner_join(tuf_rot_by_sign_dsl::tuf_rot_by_sign.on(join_on_dsl))
.select(TufRotBySign::as_select())
.load_async(conn)
.await
}

impl DataStore {
/// Inserts a new TUF repository into the database.
///
Expand Down Expand Up @@ -139,7 +162,13 @@ impl DataStore {
let artifacts = artifacts_for_repo(repo.id.into(), &conn)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?;
Ok(TufRepoDescription { repo, artifacts })

let rots_by_sign =
rots_by_sign_for_repo(repo.id.into(), &conn).await.map_err(
|e| public_error_from_diesel(e, ErrorHandler::Server),
)?;

Ok(TufRepoDescription { repo, artifacts, rots_by_sign })
}

/// Returns the TUF repo description corresponding to this system version.
Expand Down Expand Up @@ -172,7 +201,13 @@ impl DataStore {
let artifacts = artifacts_for_repo(repo.id.into(), &conn)
.await
.map_err(|e| public_error_from_diesel(e, ErrorHandler::Server))?;
Ok(TufRepoDescription { repo, artifacts })

let rots_by_sign =
rots_by_sign_for_repo(repo.id.into(), &conn).await.map_err(
|e| public_error_from_diesel(e, ErrorHandler::Server),
)?;

Ok(TufRepoDescription { repo, artifacts, rots_by_sign })
}

/// Returns the list of all TUF repo artifacts known to the system.
Expand Down Expand Up @@ -313,8 +348,14 @@ async fn insert_impl(
let artifacts =
artifacts_for_repo(existing_repo.id.into(), &conn).await?;

let recorded =
TufRepoDescription { repo: existing_repo, artifacts };
let rots_by_sign =
rots_by_sign_for_repo(existing_repo.id.into(), &conn).await?;

let recorded = TufRepoDescription {
repo: existing_repo,
artifacts,
rots_by_sign,
};
return Ok(TufRepoInsertResponse {
recorded,
status: TufRepoInsertStatus::AlreadyExists,
Expand All @@ -332,9 +373,10 @@ async fn insert_impl(
};

// Since we've inserted a new repo, we also need to insert the
// corresponding artifacts.
let all_artifacts = {
// corresponding artifacts and RoTs by sign.
let (all_artifacts, all_rots_by_sign) = {
use nexus_db_schema::schema::tuf_artifact::dsl;
use nexus_db_schema::schema::tuf_rot_by_sign::dsl as rot_dsl;

// Multiple repos can have the same artifacts, so we shouldn't error
// out if we find an existing artifact. However, we should check that
Expand Down Expand Up @@ -489,15 +531,60 @@ async fn insert_impl(

// Insert new artifacts into the database.
diesel::insert_into(dsl::tuf_artifact)
.values(new_artifacts)
.values(new_artifacts.clone())
.execute_async(&conn)
.await?;
}

let rots_by_sign = desc.rots_by_sign;

let new_artifact_keys: HashSet<_> = new_artifacts
.iter()
.map(|artifact| (&artifact.name, &artifact.version, &artifact.kind))
.collect();

// Filter `rots_by_sign` based on the keys in new_artifact_keys.
let new_rots_by_sign: Vec<TufRotBySign> = rots_by_sign
.clone()
.into_iter()
.filter(|rot| {
let rot_key = (&rot.name, &rot.version, &rot.kind);
new_artifact_keys.contains(&rot_key)
})
.collect();

debug!(
log,
"inserting {} new RoTs by sign", new_rots_by_sign.len();
"new_rots_by_sign" => ?new_rots_by_sign,
);

if !new_rots_by_sign.is_empty() {
// Insert new corresponding rots_by_sign into the database.
diesel::insert_into(rot_dsl::tuf_rot_by_sign)
.values(new_rots_by_sign)
.execute_async(&conn)
.await?;
}

all_artifacts
// For each artifact in all_artifacts, we'll want a corresponding rot_by_sign
let all_artifact_keys: HashSet<_> = all_artifacts
.iter()
.map(|artifact| (&artifact.name, &artifact.version, &artifact.kind))
.collect();

let all_rots_by_sign: Vec<TufRotBySign> = rots_by_sign
.into_iter()
.filter(|rot| {
let rot_key = (&rot.name, &rot.version, &rot.kind);
all_artifact_keys.contains(&rot_key)
})
.collect();

(all_artifacts, all_rots_by_sign)
};

// Finally, insert all the associations into the tuf_repo_artifact table.
// Insert all the associations into the tuf_repo_artifact table.
{
use nexus_db_schema::schema::tuf_repo_artifact::dsl;

Expand All @@ -520,7 +607,34 @@ async fn insert_impl(
.await?;
}

let recorded = TufRepoDescription { repo, artifacts: all_artifacts };
// Finally, Insert all the associations into the tuf_repo_rot_by_sign table.
{
use nexus_db_schema::schema::tuf_repo_rot_by_sign::dsl;

let mut values = Vec::new();
for rot_by_sign in &all_rots_by_sign {
slog::debug!(
log,
"inserting RoT by sign into tuf_repo_rot_by_sign table";
"rot_by_sign" => %rot_by_sign.id,
);
values.push((
dsl::tuf_repo_id.eq(desc.repo.id),
dsl::tuf_rot_by_sign_id.eq(rot_by_sign.id),
));
}

diesel::insert_into(dsl::tuf_repo_rot_by_sign)
.values(values)
.execute_async(&conn)
.await?;
}

let recorded = TufRepoDescription {
repo,
artifacts: all_artifacts,
rots_by_sign: all_rots_by_sign,
};
Ok(TufRepoInsertResponse {
recorded,
status: TufRepoInsertStatus::Inserted,
Expand Down
Loading
Loading