Skip to content

Commit 5c325c0

Browse files
committed
[sha512]: common: add scalable digest utilities and update libimage
- Create common/pkg/digestutils package with comprehensive digest handling functions * IsDigestReference() for robust digest detection using go-digest library * ExtractAlgorithmFromDigest() for parsing digest strings with validation * HasDigestPrefix(), GetDigestPrefix(), TrimDigestPrefix() for scalable prefix handling - Update libimage to use digestutils instead of hardcoded SHA256/SHA512 checks * Replace strings.HasPrefix() calls in filters.go, image.go, runtime.go * Support only SHA256 and SHA512 algorithms as per container-libs requirements - Update existing libimage tests to work with new digestutils functions Signed-off-by: Lokesh Mandvekar <[email protected]>
1 parent 621025c commit 5c325c0

File tree

8 files changed

+798
-14
lines changed

8 files changed

+798
-14
lines changed

common/libimage/filters.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"time"
1313

1414
"github.com/sirupsen/logrus"
15+
"go.podman.io/common/pkg/digestutils"
1516
filtersPkg "go.podman.io/common/pkg/filters"
1617
"go.podman.io/common/pkg/timetype"
1718
"go.podman.io/image/v5/docker/reference"
@@ -481,7 +482,7 @@ func filterID(value string) filterFunc {
481482

482483
// filterDigest creates a digest filter for matching the specified value.
483484
func filterDigest(value string) (filterFunc, error) {
484-
if !strings.HasPrefix(value, "sha256:") {
485+
if !digestutils.HasDigestPrefix(value) {
485486
return nil, fmt.Errorf("invalid value %q for digest filter", value)
486487
}
487488
return func(img *Image, _ *layerTree) (bool, error) {

common/libimage/filters_test.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,19 +169,35 @@ func TestFilterDigest(t *testing.T) {
169169
}{
170170
{string(busybox.Digest()[:10]), 1, busybox.ID()},
171171
{string(alpine.Digest()[:10]), 1, alpine.ID()},
172+
// Test SHA512 digest prefix matching
173+
{"sha512:1234567890abcdef", 0, ""}, // Non-existent SHA512 digest
174+
{"sha256:1234567890abcdef", 0, ""}, // Non-existent SHA256 digest
172175
} {
173176
listOptions := &ListImagesOptions{
174177
Filters: []string{"digest=" + test.filter},
175178
}
176179
listedImages, err := runtime.ListImages(ctx, listOptions)
177180
require.NoError(t, err, "%v", test)
178181
require.Len(t, listedImages, test.matches, "%s -> %v", test.filter, listedImages)
179-
require.Equal(t, listedImages[0].ID(), test.id)
182+
if test.matches > 0 {
183+
require.Equal(t, listedImages[0].ID(), test.id)
184+
}
180185
}
181186
_, err = runtime.ListImages(ctx, &ListImagesOptions{
182187
Filters: []string{"digest=this-is-not-a-digest"},
183188
})
184189
assert.Error(t, err)
190+
191+
// Test invalid digest algorithms
192+
_, err = runtime.ListImages(ctx, &ListImagesOptions{
193+
Filters: []string{"digest=md5:1234567890abcdef"},
194+
})
195+
assert.Error(t, err)
196+
197+
_, err = runtime.ListImages(ctx, &ListImagesOptions{
198+
Filters: []string{"digest=sha384:1234567890abcdef"},
199+
})
200+
assert.Error(t, err)
185201
}
186202

187203
func TestFilterID(t *testing.T) {

common/libimage/image.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
1919
"github.com/sirupsen/logrus"
2020
"go.podman.io/common/libimage/platform"
21+
"go.podman.io/common/pkg/digestutils"
2122
"go.podman.io/image/v5/docker/reference"
2223
"go.podman.io/image/v5/image"
2324
"go.podman.io/image/v5/manifest"
@@ -474,7 +475,7 @@ func (i *Image) removeRecursive(ctx context.Context, rmMap map[string]*RemoveIma
474475
// error.
475476
if referencedBy != "" && numNames != 1 {
476477
byID := strings.HasPrefix(i.ID(), referencedBy)
477-
byDigest := strings.HasPrefix(referencedBy, "sha256:")
478+
byDigest := digestutils.HasDigestPrefix(referencedBy)
478479
if !options.Force {
479480
if byID && numNames > 1 {
480481
return processedIDs, fmt.Errorf("unable to delete image %q by ID with more than one tag (%s): please force removal", i.ID(), i.Names())
@@ -577,7 +578,7 @@ var errTagDigest = errors.New("tag by digest not supported")
577578
// Tag the image with the specified name and store it in the local containers
578579
// storage. The name is normalized according to the rules of NormalizeName.
579580
func (i *Image) Tag(name string) error {
580-
if strings.HasPrefix(name, "sha256:") { // ambiguous input
581+
if digestutils.HasDigestPrefix(name) { // ambiguous input
581582
return fmt.Errorf("%s: %w", name, errTagDigest)
582583
}
583584

@@ -613,7 +614,7 @@ var errUntagDigest = errors.New("untag by digest not supported")
613614
// the local containers storage. The name is normalized according to the rules
614615
// of NormalizeName.
615616
func (i *Image) Untag(name string) error {
616-
if strings.HasPrefix(name, "sha256:") { // ambiguous input
617+
if digestutils.HasDigestPrefix(name) { // ambiguous input
617618
return fmt.Errorf("%s: %w", name, errUntagDigest)
618619
}
619620

common/libimage/import.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import (
1111

1212
v1 "github.com/opencontainers/image-spec/specs-go/v1"
1313
"github.com/sirupsen/logrus"
14+
"go.podman.io/common/pkg/digestutils"
1415
"go.podman.io/common/pkg/download"
1516
storageTransport "go.podman.io/image/v5/storage"
1617
tarballTransport "go.podman.io/image/v5/tarball"
18+
supportedDigests "go.podman.io/storage/pkg/supported-digests"
1719
)
1820

1921
// ImportOptions allow for customizing image imports.
@@ -128,5 +130,14 @@ func (r *Runtime) Import(ctx context.Context, path string, options *ImportOption
128130
}
129131
}
130132

131-
return "sha256:" + name, nil
133+
// Extract the algorithm from the getImageID result
134+
// getImageID returns something like "@sha256:abc123" or "@sha512:def456"
135+
// We need to preserve the algorithm that was actually used
136+
if algorithm, hash := digestutils.ExtractAlgorithmFromDigest(name); algorithm != "" {
137+
return algorithm + ":" + hash, nil
138+
}
139+
140+
// Fallback to configured algorithm if we can't parse the digest
141+
digestAlgorithm := supportedDigests.TmpDigestForNewObjects()
142+
return digestAlgorithm.String() + ":" + name, nil
132143
}

common/libimage/pull.go

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
ociSpec "github.com/opencontainers/image-spec/specs-go/v1"
1616
"github.com/sirupsen/logrus"
1717
"go.podman.io/common/pkg/config"
18+
"go.podman.io/common/pkg/digestutils"
1819
registryTransport "go.podman.io/image/v5/docker"
1920
dockerArchiveTransport "go.podman.io/image/v5/docker/archive"
2021
dockerDaemonTransport "go.podman.io/image/v5/docker/daemon"
@@ -26,6 +27,7 @@ import (
2627
"go.podman.io/image/v5/transports/alltransports"
2728
"go.podman.io/image/v5/types"
2829
"go.podman.io/storage"
30+
supportedDigests "go.podman.io/storage/pkg/supported-digests"
2931
)
3032

3133
// PullOptions allows for customizing image pulls.
@@ -101,7 +103,7 @@ func (r *Runtime) Pull(ctx context.Context, name string, pullPolicy config.PullP
101103

102104
// If the image clearly refers to a local one, we can look it up directly.
103105
// In fact, we need to since they are not parseable.
104-
if strings.HasPrefix(name, "sha256:") || (len(name) == 64 && !strings.ContainsAny(name, "/.:@")) {
106+
if digestutils.IsDigestReference(name) {
105107
if pullPolicy == config.PullPolicyAlways {
106108
return nil, fmt.Errorf("pull policy is always but image has been referred to by ID (%s)", name)
107109
}
@@ -261,7 +263,16 @@ func (r *Runtime) copyFromDefault(ctx context.Context, ref types.ImageReference,
261263
if err != nil {
262264
return nil, nil, err
263265
}
264-
imageName = "sha256:" + storageName[1:]
266+
// Extract the algorithm from the getImageID result
267+
// getImageID returns something like "@sha256:abc123" or "@sha512:def456"
268+
// We need to preserve the algorithm that was actually used
269+
if algorithm, hash := digestutils.ExtractAlgorithmFromDigest(storageName); algorithm != "" {
270+
imageName = algorithm + ":" + hash
271+
} else {
272+
// Fallback to configured algorithm
273+
digestAlgorithm := supportedDigests.TmpDigestForNewObjects()
274+
imageName = digestAlgorithm.String() + ":" + storageName[1:]
275+
}
265276
} else { // If the OCI-reference includes an image reference, use it
266277
storageName = refName
267278
imageName = storageName
@@ -280,7 +291,16 @@ func (r *Runtime) copyFromDefault(ctx context.Context, ref types.ImageReference,
280291
if err != nil {
281292
return nil, nil, err
282293
}
283-
imageName = "sha256:" + storageName[1:]
294+
// Extract the algorithm from the getImageID result
295+
// getImageID returns something like "@sha256:abc123" or "@sha512:def456"
296+
// We need to preserve the algorithm that was actually used
297+
if algorithm, hash := digestutils.ExtractAlgorithmFromDigest(storageName); algorithm != "" {
298+
imageName = algorithm + ":" + hash
299+
} else {
300+
// Fallback to configured algorithm
301+
digestAlgorithm := supportedDigests.TmpDigestForNewObjects()
302+
imageName = digestAlgorithm.String() + ":" + storageName[1:]
303+
}
284304
default:
285305
named, err := NormalizeName(storageName)
286306
if err != nil {
@@ -306,7 +326,16 @@ func (r *Runtime) copyFromDefault(ctx context.Context, ref types.ImageReference,
306326
if err != nil {
307327
return nil, nil, err
308328
}
309-
imageName = "sha256:" + storageName[1:]
329+
// Extract the algorithm from the getImageID result
330+
// getImageID returns something like "@sha256:abc123" or "@sha512:def456"
331+
// We need to preserve the algorithm that was actually used
332+
if algorithm, hash := digestutils.ExtractAlgorithmFromDigest(storageName); algorithm != "" {
333+
imageName = algorithm + ":" + hash
334+
} else {
335+
// Fallback to configured algorithm
336+
digestAlgorithm := supportedDigests.TmpDigestForNewObjects()
337+
imageName = digestAlgorithm.String() + ":" + storageName[1:]
338+
}
310339
}
311340

312341
// Create a storage reference.
@@ -340,8 +369,17 @@ func (r *Runtime) storageReferencesReferencesFromArchiveReader(ctx context.Conte
340369
}
341370
destNames = append(destNames, destName)
342371
// Make sure the image can be loaded after the pull by
343-
// replacing the @ with sha256:.
344-
imageNames = append(imageNames, "sha256:"+destName[1:])
372+
// replacing the @ with the correct algorithm.
373+
// Extract the algorithm from the getImageID result
374+
// getImageID returns something like "@sha256:abc123" or "@sha512:def456"
375+
// We need to preserve the algorithm that was actually used
376+
if algorithm, hash := digestutils.ExtractAlgorithmFromDigest(destName); algorithm != "" {
377+
imageNames = append(imageNames, algorithm+":"+hash)
378+
} else {
379+
// Fallback to configured algorithm
380+
digestAlgorithm := supportedDigests.TmpDigestForNewObjects()
381+
imageNames = append(imageNames, digestAlgorithm.String()+":"+destName[1:])
382+
}
345383
} else {
346384
for i := range destNames {
347385
ref, err := NormalizeName(destNames[i])

common/libimage/runtime.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"go.podman.io/common/libimage/define"
1717
"go.podman.io/common/libimage/platform"
1818
"go.podman.io/common/pkg/config"
19+
"go.podman.io/common/pkg/digestutils"
1920
"go.podman.io/image/v5/docker/reference"
2021
"go.podman.io/image/v5/pkg/shortnames"
2122
storageTransport "go.podman.io/image/v5/storage"
@@ -273,9 +274,9 @@ func (r *Runtime) LookupImage(name string, options *LookupImageOptions) (*Image,
273274

274275
byDigest := false
275276
originalName := name
276-
if strings.HasPrefix(name, "sha256:") {
277+
if trimmed, found := digestutils.TrimDigestPrefix(name); found {
277278
byDigest = true
278-
name = strings.TrimPrefix(name, "sha256:")
279+
name = trimmed
279280
}
280281
byFullID := reference.IsFullIdentifier(name)
281282

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
//go:build !remote
2+
3+
package digestutils
4+
5+
import (
6+
"strings"
7+
8+
"github.com/opencontainers/go-digest"
9+
)
10+
11+
// IsDigestReference determines if the given name is a digest-based reference.
12+
// This function properly detects digests using the go-digest library instead of
13+
// hardcoded string prefixes, avoiding conflicts with repository names like "sha256" or "sha512".
14+
//
15+
// The function supports:
16+
// - Standard digest formats (algorithm:hash) like "sha256:abc123..." or "sha512:def456..."
17+
// - Legacy 64-character hex format (SHA256 without algorithm prefix) for backward compatibility
18+
//
19+
// Examples:
20+
// - "sha256:916f0027a575074ce72a331777c3478d6513f786a591bd892da1a577bf2335f9" → true
21+
// - "sha512:0e1e21ecf105ec853d24d728867ad70613c21663a4693074b2a3619c1bd39d66b588c33723bb466c72424e80e3ca63c249078ab347bab9428500e7ee43059d0d" → true
22+
// - "abcd1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab" → true (legacy)
23+
// - "sha256" → false (repository name)
24+
// - "sha512:latest" → false (repository with tag)
25+
// - "docker.io/sha256:latest" → false (repository with domain)
26+
func IsDigestReference(name string) bool {
27+
// First check if it's a valid digest format (algorithm:hash)
28+
if _, err := digest.Parse(name); err == nil {
29+
return true
30+
}
31+
32+
// Also check for the legacy 64-character hex format (SHA256 without algorithm prefix)
33+
// This maintains backward compatibility for existing deployments
34+
if len(name) == 64 && !strings.ContainsAny(name, "/.:@") {
35+
// Verify it's actually hex
36+
for _, c := range name {
37+
if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) {
38+
return false
39+
}
40+
}
41+
return true
42+
}
43+
44+
return false
45+
}
46+
47+
// ExtractAlgorithmFromDigest extracts the algorithm and hash from a digest string.
48+
// It expects input like "@sha256:abc123" or "@sha512:def456".
49+
// Returns (algorithm, hash) if successful, or ("", "") if parsing fails.
50+
//
51+
// This function validates that the extracted algorithm and hash form a valid digest.
52+
// It is useful for preserving the algorithm that was determined by functions like getImageID,
53+
// rather than overriding it with a globally configured algorithm.
54+
//
55+
// Examples:
56+
// - "@sha256:abc123" → ("sha256", "abc123") (if valid)
57+
// - "@sha512:def456" → ("sha512", "def456") (if valid)
58+
// - "sha256:abc123" → ("", "") (missing @ prefix)
59+
// - "@invalid" → ("", "") (missing colon)
60+
// - "@sha256:invalid" → ("", "") (invalid hash format)
61+
func ExtractAlgorithmFromDigest(digestStr string) (string, string) {
62+
if !strings.HasPrefix(digestStr, "@") {
63+
return "", ""
64+
}
65+
66+
// Remove the "@" prefix
67+
digestStr = digestStr[1:]
68+
69+
// Split on the first ":" to get algorithm:hash
70+
parts := strings.SplitN(digestStr, ":", 2)
71+
if len(parts) != 2 {
72+
return "", ""
73+
}
74+
75+
algorithm, hash := parts[0], parts[1]
76+
77+
// Validate that the algorithm and hash form a valid digest
78+
// This ensures we only return valid digest components
79+
if _, err := digest.Parse(algorithm + ":" + hash); err != nil {
80+
return "", ""
81+
}
82+
83+
return algorithm, hash
84+
}
85+
86+
// HasDigestPrefix checks if a string starts with any supported digest algorithm prefix.
87+
// This is more scalable than hardcoding multiple HasPrefix checks for individual algorithms.
88+
//
89+
// Examples:
90+
// - "sha256:abc123" → true
91+
// - "sha512:def456" → true
92+
// - "image:latest" → false
93+
// - "registry.io/repo" → false
94+
func HasDigestPrefix(s string) bool {
95+
// Check if the string starts with any supported digest algorithm
96+
// This is more efficient than checking each algorithm individually
97+
for _, prefix := range []string{"sha256:", "sha512:"} {
98+
if strings.HasPrefix(s, prefix) {
99+
return true
100+
}
101+
}
102+
return false
103+
}
104+
105+
// GetDigestPrefix returns the digest algorithm prefix if the string starts with one.
106+
// Returns the prefix (including colon) if found, empty string otherwise.
107+
//
108+
// Examples:
109+
// - "sha256:abc123" → "sha256:"
110+
// - "sha512:def456" → "sha512:"
111+
// - "image:latest" → ""
112+
func GetDigestPrefix(s string) string {
113+
prefixes := []string{"sha256:", "sha512:"}
114+
for _, prefix := range prefixes {
115+
if strings.HasPrefix(s, prefix) {
116+
return prefix
117+
}
118+
}
119+
return ""
120+
}
121+
122+
// TrimDigestPrefix removes the digest algorithm prefix from a string if present.
123+
// Returns the string without the prefix and a boolean indicating if a prefix was found.
124+
//
125+
// Examples:
126+
// - "sha256:abc123" → ("abc123", true)
127+
// - "sha512:def456" → ("def456", true)
128+
// - "image:latest" → ("image:latest", false)
129+
func TrimDigestPrefix(s string) (string, bool) {
130+
if prefix := GetDigestPrefix(s); prefix != "" {
131+
return strings.TrimPrefix(s, prefix), true
132+
}
133+
return s, false
134+
}

0 commit comments

Comments
 (0)