Skip to content

Commit

Permalink
[wicketd] Fall back to old behavior if SP is too old to support readi…
Browse files Browse the repository at this point in the history
…ng RoT CMPA/CFPA (#4326)

We hit this on rack3 but did not hit it on dogfood due to more frequent
updates to dogfood's SP/RoT. Prior to this PR, wicketd expected to be
able to ask an SP for its RoT's CMPA/CFPA pages, but if a rack is
jumping from the 1.0.2 release to current master, its SPs are too old to
understand that message. With this change, we will fall back to
wicketd's previous behavior of requiring exactly 1 RoT archive if we
fail to fetch the CMPA from the target component.
  • Loading branch information
jgallagher authored Oct 24, 2023
1 parent 35a7f44 commit 4bd19c8
Show file tree
Hide file tree
Showing 2 changed files with 123 additions and 75 deletions.
193 changes: 121 additions & 72 deletions wicketd/src/update_tracker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1608,90 +1608,139 @@ impl UpdateContext {
page.copy_from_slice(&output_buf[..n]);
Ok(page)
};
let cmpa = self

// We may be talking to an SP / RoT that doesn't yet know how to give us
// its CMPA. If we hit a protocol version error here, we can fall back
// to our old behavior of requiring exactly 1 RoT artifact.
let mut artifact_to_apply = None;

let cmpa = match self
.mgs_client
.sp_rot_cmpa_get(
self.sp.type_,
self.sp.slot,
SpComponent::ROT.const_as_str(),
)
.await
.map_err(|err| UpdateTerminalError::GetRotCmpaFailed {
error: err.into(),
})
.and_then(|response| {
{
Ok(response) => {
let data = response.into_inner().base64_data;
base64_decode_rot_page(data).map_err(|error| {
Some(base64_decode_rot_page(data).map_err(|error| {
UpdateTerminalError::GetRotCmpaFailed { error }
})
})?;
let cfpa = self
.mgs_client
.sp_rot_cfpa_get(
self.sp.type_,
self.sp.slot,
SpComponent::ROT.const_as_str(),
&gateway_client::types::GetCfpaParams {
slot: RotCfpaSlot::Active,
},
)
.await
.map_err(|err| UpdateTerminalError::GetRotCfpaFailed {
error: err.into(),
})
.and_then(|response| {
let data = response.into_inner().base64_data;
base64_decode_rot_page(data).map_err(|error| {
UpdateTerminalError::GetRotCfpaFailed { error }
})
})?;

// Loop through our possible artifacts and find the first (we only
// expect one!) that verifies against the RoT's CMPA/CFPA.
let mut artifact_to_apply = None;
for artifact in available_artifacts {
let image = artifact
.data
.reader_stream()
.and_then(|stream| async {
let mut buf = Vec::with_capacity(artifact.data.file_size());
StreamReader::new(stream)
.read_to_end(&mut buf)
.await
.context("I/O error reading extracted archive")?;
Ok(buf)
})
.await
.map_err(|error| {
UpdateTerminalError::FailedFindingSignedRotImage { error }
})?;
let archive = RawHubrisArchive::from_vec(image).map_err(|err| {
UpdateTerminalError::FailedFindingSignedRotImage {
error: anyhow::Error::new(err).context(format!(
"failed to read hubris archive for {:?}",
artifact.id
)),
}
})?;
match archive.verify(&cmpa, &cfpa) {
Ok(()) => {
})?)
}
// TODO is there a better way to check the _specific_ error response
// we get here? We only have a couple of strings; we could check the
// error string contents for something like "WrongVersion", but
// that's pretty fragile. Instead we'll treat any error response
// here as a "fallback to previous behavior".
Err(err @ gateway_client::Error::ErrorResponse(_)) => {
if available_artifacts.len() == 1 {
info!(
self.log, "RoT archive verification success";
"name" => artifact.id.name.as_str(),
"version" => %artifact.id.version,
"kind" => ?artifact.id.kind,
self.log,
"Failed to get RoT CMPA page; \
using only available RoT artifact";
"err" => %err,
);
artifact_to_apply = Some(artifact.clone());
break;
}
Err(err) => {
// We log this but don't fail - we want to continue looking
// for a verifiable artifact.
info!(
self.log, "RoT archive verification failed";
"artifact" => ?artifact.id,
"err" => %DisplayErrorChain::new(&err),
artifact_to_apply = Some(available_artifacts[0].clone());
None
} else {
error!(
self.log,
"Failed to get RoT CMPA; unable to choose from \
multiple available RoT artifacts";
"err" => %err,
"num_rot_artifacts" => available_artifacts.len(),
);
return Err(UpdateTerminalError::GetRotCmpaFailed {
error: err.into(),
});
}
}
// For any other error (e.g., comms failures), just fail as normal.
Err(err) => {
return Err(UpdateTerminalError::GetRotCmpaFailed {
error: err.into(),
});
}
};

// If we were able to fetch the CMPA, we also need to fetch the CFPA and
// then find the correct RoT artifact. If we weren't able to fetch the
// CMPA, we either already set `artifact_to_apply` to the one-and-only
// RoT artifact available, or we returned a terminal error.
if let Some(cmpa) = cmpa {
let cfpa = self
.mgs_client
.sp_rot_cfpa_get(
self.sp.type_,
self.sp.slot,
SpComponent::ROT.const_as_str(),
&gateway_client::types::GetCfpaParams {
slot: RotCfpaSlot::Active,
},
)
.await
.map_err(|err| UpdateTerminalError::GetRotCfpaFailed {
error: err.into(),
})
.and_then(|response| {
let data = response.into_inner().base64_data;
base64_decode_rot_page(data).map_err(|error| {
UpdateTerminalError::GetRotCfpaFailed { error }
})
})?;

// Loop through our possible artifacts and find the first (we only
// expect one!) that verifies against the RoT's CMPA/CFPA.
for artifact in available_artifacts {
let image = artifact
.data
.reader_stream()
.and_then(|stream| async {
let mut buf =
Vec::with_capacity(artifact.data.file_size());
StreamReader::new(stream)
.read_to_end(&mut buf)
.await
.context("I/O error reading extracted archive")?;
Ok(buf)
})
.await
.map_err(|error| {
UpdateTerminalError::FailedFindingSignedRotImage {
error,
}
})?;
let archive =
RawHubrisArchive::from_vec(image).map_err(|err| {
UpdateTerminalError::FailedFindingSignedRotImage {
error: anyhow::Error::new(err).context(format!(
"failed to read hubris archive for {:?}",
artifact.id
)),
}
})?;
match archive.verify(&cmpa, &cfpa) {
Ok(()) => {
info!(
self.log, "RoT archive verification success";
"name" => artifact.id.name.as_str(),
"version" => %artifact.id.version,
"kind" => ?artifact.id.kind,
);
artifact_to_apply = Some(artifact.clone());
break;
}
Err(err) => {
// We log this but don't fail - we want to continue
// looking for a verifiable artifact.
info!(
self.log, "RoT archive verification failed";
"artifact" => ?artifact.id,
"err" => %DisplayErrorChain::new(&err),
);
}
}
}
}
Expand Down
5 changes: 2 additions & 3 deletions wicketd/tests/integration_tests/updates.rs
Original file line number Diff line number Diff line change
Expand Up @@ -168,9 +168,8 @@ async fn test_updates() {
match terminal_event.kind {
StepEventKind::ExecutionFailed { failed_step, .. } => {
// TODO: obviously we shouldn't stop here, get past more of the
// update process in this test. We currently fail when attempting to
// look up the RoT's CMPA/CFPA.
assert_eq!(failed_step.info.component, UpdateComponent::Rot);
// update process in this test.
assert_eq!(failed_step.info.component, UpdateComponent::Sp);
}
other => {
panic!("unexpected terminal event kind: {other:?}");
Expand Down

0 comments on commit 4bd19c8

Please sign in to comment.