From 09fcf103eca1640f2fb6c1062b5902e5989b469a Mon Sep 17 00:00:00 2001 From: Lokesh Mandvekar Date: Wed, 10 Dec 2025 14:52:15 -0500 Subject: [PATCH 1/3] [WIP] vendor: update container-libs/image Signed-off-by: Lokesh Mandvekar --- go.mod | 10 +- go.sum | 26 ++-- vendor/go.podman.io/image/v5/copy/blob.go | 20 ++- vendor/go.podman.io/image/v5/copy/copy.go | 39 +++++ vendor/go.podman.io/image/v5/copy/single.go | 61 +++++++- .../image/v5/directory/directory_dest.go | 13 +- .../image/v5/directory/directory_transport.go | 16 +- .../image/v5/internal/digests/digests.go | 146 ++++++++++++++++++ .../internal/imagedestination/impl/compat.go | 3 + .../v5/internal/imagedestination/wrapper.go | 10 ++ .../image/v5/internal/private/private.go | 7 +- .../internal/putblobdigest/put_blob_digest.go | 19 ++- vendor/golang.org/x/sync/errgroup/errgroup.go | 4 +- vendor/modules.txt | 12 +- 14 files changed, 344 insertions(+), 42 deletions(-) create mode 100644 vendor/go.podman.io/image/v5/internal/digests/digests.go diff --git a/go.mod b/go.mod index 2f80480e16..7974fb2b86 100644 --- a/go.mod +++ b/go.mod @@ -99,14 +99,16 @@ require ( go.opentelemetry.io/otel v1.38.0 // indirect go.opentelemetry.io/otel/metric v1.38.0 // indirect go.opentelemetry.io/otel/trace v1.38.0 // indirect - golang.org/x/crypto v0.45.0 // indirect + golang.org/x/crypto v0.46.0 // indirect golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sync v0.18.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/text v0.32.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20250929231259-57b25ae835d4 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect google.golang.org/grpc v1.76.0 // indirect google.golang.org/protobuf v1.36.10 // indirect ) + +replace go.podman.io/image/v5 => /home/lsm5/repositories/containers/container-libs/image/ diff --git a/go.sum b/go.sum index 3fad2a1909..a19a4a368f 100644 --- a/go.sum +++ b/go.sum @@ -235,8 +235,6 @@ go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJr go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.podman.io/common v0.66.2-0.20251209145320-afd10d86c8a5 h1:kVB7HBNWDm7sVSyUBG037I/6mhb2eVHfSAoMW7aH6wE= go.podman.io/common v0.66.2-0.20251209145320-afd10d86c8a5/go.mod h1:59YHuY+GmyHbxc3DI+i9aaPlCPZGFBs/UOACpeMREik= -go.podman.io/image/v5 v5.38.1-0.20251209145320-afd10d86c8a5 h1:ydoO4pr7/hQkJxBDqoQc92Glzlt3Z66o9gul39wJ3SQ= -go.podman.io/image/v5 v5.38.1-0.20251209145320-afd10d86c8a5/go.mod h1:r0hBt77AwD5hfGm1RaCG0bYnAdTSAmELZ7yV+FtlhRw= go.podman.io/storage v1.61.1-0.20251209145320-afd10d86c8a5 h1:QNtB45DWrrh/wN5TxU236qcnzMDotcG4MFVgkMfazTk= go.podman.io/storage v1.61.1-0.20251209145320-afd10d86c8a5/go.mod h1:0Sw1WjfSbddrsjwiPIc/PGTp4y+qE96MV1zwVnguTCo= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -247,15 +245,15 @@ golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliY golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA= -golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -266,8 +264,8 @@ golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -275,8 +273,8 @@ golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= -golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -311,16 +309,16 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= -golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ= -golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= diff --git a/vendor/go.podman.io/image/v5/copy/blob.go b/vendor/go.podman.io/image/v5/copy/blob.go index 9db6338d75..f9d58c9791 100644 --- a/vendor/go.podman.io/image/v5/copy/blob.go +++ b/vendor/go.podman.io/image/v5/copy/blob.go @@ -7,6 +7,7 @@ import ( "io" "github.com/sirupsen/logrus" + "go.podman.io/image/v5/internal/digests" "go.podman.io/image/v5/internal/private" compressiontypes "go.podman.io/image/v5/pkg/compression/types" "go.podman.io/image/v5/types" @@ -101,6 +102,11 @@ func (ic *imageCopier) copyBlobFromStream(ctx context.Context, srcReader io.Read Cache: ic.c.blobInfoCache, IsConfig: isConfig, EmptyLayer: emptyLayer, + Digests: ic.c.options.digestOptions, + // CannotChangeDigestReason requires stream.info.Digest to always be set, and it is: + // If ic.cannotModifyManifestReason, stream.info was not modified since its initialization at the top of this + // function, and the caller is required to provide a digest. + CannotChangeDigestReason: ic.cannotModifyManifestReason, } if !isConfig { options.LayerIndex = &layerIndex @@ -132,8 +138,20 @@ func (ic *imageCopier) copyBlobFromStream(ctx context.Context, srcReader io.Read if digestingReader.validationFailed { // Coverage: This should never happen. return types.BlobInfo{}, fmt.Errorf("Internal error writing blob %s, digest verification failed but was ignored", srcInfo.Digest) } + // Verify that the uploaded digest is compatible with our digest options if stream.info.Digest != "" && uploadedInfo.Digest != stream.info.Digest { - return types.BlobInfo{}, fmt.Errorf("Internal error writing blob %s, blob with digest %s saved with digest %s", srcInfo.Digest, stream.info.Digest, uploadedInfo.Digest) + // Use Miloslav's digest infrastructure to determine if this digest change is acceptable + expectedAlgo, err := ic.c.options.digestOptions.Choose(digests.Situation{ + Preexisting: stream.info.Digest, + CannotChangeAlgorithmReason: ic.cannotModifyManifestReason, + }) + if err != nil { + return types.BlobInfo{}, err + } + // If we're forcing a different algorithm and the uploaded digest uses that algorithm, it's acceptable + if uploadedInfo.Digest.Algorithm() != expectedAlgo { + return types.BlobInfo{}, fmt.Errorf("Internal error writing blob %s, blob with digest %s saved with digest %s", srcInfo.Digest, stream.info.Digest, uploadedInfo.Digest) + } } if digestingReader.validationSucceeded { if err := compressionStep.recordValidatedDigestData(ic.c, uploadedInfo, srcInfo, encryptionStep, decryptionStep); err != nil { diff --git a/vendor/go.podman.io/image/v5/copy/copy.go b/vendor/go.podman.io/image/v5/copy/copy.go index eed5f8d96d..a0377a7cdd 100644 --- a/vendor/go.podman.io/image/v5/copy/copy.go +++ b/vendor/go.podman.io/image/v5/copy/copy.go @@ -14,6 +14,7 @@ import ( "github.com/sirupsen/logrus" "go.podman.io/image/v5/docker/reference" internalblobinfocache "go.podman.io/image/v5/internal/blobinfocache" + "go.podman.io/image/v5/internal/digests" "go.podman.io/image/v5/internal/image" "go.podman.io/image/v5/internal/imagedestination" "go.podman.io/image/v5/internal/imagesource" @@ -155,6 +156,35 @@ type Options struct { // In oci-archive: destinations, this will set the create/mod/access timestamps in each tar entry // (but not a timestamp of the created archive file). DestinationTimestamp *time.Time + + // FIXME: + // - this reference to an internal type is unusable from the outside even if we made the field public + // - what is the actual semantics? Right now it is probably “choices to use when writing to the destination”, TBD + // - anyway do we want to expose _all_ of the digests.Options tunables, or fewer? + // - … do we want to expose _more_ granularity than that? + // - (“must have at least sha512 integrity when reading”, what does “at least” mean for random pairs of algorithms?) + // - should some of this be in config files, maybe ever per-registry? + digestOptions digests.Options +} + +// SetDigestOptions sets the digest options for this copy operation. +// This allows callers to force the use of a specific digest algorithm. +func (o *Options) SetDigestOptions(opts digests.Options) { + o.digestOptions = opts +} + +// SetForceDigestAlgorithm forces the use of a specific digest algorithm for this copy operation. +// algo should be one of "sha256", "sha384", or "sha512". +func (o *Options) SetForceDigestAlgorithm(algo digest.Algorithm) error { + if !algo.Available() { + return fmt.Errorf("digest algorithm %q is not available", algo.String()) + } + digestOpts, err := digests.MustUse(algo) + if err != nil { + return fmt.Errorf("failed to set force-digest algorithm: %w", err) + } + o.digestOptions = digestOpts + return nil } // OptionCompressionVariant allows to supply information about @@ -200,6 +230,15 @@ func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef, if options == nil { options = &Options{} } + // FIXME: Currently, digestsOptions is not implemented at all, and exists in the codebase + // only to allow gradually building the feature set. + // After c/image/copy consistently implements it, provide a public digest options API of some kind. + optionsCopy := *options + // Only set default if not already configured + if optionsCopy.digestOptions.MustUseSet() == "" { + optionsCopy.digestOptions = digests.CanonicalDefault() + } + options = &optionsCopy if err := validateImageListSelection(options.ImageListSelection); err != nil { return nil, err diff --git a/vendor/go.podman.io/image/v5/copy/single.go b/vendor/go.podman.io/image/v5/copy/single.go index 5a5d5e3ddb..273199bfdf 100644 --- a/vendor/go.podman.io/image/v5/copy/single.go +++ b/vendor/go.podman.io/image/v5/copy/single.go @@ -592,10 +592,20 @@ func (ic *imageCopier) copyUpdatedConfigAndManifest(ctx context.Context, instanc return nil, "", fmt.Errorf("reading manifest: %w", err) } - if err := ic.copyConfig(ctx, pendingImage); err != nil { + newConfigDigest, err := ic.copyConfig(ctx, pendingImage) + if err != nil { return nil, "", err } + // If the config digest changed (due to forcing a different digest algorithm), + // update it in the manifest using typed manifest structures + if newConfigDigest != nil { + man, err = ic.updateManifestConfigDigest(man, pendingImage, *newConfigDigest) + if err != nil { + return nil, "", fmt.Errorf("updating manifest config digest: %w", err) + } + } + ic.c.Printf("Writing manifest to image destination\n") manifestDigest, err := manifest.Digest(man) if err != nil { @@ -611,13 +621,45 @@ func (ic *imageCopier) copyUpdatedConfigAndManifest(ctx context.Context, instanc return man, manifestDigest, nil } +// updateManifestConfigDigest updates the config digest in a manifest blob using typed manifest structures. +// This leverages the existing manifest parsing and serialization infrastructure. +func (ic *imageCopier) updateManifestConfigDigest(manifestBlob []byte, src types.Image, newConfigDigest digest.Digest) ([]byte, error) { + // Get the manifest MIME type to parse it correctly + _, mt, err := src.Manifest(context.Background()) + if err != nil { + return nil, fmt.Errorf("getting manifest type: %w", err) + } + + // Parse the manifest using the typed manifest infrastructure + m, err := manifest.FromBlob(manifestBlob, mt) + if err != nil { + return nil, fmt.Errorf("parsing manifest: %w", err) + } + + // Update the config digest based on manifest type + switch typedManifest := m.(type) { + case *manifest.OCI1: + typedManifest.Config.Digest = newConfigDigest + return typedManifest.Serialize() + case *manifest.Schema2: + typedManifest.ConfigDescriptor.Digest = newConfigDigest + return typedManifest.Serialize() + case *manifest.Schema1: + // Schema1 doesn't have a separate config blob, so this shouldn't happen + return nil, fmt.Errorf("cannot update config digest for schema1 manifest") + default: + return nil, fmt.Errorf("unsupported manifest type for config digest update: %T", m) + } +} + // copyConfig copies config.json, if any, from src to dest. -func (ic *imageCopier) copyConfig(ctx context.Context, src types.Image) error { +// It returns the new config digest if it changed (due to digest algorithm forcing), or nil otherwise. +func (ic *imageCopier) copyConfig(ctx context.Context, src types.Image) (*digest.Digest, error) { srcInfo := src.ConfigInfo() if srcInfo.Digest != "" { if err := ic.c.concurrentBlobCopiesSemaphore.Acquire(ctx, 1); err != nil { // This can only fail with ctx.Err(), so no need to blame acquiring the semaphore. - return fmt.Errorf("copying config: %w", err) + return nil, fmt.Errorf("copying config: %w", err) } defer ic.c.concurrentBlobCopiesSemaphore.Release(1) @@ -645,13 +687,20 @@ func (ic *imageCopier) copyConfig(ctx context.Context, src types.Image) error { return destInfo, nil }() if err != nil { - return err + return nil, err } if destInfo.Digest != srcInfo.Digest { - return fmt.Errorf("Internal error: copying uncompressed config blob %s changed digest to %s", srcInfo.Digest, destInfo.Digest) + // Allow digest algorithm changes when forcing a specific digest algorithm + forcingDifferentAlgo := ic.c.options.digestOptions.MustUseSet() != "" && + destInfo.Digest.Algorithm() != srcInfo.Digest.Algorithm() + if !forcingDifferentAlgo { + return nil, fmt.Errorf("Internal error: copying uncompressed config blob %s changed digest to %s", srcInfo.Digest, destInfo.Digest) + } + // Return the new digest so the manifest can be updated + return &destInfo.Digest, nil } } - return nil + return nil, nil } // diffIDResult contains both a digest value and an error from diffIDComputationGoroutine. diff --git a/vendor/go.podman.io/image/v5/directory/directory_dest.go b/vendor/go.podman.io/image/v5/directory/directory_dest.go index 5c704e8027..0d7b08a912 100644 --- a/vendor/go.podman.io/image/v5/directory/directory_dest.go +++ b/vendor/go.podman.io/image/v5/directory/directory_dest.go @@ -11,6 +11,7 @@ import ( "github.com/opencontainers/go-digest" "github.com/sirupsen/logrus" + "go.podman.io/image/v5/internal/digests" "go.podman.io/image/v5/internal/imagedestination/impl" "go.podman.io/image/v5/internal/imagedestination/stubs" "go.podman.io/image/v5/internal/private" @@ -150,7 +151,11 @@ func (d *dirImageDestination) PutBlobWithOptions(ctx context.Context, stream io. } }() - digester, stream := putblobdigest.DigestIfUnknown(stream, inputInfo) + algorithm, err := options.Digests.Choose(digests.Situation{Preexisting: inputInfo.Digest, CannotChangeAlgorithmReason: options.CannotChangeDigestReason}) + if err != nil { + return private.UploadedBlob{}, err + } + digester, stream := putblobdigest.DigestIfAlgorithmUnknown(stream, inputInfo, algorithm) // TODO: This can take quite some time, and should ideally be cancellable using ctx.Done(). size, err := io.Copy(blobFile, stream) @@ -227,6 +232,9 @@ func (d *dirImageDestination) TryReusingBlobWithOptions(ctx context.Context, inf // If the destination is in principle available, refuses this manifest type (e.g. it does not recognize the schema), // but may accept a different manifest type, the returned error must be an ManifestTypeRejectedError. func (d *dirImageDestination) PutManifest(ctx context.Context, manifest []byte, instanceDigest *digest.Digest) error { + if instanceDigest != nil && instanceDigest.Algorithm() != digest.Canonical { // compare the special case in manifestPath + d.usesNonSHA256Digest = true + } path, err := d.ref.manifestPath(instanceDigest) if err != nil { return err @@ -239,6 +247,9 @@ func (d *dirImageDestination) PutManifest(ctx context.Context, manifest []byte, // (when the primary manifest is a manifest list); this should always be nil if the primary manifest is not a manifest list. // MUST be called after PutManifest (signatures may reference manifest contents). func (d *dirImageDestination) PutSignaturesWithFormat(ctx context.Context, signatures []signature.Signature, instanceDigest *digest.Digest) error { + if instanceDigest != nil && instanceDigest.Algorithm() != digest.Canonical { // compare the special case in signaturePath + d.usesNonSHA256Digest = true + } for i, sig := range signatures { blob, err := signature.Blob(sig) if err != nil { diff --git a/vendor/go.podman.io/image/v5/directory/directory_transport.go b/vendor/go.podman.io/image/v5/directory/directory_transport.go index 165159e588..6275c9cebe 100644 --- a/vendor/go.podman.io/image/v5/directory/directory_transport.go +++ b/vendor/go.podman.io/image/v5/directory/directory_transport.go @@ -166,7 +166,13 @@ func (ref dirReference) manifestPath(instanceDigest *digest.Digest) (string, err if err := instanceDigest.Validate(); err != nil { // digest.Digest.Encoded() panics on failure, and could possibly result in a path with ../, so validate explicitly. return "", err } - return filepath.Join(ref.path, instanceDigest.Encoded()+".manifest.json"), nil + var filename string + if instanceDigest.Algorithm() == digest.Canonical { + filename = instanceDigest.Encoded() + ".manifest.json" + } else { + filename = instanceDigest.Algorithm().String() + "-" + instanceDigest.Encoded() + ".manifest.json" + } + return filepath.Join(ref.path, filename), nil } return filepath.Join(ref.path, "manifest.json"), nil } @@ -193,7 +199,13 @@ func (ref dirReference) signaturePath(index int, instanceDigest *digest.Digest) if err := instanceDigest.Validate(); err != nil { // digest.Digest.Encoded() panics on failure, and could possibly result in a path with ../, so validate explicitly. return "", err } - return filepath.Join(ref.path, fmt.Sprintf(instanceDigest.Encoded()+".signature-%d", index+1)), nil + var prefix string + if instanceDigest.Algorithm() == digest.Canonical { + prefix = instanceDigest.Encoded() + } else { + prefix = instanceDigest.Algorithm().String() + "-" + instanceDigest.Encoded() + } + return filepath.Join(ref.path, fmt.Sprintf(prefix+".signature-%d", index+1)), nil } return filepath.Join(ref.path, fmt.Sprintf("signature-%d", index+1)), nil } diff --git a/vendor/go.podman.io/image/v5/internal/digests/digests.go b/vendor/go.podman.io/image/v5/internal/digests/digests.go new file mode 100644 index 0000000000..ca76ea3be7 --- /dev/null +++ b/vendor/go.podman.io/image/v5/internal/digests/digests.go @@ -0,0 +1,146 @@ +// Package digests provides an internal representation of users’ digest use preferences. +// +// Something like this _might_ be eventually made available as a public API: +// before doing so, carefully think whether the API should be modified before we commit to it. + +package digests + +import ( + "errors" + "fmt" + + "github.com/opencontainers/go-digest" +) + +// Options records users’ preferences for used digest algorithm usage. +// It is a value type and can be copied using ordinary assignment. +// +// It can only be created using one of the provided constructors. +type Options struct { + initialized bool // To prevent uses that don’t call a public constructor; this is necessary to enforce the .Available() promise. + + // If any of the fields below is set, it is guaranteed to be .Available(). + + mustUse digest.Algorithm // If not "", written digests must use this algorithm. + prefer digest.Algorithm // If not "", use this algorithm whenever possible. + defaultAlgo digest.Algorithm // If not "", use this algorithm if there is no reason to use anything else. +} + +// CanonicalDefault is Options which default to using digest.Canonical if there is no reason to use a different algorithm +// (e.g. when there is no pre-existing digest). +// +// The configuration can be customized using .WithPreferred() or .WithDefault(). +func CanonicalDefault() Options { + // This does not set .defaultAlgo so that .WithDefault() can be called (once). + return Options{ + initialized: true, + } +} + +// MustUse constructs Options which always use algo. +func MustUse(algo digest.Algorithm) (Options, error) { + // We don’t provide Options.WithMustUse because there is no other option that makes a difference + // once .mustUse is set. + if !algo.Available() { + return Options{}, fmt.Errorf("attempt to use an unavailable digest algorithm %q", algo.String()) + } + return Options{ + initialized: true, + mustUse: algo, + }, nil +} + +// WithPreferred returns a copy of o with a “preferred” algorithm set to algo. +// The preferred algorithm is used whenever possible (but if there is a strict requirement to use something else, it will be overridden). +func (o Options) WithPreferred(algo digest.Algorithm) (Options, error) { + if err := o.ensureInitialized(); err != nil { + return Options{}, err + } + if o.prefer != "" { + return Options{}, errors.New("digests.Options already have a 'prefer' algorithm configured") + } + + if !algo.Available() { + return Options{}, fmt.Errorf("attempt to use an unavailable digest algorithm %q", algo.String()) + } + o.prefer = algo + return o, nil +} + +// WithDefault returns a copy of o with a “default” algorithm set to algo. +// The default algorithm is used if there is no reason to use anything else (e.g. when there is no pre-existing digest). +func (o Options) WithDefault(algo digest.Algorithm) (Options, error) { + if err := o.ensureInitialized(); err != nil { + return Options{}, err + } + if o.defaultAlgo != "" { + return Options{}, errors.New("digests.Options already have a 'default' algorithm configured") + } + + if !algo.Available() { + return Options{}, fmt.Errorf("attempt to use an unavailable digest algorithm %q", algo.String()) + } + o.defaultAlgo = algo + return o, nil +} + +// ensureInitialized returns an error if o is not initialized. +func (o Options) ensureInitialized() error { + if !o.initialized { + return errors.New("internal error: use of uninitialized digests.Options") + } + return nil +} + +// Situation records the context in which a digest is being chosen. +type Situation struct { + Preexisting digest.Digest // If not "", a pre-existing digest value (frequently one which is cheaper to use than others) + CannotChangeAlgorithmReason string // The reason why we must use Preexisting, or "" if we can use other algorithms. +} + +// Choose chooses a digest algorithm based on the options and the situation. +func (o Options) Choose(s Situation) (digest.Algorithm, error) { + if err := o.ensureInitialized(); err != nil { + return "", err + } + + if s.CannotChangeAlgorithmReason != "" && s.Preexisting == "" { + return "", fmt.Errorf("internal error: digests.Situation.CannotChangeAlgorithmReason is set but Preexisting is empty") + } + + var choice digest.Algorithm // = what we want to use + switch { + case o.mustUse != "": + choice = o.mustUse + case s.CannotChangeAlgorithmReason != "": + choice = s.Preexisting.Algorithm() + if !choice.Available() { + return "", fmt.Errorf("existing digest uses unimplemented algorithm %s", choice) + } + case o.prefer != "": + choice = o.prefer + case s.Preexisting != "" && s.Preexisting.Algorithm().Available(): + choice = s.Preexisting.Algorithm() + case o.defaultAlgo != "": + choice = o.defaultAlgo + default: + choice = digest.Canonical // We assume digest.Canonical is always available. + } + + if s.CannotChangeAlgorithmReason != "" && choice != s.Preexisting.Algorithm() { + return "", fmt.Errorf("requested to always use digest algorithm %s but we cannot replace existing digest algorithm %s: %s", + choice, s.Preexisting.Algorithm(), s.CannotChangeAlgorithmReason) + } + + return choice, nil +} + +// MustUseSet returns an algorithm if o is set to always use a specific algorithm, "" if it is flexible. +func (o Options) MustUseSet() digest.Algorithm { + // We don’t do .ensureInitialized() because that would require an extra error value just for that. + // This should not be a part of any public API either way. + if o.mustUse != "" { + return o.mustUse + } + return "" +} diff --git a/vendor/go.podman.io/image/v5/internal/imagedestination/impl/compat.go b/vendor/go.podman.io/image/v5/internal/imagedestination/impl/compat.go index 9a8d187138..f77914657f 100644 --- a/vendor/go.podman.io/image/v5/internal/imagedestination/impl/compat.go +++ b/vendor/go.podman.io/image/v5/internal/imagedestination/impl/compat.go @@ -6,6 +6,7 @@ import ( "github.com/opencontainers/go-digest" "go.podman.io/image/v5/internal/blobinfocache" + "go.podman.io/image/v5/internal/digests" "go.podman.io/image/v5/internal/private" "go.podman.io/image/v5/internal/signature" "go.podman.io/image/v5/types" @@ -46,6 +47,8 @@ func (c *Compat) PutBlob(ctx context.Context, stream io.Reader, inputInfo types. res, err := c.dest.PutBlobWithOptions(ctx, stream, inputInfo, private.PutBlobOptions{ Cache: blobinfocache.FromBlobInfoCache(cache), IsConfig: isConfig, + + Digests: digests.CanonicalDefault(), }) if err != nil { return types.BlobInfo{}, err diff --git a/vendor/go.podman.io/image/v5/internal/imagedestination/wrapper.go b/vendor/go.podman.io/image/v5/internal/imagedestination/wrapper.go index cbbb6b42a5..5496c900aa 100644 --- a/vendor/go.podman.io/image/v5/internal/imagedestination/wrapper.go +++ b/vendor/go.podman.io/image/v5/internal/imagedestination/wrapper.go @@ -5,6 +5,7 @@ import ( "io" "github.com/opencontainers/go-digest" + "go.podman.io/image/v5/internal/digests" "go.podman.io/image/v5/internal/imagedestination/stubs" "go.podman.io/image/v5/internal/private" "go.podman.io/image/v5/internal/signature" @@ -53,6 +54,15 @@ func (w *wrapped) PutBlobWithOptions(ctx context.Context, stream io.Reader, inpu if err != nil { return private.UploadedBlob{}, err } + // Check that the returned digest is compatible with options.Digests. If it isn’t, there’s nothing we can do, but at least the callers of PutBlobWithOptions + // won’t need to double-check. + if _, err := options.Digests.Choose(digests.Situation{ + Preexisting: res.Digest, + CannotChangeAlgorithmReason: "external transport API does not allow choosing a digest algorithm", + }); err != nil { + return private.UploadedBlob{}, err + } + return private.UploadedBlob{ Digest: res.Digest, Size: res.Size, diff --git a/vendor/go.podman.io/image/v5/internal/private/private.go b/vendor/go.podman.io/image/v5/internal/private/private.go index a5d2057aef..e9fab8060c 100644 --- a/vendor/go.podman.io/image/v5/internal/private/private.go +++ b/vendor/go.podman.io/image/v5/internal/private/private.go @@ -9,6 +9,7 @@ import ( imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/internal/blobinfocache" + "go.podman.io/image/v5/internal/digests" "go.podman.io/image/v5/internal/signature" compression "go.podman.io/image/v5/pkg/compression/types" "go.podman.io/image/v5/types" @@ -111,8 +112,10 @@ type PutBlobOptions struct { // if they use internal/imagedestination/impl.Compat; // in that case, they will all be consistently zero-valued. - EmptyLayer bool // True if the blob is an "empty"/"throwaway" layer, and may not necessarily be physically represented. - LayerIndex *int // If the blob is a layer, a zero-based index of the layer within the image; nil otherwise. + EmptyLayer bool // True if the blob is an "empty"/"throwaway" layer, and may not necessarily be physically represented. + LayerIndex *int // If the blob is a layer, a zero-based index of the layer within the image; nil otherwise. + Digests digests.Options // Unlike other private fields, this is always initialized, and mandatory requests are enforced by the compatibility wrapper. + CannotChangeDigestReason string // The reason why PutBlobWithOptions is provided with a digest and the destination must use precisely that one (in particular, use that algorithm). } // PutBlobPartialOptions are used in PutBlobPartial. diff --git a/vendor/go.podman.io/image/v5/internal/putblobdigest/put_blob_digest.go b/vendor/go.podman.io/image/v5/internal/putblobdigest/put_blob_digest.go index ce50542751..865ee8abcd 100644 --- a/vendor/go.podman.io/image/v5/internal/putblobdigest/put_blob_digest.go +++ b/vendor/go.podman.io/image/v5/internal/putblobdigest/put_blob_digest.go @@ -13,15 +13,15 @@ type Digester struct { digester digest.Digester // Or nil } -// newDigester initiates computation of a digest.Canonical digest of stream, +// newDigester initiates computation of a digest of stream using the specified algorithm, // if !validDigest; otherwise it just records knownDigest to be returned later. // The caller MUST use the returned stream instead of the original value. -func newDigester(stream io.Reader, knownDigest digest.Digest, validDigest bool) (Digester, io.Reader) { +func newDigester(stream io.Reader, knownDigest digest.Digest, validDigest bool, algorithm digest.Algorithm) (Digester, io.Reader) { if validDigest { return Digester{knownDigest: knownDigest}, stream } else { res := Digester{ - digester: digest.Canonical.Digester(), + digester: algorithm.Digester(), } stream = io.TeeReader(stream, res.digester.Hash()) return res, stream @@ -34,7 +34,7 @@ func newDigester(stream io.Reader, knownDigest digest.Digest, validDigest bool) // The caller MUST use the returned stream instead of the original value. func DigestIfUnknown(stream io.Reader, blobInfo types.BlobInfo) (Digester, io.Reader) { d := blobInfo.Digest - return newDigester(stream, d, d != "") + return newDigester(stream, d, d != "", digest.Canonical) } // DigestIfCanonicalUnknown initiates computation of a digest.Canonical digest of stream, @@ -43,7 +43,16 @@ func DigestIfUnknown(stream io.Reader, blobInfo types.BlobInfo) (Digester, io.Re // The caller MUST use the returned stream instead of the original value. func DigestIfCanonicalUnknown(stream io.Reader, blobInfo types.BlobInfo) (Digester, io.Reader) { d := blobInfo.Digest - return newDigester(stream, d, d != "" && d.Algorithm() == digest.Canonical) + return newDigester(stream, d, d != "" && d.Algorithm() == digest.Canonical, digest.Canonical) +} + +// DigestIfAlgorithmUnknown initiates computation of a digest of stream, +// if a digest of the specified algorithm is not supplied in the provided blobInfo; +// otherwise blobInfo.Digest will be used. +// The caller MUST use the returned stream instead of the original value. +func DigestIfAlgorithmUnknown(stream io.Reader, blobInfo types.BlobInfo, algorithm digest.Algorithm) (Digester, io.Reader) { + d := blobInfo.Digest + return newDigester(stream, d, d != "" && d.Algorithm() == algorithm, algorithm) } // Digest() returns a digest value possibly computed by Digester. diff --git a/vendor/golang.org/x/sync/errgroup/errgroup.go b/vendor/golang.org/x/sync/errgroup/errgroup.go index 2f45dbc86e..f69fd75468 100644 --- a/vendor/golang.org/x/sync/errgroup/errgroup.go +++ b/vendor/golang.org/x/sync/errgroup/errgroup.go @@ -144,8 +144,8 @@ func (g *Group) SetLimit(n int) { g.sem = nil return } - if len(g.sem) != 0 { - panic(fmt.Errorf("errgroup: modify limit while %v goroutines in the group are still active", len(g.sem))) + if active := len(g.sem); active != 0 { + panic(fmt.Errorf("errgroup: modify limit while %v goroutines in the group are still active", active)) } g.sem = make(chan token, n) } diff --git a/vendor/modules.txt b/vendor/modules.txt index bc20774adf..c4a2ea9df6 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -367,7 +367,7 @@ go.podman.io/common/pkg/password go.podman.io/common/pkg/report go.podman.io/common/pkg/report/camelcase go.podman.io/common/pkg/retry -# go.podman.io/image/v5 v5.38.1-0.20251209145320-afd10d86c8a5 +# go.podman.io/image/v5 v5.38.1-0.20251209145320-afd10d86c8a5 => /home/lsm5/repositories/containers/container-libs/image/ ## explicit; go 1.24.6 go.podman.io/image/v5/copy go.podman.io/image/v5/directory @@ -380,6 +380,7 @@ go.podman.io/image/v5/docker/policyconfiguration go.podman.io/image/v5/docker/reference go.podman.io/image/v5/image go.podman.io/image/v5/internal/blobinfocache +go.podman.io/image/v5/internal/digests go.podman.io/image/v5/internal/image go.podman.io/image/v5/internal/imagedestination go.podman.io/image/v5/internal/imagedestination/impl @@ -488,7 +489,7 @@ go.podman.io/storage/pkg/tarlog go.podman.io/storage/pkg/truncindex go.podman.io/storage/pkg/unshare go.podman.io/storage/types -# golang.org/x/crypto v0.45.0 +# golang.org/x/crypto v0.46.0 ## explicit; go 1.24.0 golang.org/x/crypto/cast5 golang.org/x/crypto/cryptobyte @@ -515,11 +516,11 @@ golang.org/x/net/idna golang.org/x/net/internal/httpcommon golang.org/x/net/internal/timeseries golang.org/x/net/trace -# golang.org/x/oauth2 v0.33.0 +# golang.org/x/oauth2 v0.34.0 ## explicit; go 1.24.0 golang.org/x/oauth2 golang.org/x/oauth2/internal -# golang.org/x/sync v0.18.0 +# golang.org/x/sync v0.19.0 ## explicit; go 1.24.0 golang.org/x/sync/errgroup golang.org/x/sync/semaphore @@ -532,7 +533,7 @@ golang.org/x/sys/windows # golang.org/x/term v0.38.0 ## explicit; go 1.24.0 golang.org/x/term -# golang.org/x/text v0.31.0 +# golang.org/x/text v0.32.0 ## explicit; go 1.24.0 golang.org/x/text/feature/plural golang.org/x/text/internal @@ -661,3 +662,4 @@ google.golang.org/protobuf/types/known/timestamppb # gopkg.in/yaml.v3 v3.0.1 ## explicit gopkg.in/yaml.v3 +# go.podman.io/image/v5 => /home/lsm5/repositories/containers/container-libs/image/ From 1cea45bba5f8a8888c40c4241f74808051ca221b Mon Sep 17 00:00:00 2001 From: Lokesh Mandvekar Date: Mon, 8 Dec 2025 13:24:35 -0500 Subject: [PATCH 2/3] copy: add --force-digest flag to force digest algorithm This commit adds support for the --force-digest flag to skopeo copy, which allows users to force the use of a specific digest algorithm (sha256, sha512) when copying images. Changes: - cmd/skopeo/copy.go: Parse --force-digest flag and set digest options Example usage: skopeo copy --force-digest=sha512 docker://alpine dir:/path/to/dest This will copy the image and recompute all digests using sha512, even if the source image uses sha256. Signed-off-by: Lokesh Mandvekar --- cmd/skopeo/copy.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cmd/skopeo/copy.go b/cmd/skopeo/copy.go index d0af11319d..8bc576d9d5 100644 --- a/cmd/skopeo/copy.go +++ b/cmd/skopeo/copy.go @@ -9,6 +9,7 @@ import ( encconfig "github.com/containers/ocicrypt/config" enchelpers "github.com/containers/ocicrypt/helpers" + digest "github.com/opencontainers/go-digest" "github.com/spf13/cobra" commonFlag "go.podman.io/common/pkg/flag" "go.podman.io/common/pkg/retry" @@ -29,6 +30,7 @@ type copyOptions struct { additionalTags []string // For docker-archive: destinations, in addition to the name:tag specified as destination, also add these signIdentity string // Identity of the signed image, must be a fully specified docker reference digestFile string // Write digest to this file + forceDigest string // Force a specific digest algorithm (sha256, sha384, or sha512) quiet bool // Suppress output information when copying images all bool // Copy all of the images if the source is a list multiArch commonFlag.OptionalString // How to handle multi architecture images @@ -81,6 +83,7 @@ See skopeo(1) section "IMAGE NAMES" for the expected format flags.Var(commonFlag.NewOptionalStringValue(&opts.multiArch), "multi-arch", `How to handle multi-architecture images (system, all, or index-only)`) flags.StringVar(&opts.signIdentity, "sign-identity", "", "Identity of signed image, must be a fully specified docker reference. Defaults to the target docker reference.") flags.StringVar(&opts.digestFile, "digestfile", "", "Write the digest of the pushed image to the specified file") + flags.StringVar(&opts.forceDigest, "force-digest", "", "Force a specific digest algorithm (sha256, sha384, or sha512)") flags.StringSliceVar(&opts.encryptionKeys, "encryption-key", []string{}, "*Experimental* key with the encryption protocol to use needed to encrypt the image (e.g. jwe:/path/to/key.pem)") flags.IntSliceVar(&opts.encryptLayer, "encrypt-layer", []int{}, "*Experimental* the 0-indexed layer indices, with support for negative indexing (e.g. 0 is the first layer, -1 is the last layer)") flags.StringSliceVar(&opts.decryptionKeys, "decryption-key", []string{}, "*Experimental* key needed to decrypt the image") @@ -242,6 +245,14 @@ func (opts *copyOptions) run(args []string, stdout io.Writer) (retErr error) { copyOpts.MaxParallelDownloads = opts.imageParallelCopies copyOpts.ForceCompressionFormat = opts.destImage.forceCompressionFormat + // Handle force-digest option + if opts.forceDigest != "" { + algo := digest.Algorithm(opts.forceDigest) + if err := copyOpts.SetForceDigestAlgorithm(algo); err != nil { + return err + } + } + return retry.IfNecessary(ctx, func() error { manifestBytes, err := copy.Image(ctx, policyContext, destRef, srcRef, copyOpts) if err != nil { From 83ac6d83a84a93df573bd73493beb59b5cddd382 Mon Sep 17 00:00:00 2001 From: Lokesh Mandvekar Date: Wed, 10 Dec 2025 16:07:16 -0500 Subject: [PATCH 3/3] docs: manpage update for `skopeo copy --force-digest` Signed-off-by: Lokesh Mandvekar --- docs/skopeo-copy.1.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/docs/skopeo-copy.1.md b/docs/skopeo-copy.1.md index 1e824c7ea2..02e64fb4b5 100644 --- a/docs/skopeo-copy.1.md +++ b/docs/skopeo-copy.1.md @@ -71,6 +71,12 @@ This option does not change what will be copied; consider using `--all` at the s MANIFEST TYPE (oci, v2s1, or v2s2) to use in the destination (default is manifest type of source, with fallbacks) +**--force-digest**=_algorithm_ **EXPERIMENTAL** + +Algorithm to use for computing digests (sha256, sha512). When specified, all digests will be recomputed using the specified algorithm during the copy operation, regardless of the digest algorithm used in the source image. + +**Note:** This flag is experimental and its behavior may change in future releases. + **--help**, **-h** Print usage statement @@ -267,6 +273,11 @@ To copy and sign an image: $ skopeo copy --sign-by dev@example.com containers-storage:example/busybox:streaming docker://example/busybox:gold ``` +To copy an image and force all digests to use sha512: +```console +$ skopeo copy --force-digest=sha512 docker://docker.io/library/alpine:latest dir:/tmp/alpine +``` + To encrypt an image: ```console $ skopeo copy docker://docker.io/library/nginx:1.17.8 oci:local_nginx:1.17.8