Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion common/libimage/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"time"

"github.com/sirupsen/logrus"
"go.podman.io/common/pkg/digestutils"
filtersPkg "go.podman.io/common/pkg/filters"
"go.podman.io/common/pkg/timetype"
"go.podman.io/image/v5/docker/reference"
Expand Down Expand Up @@ -481,7 +482,7 @@ func filterID(value string) filterFunc {

// filterDigest creates a digest filter for matching the specified value.
func filterDigest(value string) (filterFunc, error) {
if !strings.HasPrefix(value, "sha256:") {
if !digestutils.HasDigestPrefix(value) {
return nil, fmt.Errorf("invalid value %q for digest filter", value)
}
return func(img *Image, _ *layerTree) (bool, error) {
Expand Down
18 changes: 17 additions & 1 deletion common/libimage/filters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -169,19 +169,35 @@ func TestFilterDigest(t *testing.T) {
}{
{string(busybox.Digest()[:10]), 1, busybox.ID()},
{string(alpine.Digest()[:10]), 1, alpine.ID()},
// Test SHA512 digest prefix matching
{"sha512:1234567890abcdef", 0, ""}, // Non-existent SHA512 digest
{"sha256:1234567890abcdef", 0, ""}, // Non-existent SHA256 digest
} {
listOptions := &ListImagesOptions{
Filters: []string{"digest=" + test.filter},
}
listedImages, err := runtime.ListImages(ctx, listOptions)
require.NoError(t, err, "%v", test)
require.Len(t, listedImages, test.matches, "%s -> %v", test.filter, listedImages)
require.Equal(t, listedImages[0].ID(), test.id)
if test.matches > 0 {
require.Equal(t, listedImages[0].ID(), test.id)
}
}
_, err = runtime.ListImages(ctx, &ListImagesOptions{
Filters: []string{"digest=this-is-not-a-digest"},
})
assert.Error(t, err)

// Test invalid digest algorithms
_, err = runtime.ListImages(ctx, &ListImagesOptions{
Filters: []string{"digest=md5:1234567890abcdef"},
})
assert.Error(t, err)

_, err = runtime.ListImages(ctx, &ListImagesOptions{
Filters: []string{"digest=sha384:1234567890abcdef"},
})
assert.Error(t, err)
}

func TestFilterID(t *testing.T) {
Expand Down
7 changes: 4 additions & 3 deletions common/libimage/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
"go.podman.io/common/libimage/platform"
"go.podman.io/common/pkg/digestutils"
"go.podman.io/image/v5/docker/reference"
"go.podman.io/image/v5/image"
"go.podman.io/image/v5/manifest"
Expand Down Expand Up @@ -474,7 +475,7 @@ func (i *Image) removeRecursive(ctx context.Context, rmMap map[string]*RemoveIma
// error.
if referencedBy != "" && numNames != 1 {
byID := strings.HasPrefix(i.ID(), referencedBy)
byDigest := strings.HasPrefix(referencedBy, "sha256:")
byDigest := digestutils.HasDigestPrefix(referencedBy)
if !options.Force {
if byID && numNames > 1 {
return processedIDs, fmt.Errorf("unable to delete image %q by ID with more than one tag (%s): please force removal", i.ID(), i.Names())
Expand Down Expand Up @@ -577,7 +578,7 @@ var errTagDigest = errors.New("tag by digest not supported")
// Tag the image with the specified name and store it in the local containers
// storage. The name is normalized according to the rules of NormalizeName.
func (i *Image) Tag(name string) error {
if strings.HasPrefix(name, "sha256:") { // ambiguous input
if digestutils.HasDigestPrefix(name) { // ambiguous input
return fmt.Errorf("%s: %w", name, errTagDigest)
}

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

Expand Down
13 changes: 12 additions & 1 deletion common/libimage/import.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import (

v1 "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
"go.podman.io/common/pkg/digestutils"
"go.podman.io/common/pkg/download"
storageTransport "go.podman.io/image/v5/storage"
tarballTransport "go.podman.io/image/v5/tarball"
supportedDigests "go.podman.io/storage/pkg/supported-digests"
)

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

return "sha256:" + name, nil
// Extract the algorithm from the getImageID result
// getImageID returns something like "@sha256:abc123" or "@sha512:def456"
// We need to preserve the algorithm that was actually used
if algorithm, hash := digestutils.ExtractAlgorithmFromDigest(name); algorithm != "" {
return algorithm + ":" + hash, nil
}

// Fallback to configured algorithm if we can't parse the digest
digestAlgorithm := supportedDigests.TmpDigestForNewObjects()
return digestAlgorithm.String() + ":" + name, nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getImageID knows which algorithm was used, and it’s not necessarily this one. Applies in several other places.

}
50 changes: 44 additions & 6 deletions common/libimage/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
ociSpec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/sirupsen/logrus"
"go.podman.io/common/pkg/config"
"go.podman.io/common/pkg/digestutils"
registryTransport "go.podman.io/image/v5/docker"
dockerArchiveTransport "go.podman.io/image/v5/docker/archive"
dockerDaemonTransport "go.podman.io/image/v5/docker/daemon"
Expand All @@ -26,6 +27,7 @@ import (
"go.podman.io/image/v5/transports/alltransports"
"go.podman.io/image/v5/types"
"go.podman.io/storage"
supportedDigests "go.podman.io/storage/pkg/supported-digests"
)

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

// If the image clearly refers to a local one, we can look it up directly.
// In fact, we need to since they are not parseable.
if strings.HasPrefix(name, "sha256:") || (len(name) == 64 && !strings.ContainsAny(name, "/.:@")) {
if digestutils.IsDigestReference(name) {
if pullPolicy == config.PullPolicyAlways {
return nil, fmt.Errorf("pull policy is always but image has been referred to by ID (%s)", name)
}
Expand Down Expand Up @@ -261,7 +263,16 @@ func (r *Runtime) copyFromDefault(ctx context.Context, ref types.ImageReference,
if err != nil {
return nil, nil, err
}
imageName = "sha256:" + storageName[1:]
// Extract the algorithm from the getImageID result
// getImageID returns something like "@sha256:abc123" or "@sha512:def456"
// We need to preserve the algorithm that was actually used
if algorithm, hash := digestutils.ExtractAlgorithmFromDigest(storageName); algorithm != "" {
imageName = algorithm + ":" + hash
} else {
// Fallback to configured algorithm
digestAlgorithm := supportedDigests.TmpDigestForNewObjects()
imageName = digestAlgorithm.String() + ":" + storageName[1:]
}
} else { // If the OCI-reference includes an image reference, use it
storageName = refName
imageName = storageName
Expand All @@ -280,7 +291,16 @@ func (r *Runtime) copyFromDefault(ctx context.Context, ref types.ImageReference,
if err != nil {
return nil, nil, err
}
imageName = "sha256:" + storageName[1:]
// Extract the algorithm from the getImageID result
// getImageID returns something like "@sha256:abc123" or "@sha512:def456"
// We need to preserve the algorithm that was actually used
if algorithm, hash := digestutils.ExtractAlgorithmFromDigest(storageName); algorithm != "" {
imageName = algorithm + ":" + hash
} else {
// Fallback to configured algorithm
digestAlgorithm := supportedDigests.TmpDigestForNewObjects()
imageName = digestAlgorithm.String() + ":" + storageName[1:]
}
default:
named, err := NormalizeName(storageName)
if err != nil {
Expand All @@ -306,7 +326,16 @@ func (r *Runtime) copyFromDefault(ctx context.Context, ref types.ImageReference,
if err != nil {
return nil, nil, err
}
imageName = "sha256:" + storageName[1:]
// Extract the algorithm from the getImageID result
// getImageID returns something like "@sha256:abc123" or "@sha512:def456"
// We need to preserve the algorithm that was actually used
if algorithm, hash := digestutils.ExtractAlgorithmFromDigest(storageName); algorithm != "" {
imageName = algorithm + ":" + hash
} else {
// Fallback to configured algorithm
digestAlgorithm := supportedDigests.TmpDigestForNewObjects()
imageName = digestAlgorithm.String() + ":" + storageName[1:]
}
}

// Create a storage reference.
Expand Down Expand Up @@ -340,8 +369,17 @@ func (r *Runtime) storageReferencesReferencesFromArchiveReader(ctx context.Conte
}
destNames = append(destNames, destName)
// Make sure the image can be loaded after the pull by
// replacing the @ with sha256:.
imageNames = append(imageNames, "sha256:"+destName[1:])
// replacing the @ with the correct algorithm.
// Extract the algorithm from the getImageID result
// getImageID returns something like "@sha256:abc123" or "@sha512:def456"
// We need to preserve the algorithm that was actually used
if algorithm, hash := digestutils.ExtractAlgorithmFromDigest(destName); algorithm != "" {
imageNames = append(imageNames, algorithm+":"+hash)
} else {
// Fallback to configured algorithm
digestAlgorithm := supportedDigests.TmpDigestForNewObjects()
imageNames = append(imageNames, digestAlgorithm.String()+":"+destName[1:])
}
} else {
for i := range destNames {
ref, err := NormalizeName(destNames[i])
Expand Down
5 changes: 3 additions & 2 deletions common/libimage/runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"go.podman.io/common/libimage/define"
"go.podman.io/common/libimage/platform"
"go.podman.io/common/pkg/config"
"go.podman.io/common/pkg/digestutils"
"go.podman.io/image/v5/docker/reference"
"go.podman.io/image/v5/pkg/shortnames"
storageTransport "go.podman.io/image/v5/storage"
Expand Down Expand Up @@ -273,9 +274,9 @@ func (r *Runtime) LookupImage(name string, options *LookupImageOptions) (*Image,

byDigest := false
originalName := name
if strings.HasPrefix(name, "sha256:") {
if trimmed, found := digestutils.TrimDigestPrefix(name); found {
byDigest = true
name = strings.TrimPrefix(name, "sha256:")
name = trimmed
}
byFullID := reference.IsFullIdentifier(name)

Expand Down
134 changes: 134 additions & 0 deletions common/pkg/digestutils/digestutils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
//go:build !remote

package digestutils

import (
"strings"

"github.com/opencontainers/go-digest"
)

// IsDigestReference determines if the given name is a digest-based reference.
// This function properly detects digests using the go-digest library instead of
// hardcoded string prefixes, avoiding conflicts with repository names like "sha256" or "sha512".
//
// The function supports:
// - Standard digest formats (algorithm:hash) like "sha256:abc123..." or "sha512:def456..."
// - Legacy 64-character hex format (SHA256 without algorithm prefix) for backward compatibility
//
// Examples:
// - "sha256:916f0027a575074ce72a331777c3478d6513f786a591bd892da1a577bf2335f9" → true
// - "sha512:0e1e21ecf105ec853d24d728867ad70613c21663a4693074b2a3619c1bd39d66b588c33723bb466c72424e80e3ca63c249078ab347bab9428500e7ee43059d0d" → true
// - "abcd1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab" → true (legacy)
// - "sha256" → false (repository name)
// - "sha512:latest" → false (repository with tag)
// - "docker.io/sha256:latest" → false (repository with domain)
func IsDigestReference(name string) bool {
// First check if it's a valid digest format (algorithm:hash)
if _, err := digest.Parse(name); err == nil {
return true
}

// Also check for the legacy 64-character hex format (SHA256 without algorithm prefix)
// This maintains backward compatibility for existing deployments
if len(name) == 64 && !strings.ContainsAny(name, "/.:@") {
// Verify it's actually hex
for _, c := range name {
if (c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F') {
return false
}
}
return true
}

return false
}

// ExtractAlgorithmFromDigest extracts the algorithm and hash from a digest string.
// It expects input like "@sha256:abc123" or "@sha512:def456".
// Returns (algorithm, hash) if successful, or ("", "") if parsing fails.
//
// This function validates that the extracted algorithm and hash form a valid digest.
// It is useful for preserving the algorithm that was determined by functions like getImageID,
// rather than overriding it with a globally configured algorithm.
//
// Examples:
// - "@sha256:abc123" → ("sha256", "abc123") (if valid)
// - "@sha512:def456" → ("sha512", "def456") (if valid)
// - "sha256:abc123" → ("", "") (missing @ prefix)
// - "@invalid" → ("", "") (missing colon)
// - "@sha256:invalid" → ("", "") (invalid hash format)
func ExtractAlgorithmFromDigest(digestStr string) (string, string) {
if !strings.HasPrefix(digestStr, "@") {
return "", ""
}

// Remove the "@" prefix
digestStr = digestStr[1:]

// Split on the first ":" to get algorithm:hash
parts := strings.SplitN(digestStr, ":", 2)
if len(parts) != 2 {
return "", ""
}

algorithm, hash := parts[0], parts[1]

// Validate that the algorithm and hash form a valid digest
// This ensures we only return valid digest components
if _, err := digest.Parse(algorithm + ":" + hash); err != nil {
return "", ""
}

return algorithm, hash
}

// HasDigestPrefix checks if a string starts with any supported digest algorithm prefix.
// This is more scalable than hardcoding multiple HasPrefix checks for individual algorithms.
//
// Examples:
// - "sha256:abc123" → true
// - "sha512:def456" → true
// - "image:latest" → false
// - "registry.io/repo" → false
func HasDigestPrefix(s string) bool {
// Check if the string starts with any supported digest algorithm
// This is more efficient than checking each algorithm individually
for _, prefix := range []string{"sha256:", "sha512:"} {
if strings.HasPrefix(s, prefix) {
return true
}
}
return false
}

// GetDigestPrefix returns the digest algorithm prefix if the string starts with one.
// Returns the prefix (including colon) if found, empty string otherwise.
//
// Examples:
// - "sha256:abc123" → "sha256:"
// - "sha512:def456" → "sha512:"
// - "image:latest" → ""
func GetDigestPrefix(s string) string {
prefixes := []string{"sha256:", "sha512:"}
for _, prefix := range prefixes {
if strings.HasPrefix(s, prefix) {
return prefix
}
}
return ""
}

// TrimDigestPrefix removes the digest algorithm prefix from a string if present.
// Returns the string without the prefix and a boolean indicating if a prefix was found.
//
// Examples:
// - "sha256:abc123" → ("abc123", true)
// - "sha512:def456" → ("def456", true)
// - "image:latest" → ("image:latest", false)
func TrimDigestPrefix(s string) (string, bool) {
if prefix := GetDigestPrefix(s); prefix != "" {
return strings.TrimPrefix(s, prefix), true
}
return s, false
}
Loading
Loading