From a762705ceceb793f356915df1d63362dbd7e91d2 Mon Sep 17 00:00:00 2001 From: Lokesh Mandvekar Date: Sat, 15 Nov 2025 13:57:38 -0500 Subject: [PATCH] image/internal: validate blob against digest `validateBlobAgainstDigest` verifies that the provided blob matches the exepcted digest. If expected digest itself is invalid or unusable, it rejects the blob. Callers don't need to pre-validate the expected digest. This enables `skopeo copy` to work with sha512-digested images. Co-Authored-By: Claude Code Signed-off-by: Lokesh Mandvekar --- image/internal/image/digest_validation.go | 26 +++++ .../internal/image/digest_validation_test.go | 101 ++++++++++++++++++ image/internal/image/docker_schema2.go | 5 +- image/internal/image/oci.go | 6 +- 4 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 image/internal/image/digest_validation.go create mode 100644 image/internal/image/digest_validation_test.go diff --git a/image/internal/image/digest_validation.go b/image/internal/image/digest_validation.go new file mode 100644 index 0000000000..88a870ac34 --- /dev/null +++ b/image/internal/image/digest_validation.go @@ -0,0 +1,26 @@ +package image + +import ( + "fmt" + + "github.com/opencontainers/go-digest" +) + +func validateBlobAgainstDigest(blob []byte, expectedDigest digest.Digest) error { + if expectedDigest == "" { + return fmt.Errorf("expected digest is empty") + } + err := expectedDigest.Validate() + if err != nil { + return fmt.Errorf("invalid digest format %q: %w", expectedDigest, err) + } + digestAlgorithm := expectedDigest.Algorithm() + if !digestAlgorithm.Available() { + return fmt.Errorf("unsupported digest algorithm: %s", digestAlgorithm) + } + computedDigest := digestAlgorithm.FromBytes(blob) + if computedDigest != expectedDigest { + return fmt.Errorf("blob digest %s does not match expected %s", computedDigest, expectedDigest) + } + return nil +} diff --git a/image/internal/image/digest_validation_test.go b/image/internal/image/digest_validation_test.go new file mode 100644 index 0000000000..bda48ba1fa --- /dev/null +++ b/image/internal/image/digest_validation_test.go @@ -0,0 +1,101 @@ +package image + +import ( + "testing" + + "github.com/opencontainers/go-digest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestValidateBlobAgainstDigest(t *testing.T) { + testBlob := []byte("test data") + + tests := []struct { + name string + blob []byte + expectedDigest digest.Digest + expectError bool + errorContains string + }{ + { + name: "empty digest", + blob: testBlob, + expectedDigest: "", + expectError: true, + errorContains: "expected digest is empty", + }, + { + name: "invalid digest format - no algorithm", + blob: testBlob, + expectedDigest: "invalidsyntax", + expectError: true, + errorContains: "invalid digest format", + }, + { + name: "invalid digest format - sha256 prefix w/ malformed hex", + blob: testBlob, + expectedDigest: "sha256:notahexstring!@#", + expectError: true, + errorContains: "invalid digest format", + }, + { + name: "invalid digest format - sha512 prefix w/ malformed hex", + blob: testBlob, + expectedDigest: "sha512:notahexstring!@#", + expectError: true, + errorContains: "invalid digest format", + }, + { + name: "invalid digest format - unknown algorithm", + blob: testBlob, + expectedDigest: "unknown-algo:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + expectError: true, + errorContains: "invalid digest format", + }, + { + name: "digest mismatch", + blob: testBlob, + expectedDigest: digest.SHA256.FromBytes([]byte("different data")), + expectError: true, + errorContains: "blob digest", + }, + { + name: "empty blob with matching digest", + blob: []byte{}, + expectedDigest: digest.SHA256.FromBytes([]byte{}), + expectError: false, + }, + { + name: "unavailable algorithm - blake2b", + blob: testBlob, + expectedDigest: "blake2b:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + expectError: true, + errorContains: "invalid digest format", + }, + { + name: "sha256 digest success", + blob: testBlob, + expectedDigest: digest.SHA256.FromBytes(testBlob), + expectError: false, + }, + { + name: "sha512 digest success", + blob: testBlob, + expectedDigest: digest.SHA512.FromBytes(testBlob), + expectError: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateBlobAgainstDigest(tt.blob, tt.expectedDigest) + if tt.expectError { + require.Error(t, err) + assert.Contains(t, err.Error(), tt.errorContains) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/image/internal/image/docker_schema2.go b/image/internal/image/docker_schema2.go index 1586d67900..b40f4fc71e 100644 --- a/image/internal/image/docker_schema2.go +++ b/image/internal/image/docker_schema2.go @@ -110,9 +110,8 @@ func (m *manifestSchema2) ConfigBlob(ctx context.Context) ([]byte, error) { if err != nil { return nil, err } - computedDigest := digest.FromBytes(blob) - if computedDigest != m.m.ConfigDescriptor.Digest { - return nil, fmt.Errorf("Download config.json digest %s does not match expected %s", computedDigest, m.m.ConfigDescriptor.Digest) + if err := validateBlobAgainstDigest(blob, m.m.ConfigDescriptor.Digest); err != nil { + return nil, fmt.Errorf("config validation failed: %w", err) } m.configBlob = blob } diff --git a/image/internal/image/oci.go b/image/internal/image/oci.go index 56a1a6d64e..8ddb2875e0 100644 --- a/image/internal/image/oci.go +++ b/image/internal/image/oci.go @@ -8,7 +8,6 @@ import ( "slices" ociencspec "github.com/containers/ocicrypt/spec" - "github.com/opencontainers/go-digest" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/internal/iolimits" @@ -74,9 +73,8 @@ func (m *manifestOCI1) ConfigBlob(ctx context.Context) ([]byte, error) { if err != nil { return nil, err } - computedDigest := digest.FromBytes(blob) - if computedDigest != m.m.Config.Digest { - return nil, fmt.Errorf("Download config.json digest %s does not match expected %s", computedDigest, m.m.Config.Digest) + if err := validateBlobAgainstDigest(blob, m.m.Config.Digest); err != nil { + return nil, fmt.Errorf("config validation failed: %w", err) } m.configBlob = blob }