Skip to content

Commit 621025c

Browse files
committed
[sha512]: image: enable sha512 support
- Replace hardcoded SHA256 with configurable digest algorithms using storage/pkg/supported-digests - Add centralized digest validation utilities in image/pkg/digestvalidation - Implement parameterized digest computation in image/copy/single.go - Rename DigestIfCanonicalUnknown to DigestIfConfiguredUnknown for clarity Signed-off-by: Lokesh Mandvekar <[email protected]>
1 parent 79f1c23 commit 621025c

22 files changed

+554
-59
lines changed

image/copy/digesting_reader_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ func TestDigestingReaderRead(t *testing.T) {
3434
{[]byte(""), "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"},
3535
{[]byte("abc"), "sha256:ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"},
3636
{make([]byte, 65537), "sha256:3266304f31be278d06c3bd3eb9aa3e00c59bedec0a890de466568b0b90b0e01f"},
37+
// SHA512 test cases
38+
{[]byte(""), "sha512:cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e"},
39+
{[]byte("abc"), "sha512:ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f"},
40+
{make([]byte, 65537), "sha512:490821004e5a6025fe335a11f6c27b0f73cae0434bd9d2e5ac7aee3370bd421718cad7d8fbfd5f39153b6ca3b05faede68f5d6e462eeaf143bb034791ceb72ab"},
3741
}
3842
// Valid input
3943
for _, c := range cases {

image/copy/single.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"go.podman.io/image/v5/transports"
2929
"go.podman.io/image/v5/types"
3030
chunkedToc "go.podman.io/storage/pkg/chunked/toc"
31+
supportedDigests "go.podman.io/storage/pkg/supported-digests"
3132
)
3233

3334
// imageCopier tracks state specific to a single image (possibly an item of a manifest list)
@@ -977,7 +978,15 @@ func diffIDComputationGoroutine(dest chan<- diffIDResult, layerStream io.ReadClo
977978
}
978979

