Skip to content

Commit 6211b89

Browse files
committed
client: report manifest list digest for multi-arch images
When pulling a multi-arch image, this crate automatically resolves the manifest list to the appropriate arch manifest. When doing so, also keep track of the digest of the manifest list and report it to the caller. This helps callers validate signatures of multi-arch images. Since we ensure that the digests in the manifest list match any manifest that we pull from the manifest list, it is ok to sign just the top-level manifest, although some tools will sign recursively. Signed-off-by: Tobin Feldman-Fitzthum <[email protected]>
1 parent 3831d7d commit 6211b89

File tree

1 file changed

+108
-10
lines changed

1 file changed

+108
-10
lines changed

src/client.rs

Lines changed: 108 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -921,6 +921,29 @@ impl Client {
921921
self._pull_image_manifest(image).await
922922
}
923923

924+
/// Pull a manifest from the remote OCI Distribution service.
925+
///
926+
/// The client will check if it's already been authenticated and if
927+
/// not will attempt to do.
928+
///
929+
/// Returns `(image_manifest, manifest_digest, Option<manifest_list_digest>)`.
930+
/// The manifest list digest is `Some` when the original reference pointed to
931+
/// an image index / manifest list; `None` when it pointed directly to a
932+
/// single-platform image manifest.
933+
///
934+
/// If a multi-platform Image Index manifest is encountered, a platform-specific
935+
/// Image manifest will be selected using the client's default platform resolution.
936+
pub async fn pull_image_manifest_and_list_digest(
937+
&self,
938+
image: &Reference,
939+
auth: &RegistryAuth,
940+
) -> Result<(OciImageManifest, String, Option<String>)> {
941+
self.store_auth_if_needed(image.resolve_registry(), auth)
942+
.await;
943+
944+
self._pull_image_manifest_and_list_digest(image).await
945+
}
946+
924947
/// Pull a manifest from the remote OCI Distribution service without parsing it.
925948
///
926949
/// The client will check if it's already been authenticated and if
@@ -966,25 +989,48 @@ impl Client {
966989
/// If a multi-platform Image Index manifest is encountered, a platform-specific
967990
/// Image manifest will be selected using the client's default platform resolution.
968991
async fn _pull_image_manifest(&self, image: &Reference) -> Result<(OciImageManifest, String)> {
992+
let (manifest, digest, _list_digest) =
993+
self._pull_image_manifest_and_list_digest(image).await?;
994+
Ok((manifest, digest))
995+
}
996+
997+
/// Pull an image manifest from the remote OCI Distribution service,
998+
/// also returning the manifest list digest if the image is multi-arch.
999+
///
1000+
/// If the connection has already gone through authentication, this will
1001+
/// use the bearer token. Otherwise, this will attempt an anonymous pull.
1002+
///
1003+
/// Returns `(image_manifest, manifest_digest, Option<manifest_list_digest>)`.
1004+
/// The manifest list digest is `Some` when the original reference pointed to
1005+
/// an image index / manifest list; `None` when it pointed directly to a
1006+
/// single-platform image manifest.
1007+
async fn _pull_image_manifest_and_list_digest(
1008+
&self,
1009+
image: &Reference,
1010+
) -> Result<(OciImageManifest, String, Option<String>)> {
9691011
let (manifest, digest) = self._pull_manifest(image).await?;
9701012
match manifest {
971-
OciManifest::Image(image_manifest) => Ok((image_manifest, digest)),
1013+
OciManifest::Image(image_manifest) => Ok((image_manifest, digest, None)),
9721014
OciManifest::ImageIndex(image_index_manifest) => {
1015+
let list_digest = digest;
9731016
debug!("Inspecting Image Index Manifest");
974-
let digest = if let Some(resolver) = &self.config.platform_resolver {
1017+
let platform_digest = if let Some(resolver) = &self.config.platform_resolver {
9751018
resolver(&image_index_manifest.manifests)
9761019
} else {
9771020
return Err(OciDistributionError::ImageIndexParsingNoPlatformResolverError);
9781021
};
9791022

980-
match digest {
981-
Some(digest) => {
982-
debug!("Selected manifest entry with digest: {}", digest);
983-
let manifest_entry_reference = image.clone_with_digest(digest.clone());
1023+
match platform_digest {
1024+
Some(platform_digest) => {
1025+
debug!("Selected manifest entry with digest: {}", platform_digest);
1026+
let manifest_entry_reference =
1027+
image.clone_with_digest(platform_digest.clone());
9841028
self._pull_manifest(&manifest_entry_reference)
9851029
.await
9861030
.and_then(|(manifest, _digest)| match manifest {
987-
OciManifest::Image(manifest) => Ok((manifest, digest)),
1031+
OciManifest::Image(manifest) => {
1032+
Ok((manifest, platform_digest, Some(list_digest)))
1033+
}
9881034
OciManifest::ImageIndex(_) => {
9891035
Err(OciDistributionError::ImageManifestNotFoundError(
9901036
"received Image Index manifest instead".to_string(),
@@ -1095,25 +1141,77 @@ impl Client {
10951141
digest,
10961142
String::from_utf8(config.data.into()).map_err(|e| {
10971143
OciDistributionError::GenericError(Some(format!(
1098-
"Cannot not UTF8 compliant: {e}"
1144+
"Cannot parse config as UTF-8 string: {e}"
10991145
)))
11001146
})?,
11011147
))
11021148
})
11031149
}
11041150

1151+
/// Pull a manifest and its config from the remote OCI Distribution service.
1152+
///
1153+
/// The client will check if it's already been authenticated and if
1154+
/// not will attempt to do.
1155+
///
1156+
/// Returns `(image_manifest, manifest_digest, config_json, Option<manifest_list_digest>)`.
1157+
/// The manifest list digest is `Some` when the original reference pointed to
1158+
/// an image index / manifest list; `None` when it pointed directly to a
1159+
/// single-platform image manifest.
1160+
///
1161+
/// If a multi-platform Image Index manifest is encountered, a platform-specific
1162+
/// Image manifest will be selected using the client's default platform resolution.
1163+
pub async fn pull_manifest_and_config_and_list_digest(
1164+
&self,
1165+
image: &Reference,
1166+
auth: &RegistryAuth,
1167+
) -> Result<(OciImageManifest, String, String, Option<String>)> {
1168+
self.store_auth_if_needed(image.resolve_registry(), auth)
1169+
.await;
1170+
1171+
self._pull_manifest_and_config_and_list_digest(image)
1172+
.await
1173+
.and_then(|(manifest, digest, config, list_digest)| {
1174+
Ok((
1175+
manifest,
1176+
digest,
1177+
String::from_utf8(config.data.into()).map_err(|e| {
1178+
OciDistributionError::GenericError(Some(format!(
1179+
"Cannot parse config as UTF-8 string: {e}"
1180+
)))
1181+
})?,
1182+
list_digest,
1183+
))
1184+
})
1185+
}
1186+
11051187
async fn _pull_manifest_and_config(
11061188
&self,
11071189
image: &Reference,
11081190
) -> Result<(OciImageManifest, String, Config)> {
1109-
let (manifest, digest) = self._pull_image_manifest(image).await?;
1191+
let (manifest, digest, config, _list_digest) = self
1192+
._pull_manifest_and_config_and_list_digest(image)
1193+
.await?;
1194+
Ok((manifest, digest, config))
1195+
}
1196+
1197+
async fn _pull_manifest_and_config_and_list_digest(
1198+
&self,
1199+
image: &Reference,
1200+
) -> Result<(OciImageManifest, String, Config, Option<String>)> {
1201+
let (manifest, digest, list_digest) =
1202+
self._pull_image_manifest_and_list_digest(image).await?;
11101203

11111204
let mut out: Vec<u8> = Vec::new();
11121205
debug!("Pulling config layer");
11131206
self.pull_blob(image, &manifest.config, &mut out).await?;
11141207
let media_type = manifest.config.media_type.clone();
11151208
let annotations = manifest.annotations.clone();
1116-
Ok((manifest, digest, Config::new(out, media_type, annotations)))
1209+
Ok((
1210+
manifest,
1211+
digest,
1212+
Config::new(out, media_type, annotations),
1213+
list_digest,
1214+
))
11171215
}
11181216

11191217
/// Push a manifest list to an OCI registry.

0 commit comments

Comments
 (0)