979980
// computeDiffID reads all input from layerStream, uncompresses it using decompressor if necessary, and returns its digest.
981+
// This is a wrapper around computeDiffIDWithAlgorithm that uses the globally configured digest algorithm.
980982
func computeDiffID(stream io.Reader, decompressor compressiontypes.DecompressorFunc) (digest.Digest, error) {
983+
algorithm := supportedDigests.TmpDigestForNewObjects()
984+
return computeDiffIDWithAlgorithm(stream, decompressor, algorithm)
985+
}
986+
987+
// computeDiffIDWithAlgorithm reads all input from layerStream, uncompresses it using decompressor if necessary,
988+
// and returns its digest using the specified algorithm.
989+
func computeDiffIDWithAlgorithm(stream io.Reader, decompressor compressiontypes.DecompressorFunc, algorithm digest.Algorithm) (digest.Digest, error) {
981990
if decompressor != nil {
982991
s, err := decompressor(stream)
983992
if err != nil {
@@ -987,7 +996,7 @@ func computeDiffID(stream io.Reader, decompressor compressiontypes.DecompressorF
987996
stream = s
988997
}
989998

990-
return digest.Canonical.FromReader(stream)
999+
return algorithm.FromReader(stream)
9911000
}
9921001

9931002
// algorithmsByNames returns slice of Algorithms from a sequence of Algorithm Names

image/copy/single_test.go

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"go.podman.io/image/v5/pkg/compression"
1818
compressiontypes "go.podman.io/image/v5/pkg/compression/types"
1919
"go.podman.io/image/v5/types"
20+
supportedDigests "go.podman.io/storage/pkg/supported-digests"
2021
)
2122

2223
func TestUpdatedBlobInfoFromReuse(t *testing.T) {
@@ -110,13 +111,24 @@ func goDiffIDComputationGoroutineWithTimeout(layerStream io.ReadCloser, decompre
110111
}
111112

112113
func TestDiffIDComputationGoroutine(t *testing.T) {
114+
// Test with SHA256 (default)
113115
stream, err := os.Open("fixtures/Hello.uncompressed")
114116
require.NoError(t, err)
115117
res := goDiffIDComputationGoroutineWithTimeout(stream, nil)
116118
require.NotNil(t, res)
117119
assert.NoError(t, res.err)
118120
assert.Equal(t, "sha256:185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969", res.digest.String())
119121

122+
// Test with SHA512 using the parametrized function
123+
stream2, err := os.Open("fixtures/Hello.uncompressed")
124+
require.NoError(t, err)
125+
defer stream2.Close()
126+
127+
// Use the parametrized function directly instead of overriding global state
128+
digest, err := computeDiffIDWithAlgorithm(stream2, nil, digest.SHA512)
129+
require.NoError(t, err)
130+
assert.Equal(t, "sha512:3615f80c9d293ed7402687f94b22d58e529b8cc7916f8fac7fddf7fbd5af4cf777d3d795a7a00a16bf7e7f3fb9561ee9baae480da9fe7a18769e71886b03f315", digest.String())
131+
120132
// Error reading input
121133
reader, writer := io.Pipe()
122134
err = writer.CloseWithError(errors.New("Expected error reading input in diffIDComputationGoroutine"))
@@ -130,32 +142,59 @@ func TestComputeDiffID(t *testing.T) {
130142
for _, c := range []struct {
131143
filename string
132144
decompressor compressiontypes.DecompressorFunc
145+
algorithm digest.Algorithm
133146
result digest.Digest
134147
}{
135-
{"fixtures/Hello.uncompressed", nil, "sha256:185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"},
136-
{"fixtures/Hello.gz", nil, "sha256:0bd4409dcd76476a263b8f3221b4ce04eb4686dec40bfdcc2e86a7403de13609"},
137-
{"fixtures/Hello.gz", compression.GzipDecompressor, "sha256:185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"},
138-
{"fixtures/Hello.zst", nil, "sha256:361a8e0372ad438a0316eb39a290318364c10b60d0a7e55b40aa3eafafc55238"},
139-
{"fixtures/Hello.zst", compression.ZstdDecompressor, "sha256:185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"},
148+
// SHA256 test cases (default)
149+
{"fixtures/Hello.uncompressed", nil, digest.SHA256, "sha256:185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"},
150+
{"fixtures/Hello.gz", nil, digest.SHA256, "sha256:0bd4409dcd76476a263b8f3221b4ce04eb4686dec40bfdcc2e86a7403de13609"},
151+
{"fixtures/Hello.gz", compression.GzipDecompressor, digest.SHA256, "sha256:185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"},
152+
{"fixtures/Hello.zst", nil, digest.SHA256, "sha256:361a8e0372ad438a0316eb39a290318364c10b60d0a7e55b40aa3eafafc55238"},
153+
{"fixtures/Hello.zst", compression.ZstdDecompressor, digest.SHA256, "sha256:185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969"},
154+
// SHA512 test cases
155+
{"fixtures/Hello.uncompressed", nil, digest.SHA512, "sha512:3615f80c9d293ed7402687f94b22d58e529b8cc7916f8fac7fddf7fbd5af4cf777d3d795a7a00a16bf7e7f3fb9561ee9baae480da9fe7a18769e71886b03f315"},
156+
{"fixtures/Hello.gz", nil, digest.SHA512, "sha512:8ee9be48dfc6274f65199847cd18ff4711f00329c5063b17cd128ba45ea1b9cea2479db0266cc1f4a3902874fdd7306f9c8a615347c0603b893fc75184fcb627"},
157+
{"fixtures/Hello.gz", compression.GzipDecompressor, digest.SHA512, "sha512:3615f80c9d293ed7402687f94b22d58e529b8cc7916f8fac7fddf7fbd5af4cf777d3d795a7a00a16bf7e7f3fb9561ee9baae480da9fe7a18769e71886b03f315"},
158+
{"fixtures/Hello.zst", nil, digest.SHA512, "sha512:e4ddd61689ce9d1cdd49e11dc8dc89ca064bdb09e85b9df56658560b8207647a78b95d04c3f5f2fb31abf13e1822f0d19307df18a3fdf88f58ef24a50e71a1ae"},
159+
{"fixtures/Hello.zst", compression.ZstdDecompressor, digest.SHA512, "sha512:3615f80c9d293ed7402687f94b22d58e529b8cc7916f8fac7fddf7fbd5af4cf777d3d795a7a00a16bf7e7f3fb9561ee9baae480da9fe7a18769e71886b03f315"},
140160
} {
141161
stream, err := os.Open(c.filename)
142162
require.NoError(t, err, c.filename)
143163
defer stream.Close()
144164

165+
// Save original algorithm and set the desired one
166+
originalAlgorithm := supportedDigests.TmpDigestForNewObjects()
167+
err = supportedDigests.TmpSetDigestForNewObjects(c.algorithm)
168+
require.NoError(t, err)
169+
170+
// Test the digest computation directly without ImageDestination
145171
diffID, err := computeDiffID(stream, c.decompressor)
146172
require.NoError(t, err, c.filename)
147173
assert.Equal(t, c.result, diffID)
174+
175+
// Restore the original algorithm
176+
err = supportedDigests.TmpSetDigestForNewObjects(originalAlgorithm)
177+
require.NoError(t, err)
148178
}
149179

150180
// Error initializing decompression
151-
_, err := computeDiffID(bytes.NewReader([]byte{}), compression.GzipDecompressor)
181+
originalAlgorithm := supportedDigests.TmpDigestForNewObjects()
182+
err := supportedDigests.TmpSetDigestForNewObjects(digest.SHA256)
183+
require.NoError(t, err)
184+
_, err = computeDiffID(bytes.NewReader([]byte{}), compression.GzipDecompressor)
152185
assert.Error(t, err)
186+
err = supportedDigests.TmpSetDigestForNewObjects(originalAlgorithm)
187+
require.NoError(t, err)
153188

154189
// Error reading input
155190
reader, writer := io.Pipe()
156191
defer reader.Close()
157192
err = writer.CloseWithError(errors.New("Expected error reading input in computeDiffID"))
158193
require.NoError(t, err)
194+
err = supportedDigests.TmpSetDigestForNewObjects(digest.SHA256)
195+
require.NoError(t, err)
159196
_, err = computeDiffID(reader, nil)
160197
assert.Error(t, err)
198+
err = supportedDigests.TmpSetDigestForNewObjects(originalAlgorithm)
199+
require.NoError(t, err)
161200
}

image/directory/directory_dest.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ func (d *dirImageDestination) PutBlobWithOptions(ctx context.Context, stream io.
151151
}
152152
}()
153153

154-
digester, stream := putblobdigest.DigestIfCanonicalUnknown(stream, inputInfo)
154+
digester, stream := putblobdigest.DigestIfConfiguredUnknown(stream, inputInfo)
155155
// TODO: This can take quite some time, and should ideally be cancellable using ctx.Done().
156156
size, err := io.Copy(blobFile, stream)
157157
if err != nil {

image/docker/docker_image_dest.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ func (d *dockerImageDestination) PutBlobWithOptions(ctx context.Context, stream
178178
return private.UploadedBlob{}, fmt.Errorf("determining upload URL: %w", err)
179179
}
180180

181-
digester, stream := putblobdigest.DigestIfCanonicalUnknown(stream, inputInfo)
181+
digester, stream := putblobdigest.DigestIfConfiguredUnknown(stream, inputInfo)
182182
sizeCounter := &sizeCounter{}
183183
stream = io.TeeReader(stream, sizeCounter)
184184

image/internal/image/docker_schema1.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"go.podman.io/image/v5/docker/reference"
1010
"go.podman.io/image/v5/manifest"
1111
"go.podman.io/image/v5/types"
12+
supportedDigests "go.podman.io/storage/pkg/supported-digests"
1213
)
1314

1415
type manifestSchema1 struct {
@@ -160,6 +161,13 @@ func (m *manifestSchema1) convertToManifestSchema2Generic(ctx context.Context, o
160161
//
161162
// Based on github.com/docker/docker/distribution/pull_v2.go
162163
func (m *manifestSchema1) convertToManifestSchema2(_ context.Context, options *types.ManifestUpdateOptions) (*manifestSchema2, error) {
164+
// Explicitly reject SHA512+Schema1 combinations as they are not supported
165+
// Schema1 is deprecated and Docker/registry don't support SHA512+Schema1
166+
configuredAlgorithm := supportedDigests.TmpDigestForNewObjects()
167+
if configuredAlgorithm == digest.SHA512 {
168+
return nil, fmt.Errorf("SHA512+Schema1 is not supported: Schema1 is deprecated and Docker/registry do not support SHA512 with Schema1 manifests. Please use SHA256 or convert to Schema2/OCI format")
169+
}
170+
163171
uploadedLayerInfos := options.InformationOnly.LayerInfos
164172
layerDiffIDs := options.InformationOnly.LayerDiffIDs
165173

@@ -219,7 +227,7 @@ func (m *manifestSchema1) convertToManifestSchema2(_ context.Context, options *t
219227
configDescriptor := manifest.Schema2Descriptor{
220228
MediaType: manifest.DockerV2Schema2ConfigMediaType,
221229
Size: int64(len(configJSON)),
222-
Digest: digest.FromBytes(configJSON),
230+
Digest: supportedDigests.TmpDigestForNewObjects().FromBytes(configJSON),
223231
}
224232

225233
if options.LayerInfos != nil {

image/internal/image/docker_schema1_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"go.podman.io/image/v5/docker/reference"
1616
"go.podman.io/image/v5/manifest"
1717
"go.podman.io/image/v5/types"
18+
supportedDigests "go.podman.io/storage/pkg/supported-digests"
1819
)
1920

2021
var schema1FixtureLayerInfos = []types.BlobInfo{
@@ -720,3 +721,38 @@ func TestManifestSchema1CanChangeLayerCompression(t *testing.T) {
720721
assert.True(t, m.CanChangeLayerCompression(""))
721722
}
722723
}
724+
725+
// TestSchema1SHA512Rejection tests that SHA512+Schema1 combinations are explicitly rejected
726+
func TestSchema1SHA512Rejection(t *testing.T) {
727+
// Save original algorithm and restore it after the test
728+
originalAlgorithm := supportedDigests.TmpDigestForNewObjects()
729+
defer func() {
730+
err := supportedDigests.TmpSetDigestForNewObjects(originalAlgorithm)
731+
require.NoError(t, err)
732+
}()
733+
734+
// Set SHA512 algorithm
735+
err := supportedDigests.TmpSetDigestForNewObjects(digest.SHA512)
736+
require.NoError(t, err)
737+
738+
// Create a schema1 manifest
739+
manifestBlob, err := os.ReadFile(filepath.Join("fixtures", "schema1.json"))
740+
require.NoError(t, err)
741+
742+
m, err := manifestSchema1FromManifest(manifestBlob)
743+
require.NoError(t, err)
744+
745+
// Try to convert to schema2 with SHA512 - this should fail
746+
schema1Manifest := m.(*manifestSchema1)
747+
_, err = schema1Manifest.convertToManifestSchema2(context.Background(), &types.ManifestUpdateOptions{
748+
InformationOnly: types.ManifestUpdateInformation{
749+
LayerInfos: schema1FixtureLayerInfos,
750+
},
751+
})
752+
753+
// Should get an error about SHA512+Schema1 not being supported
754+
require.Error(t, err)
755+
assert.Contains(t, err.Error(), "SHA512+Schema1 is not supported")
756+
assert.Contains(t, err.Error(), "Schema1 is deprecated")
757+
assert.Contains(t, err.Error(), "Please use SHA256 or convert to Schema2/OCI format")
758+
}

image/internal/image/docker_schema2.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import (
1717
"go.podman.io/image/v5/internal/iolimits"
1818
"go.podman.io/image/v5/manifest"
1919
"go.podman.io/image/v5/pkg/blobinfocache/none"
20+
"go.podman.io/image/v5/pkg/digestvalidation"
2021
"go.podman.io/image/v5/types"
2122
)
2223

@@ -110,9 +111,11 @@ func (m *manifestSchema2) ConfigBlob(ctx context.Context) ([]byte, error) {
110111
if err != nil {
111112
return nil, err
112113
}
113-
computedDigest := digest.FromBytes(blob)
114-
if computedDigest != m.m.ConfigDescriptor.Digest {
115-
return nil, fmt.Errorf("Download config.json digest %s does not match expected %s", computedDigest, m.m.ConfigDescriptor.Digest)
114+
expectedDigest := m.m.ConfigDescriptor.Digest
115+
116+
// Validate the blob against the expected digest using centralized validation
117+
if err := digestvalidation.ValidateBlobAgainstDigest(blob, expectedDigest); err != nil {
118+
return nil, fmt.Errorf("config descriptor validation failed: %w", err)
116119
}
117120
m.configBlob = blob
118121
}

image/internal/image/oci.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import (
88
"slices"
99

1010
ociencspec "github.com/containers/ocicrypt/spec"
11-
"github.com/opencontainers/go-digest"
1211
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
1312
"go.podman.io/image/v5/docker/reference"
1413
"go.podman.io/image/v5/internal/iolimits"
@@ -74,9 +73,12 @@ func (m *manifestOCI1) ConfigBlob(ctx context.Context) ([]byte, error) {
7473
if err != nil {
7574
return nil, err
7675
}
77-
computedDigest := digest.FromBytes(blob)
78-
if computedDigest != m.m.Config.Digest {
79-
return nil, fmt.Errorf("Download config.json digest %s does not match expected %s", computedDigest, m.m.Config.Digest)
76+
// Use the same algorithm as the expected digest
77+
expectedDigest := m.m.Config.Digest
78+
algorithm := expectedDigest.Algorithm()
79+
computedDigest := algorithm.FromBytes(blob)
80+
if computedDigest != expectedDigest {
81+
return nil, fmt.Errorf("Download config.json digest %s does not match expected %s", computedDigest, expectedDigest)
8082
}
8183
m.configBlob = blob
8284
}

image/internal/manifest/manifest.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
digest "github.com/opencontainers/go-digest"
99
imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1"
1010
compressiontypes "go.podman.io/image/v5/pkg/compression/types"
11+
supportedDigests "go.podman.io/storage/pkg/supported-digests"
1112
)
1213

1314
// FIXME: Should we just use docker/distribution and docker/docker implementations directly?
@@ -123,7 +124,7 @@ func Digest(manifest []byte) (digest.Digest, error) {
123124
}
124125
}
125126

126-
return digest.FromBytes(manifest), nil
127+
return supportedDigests.TmpDigestForNewObjects().FromBytes(manifest), nil
127128
}
128129

129130
// MatchesDigest returns true iff the manifest matches expectedDigest.

0 commit comments

Comments
 (0)