Skip to content

Commit 8a75ccc

Browse files
committed
image/copy: Allow user to force digest algorithm in CLI ops like skopeo copy
Changes to image/copy/blob.go: - Use digests.Options.Choose() to validate digest changes Changes to image/copy/copy.go: - Add SetForceDigestAlgorithm() helper to configure digest forcing Changes to image/copy/single.go: - Add updateManifestConfigDigest() helper that uses typed manifest structures (OCI1/Schema2) to update config digest, avoiding generic JSON manipulation - Modify copyConfig() to return the new config digest when changed - Update copyUpdatedConfigAndManifest() to handle config digest changes by calling updateManifestConfigDigest() when needed Signed-off-by: Lokesh Mandvekar <lsm5@redhat.com>
1 parent 5b980b7 commit 8a75ccc

3 files changed

Lines changed: 78 additions & 8 deletions

File tree

image/copy/blob.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"io"
88

99
"github.com/sirupsen/logrus"
10+
"go.podman.io/image/v5/internal/digests"
1011
"go.podman.io/image/v5/internal/private"
1112
compressiontypes "go.podman.io/image/v5/pkg/compression/types"
1213
"go.podman.io/image/v5/types"
@@ -138,7 +139,17 @@ func (ic *imageCopier) copyBlobFromStream(ctx context.Context, srcReader io.Read
138139
return types.BlobInfo{}, fmt.Errorf("Internal error writing blob %s, digest verification failed but was ignored", srcInfo.Digest)
139140
}
140141
if stream.info.Digest != "" && uploadedInfo.Digest != stream.info.Digest {
141-
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)
142+
expectedAlgo, err := ic.c.options.digestOptions.Choose(digests.Situation{
143+
Preexisting: stream.info.Digest,
144+
CannotChangeAlgorithmReason: ic.cannotModifyManifestReason,
145+
})
146+
if err != nil {
147+
return types.BlobInfo{}, err
148+
}
149+
// If we're forcing a different algorithm and the uploaded digest uses that algorithm, it's acceptable
150+
if uploadedInfo.Digest.Algorithm() != expectedAlgo {
151+
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)
152+
}
142153
}
143154
if digestingReader.validationSucceeded {
144155
if err := compressionStep.recordValidatedDigestData(ic.c, uploadedInfo, srcInfo, encryptionStep, decryptionStep); err != nil {

image/copy/copy.go

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,19 @@ type Options struct {
167167
digestOptions digests.Options
168168
}
169169

170+
// SetForceDigestAlgorithm forces the use of a specific digest algorithm for this copy operation.
171+
func (o *Options) SetForceDigestAlgorithm(algo digest.Algorithm) error {
172+
if !algo.Available() {
173+
return fmt.Errorf("digest algorithm %q is not available", algo.String())
174+
}
175+
digestOpts, err := digests.MustUse(algo)
176+
if err != nil {
177+
return fmt.Errorf("failed to set force-digest algorithm: %w", err)
178+
}
179+
o.digestOptions = digestOpts
180+
return nil
181+
}
182+
170183
// OptionCompressionVariant allows to supply information about
171184
// selected compression algorithm and compression level by the
172185
// end-user. Refer to EnsureCompressionVariantsExist to know
@@ -214,7 +227,10 @@ func Image(ctx context.Context, policyContext *signature.PolicyContext, destRef,
214227
// only to allow gradually building the feature set.
215228
// After c/image/copy consistently implements it, provide a public digest options API of some kind.
216229
optionsCopy := *options
217-
optionsCopy.digestOptions = digests.CanonicalDefault()
230+
// Only set default if not already configured
231+
if optionsCopy.digestOptions.MustUseSet() == "" {
232+
optionsCopy.digestOptions = digests.CanonicalDefault()
233+
}
218234
options = &optionsCopy
219235

220236
if err := validateImageListSelection(options.ImageListSelection); err != nil {

image/copy/single.go

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -592,10 +592,19 @@ func (ic *imageCopier) copyUpdatedConfigAndManifest(ctx context.Context, instanc
592592
return nil, "", fmt.Errorf("reading manifest: %w", err)
593593
}
594594

595-
if err := ic.copyConfig(ctx, pendingImage); err != nil {
595+
newConfigDigest, err := ic.copyConfig(ctx, pendingImage)
596+
if err != nil {
596597
return nil, "", err
597598
}
598599

600+
// Config digest changed due to forcing a different digest algorithm
601+
if newConfigDigest != nil {
602+
man, err = ic.updateManifestConfigDigest(man, pendingImage, *newConfigDigest)
603+
if err != nil {
604+
return nil, "", fmt.Errorf("updating manifest config digest: %w", err)
605+
}
606+
}
607+
599608
ic.c.Printf("Writing manifest to image destination\n")
600609
manifestDigest, err := manifest.Digest(man)
601610
if err != nil {
@@ -611,13 +620,41 @@ func (ic *imageCopier) copyUpdatedConfigAndManifest(ctx context.Context, instanc
611620
return man, manifestDigest, nil
612621
}
613622

623+
// updateManifestConfigDigest uses typed manifest structures instead of generic JSON manipulation.
624+
// This leverages the existing manifest parsing and serialization infrastructure.
625+
func (ic *imageCopier) updateManifestConfigDigest(manifestBlob []byte, src types.Image, newConfigDigest digest.Digest) ([]byte, error) {
626+
_, mt, err := src.Manifest(context.Background())
627+
if err != nil {
628+
return nil, fmt.Errorf("getting manifest type: %w", err)
629+
}
630+
631+
m, err := manifest.FromBlob(manifestBlob, mt)
632+
if err != nil {
633+
return nil, fmt.Errorf("parsing manifest: %w", err)
634+
}
635+
636+
switch typedManifest := m.(type) {
637+
case *manifest.OCI1:
638+
typedManifest.Config.Digest = newConfigDigest
639+
return typedManifest.Serialize()
640+
case *manifest.Schema2:
641+
typedManifest.ConfigDescriptor.Digest = newConfigDigest
642+
return typedManifest.Serialize()
643+
case *manifest.Schema1:
644+
return nil, fmt.Errorf("cannot update config digest for schema1 manifest")
645+
default:
646+
return nil, fmt.Errorf("unsupported manifest type for config digest update: %T", m)
647+
}
648+
}
649+
614650
// copyConfig copies config.json, if any, from src to dest.
615-
func (ic *imageCopier) copyConfig(ctx context.Context, src types.Image) error {
651+
// It returns the new config digest if it changed (due to digest algorithm forcing), or nil otherwise.
652+
func (ic *imageCopier) copyConfig(ctx context.Context, src types.Image) (*digest.Digest, error) {
616653
srcInfo := src.ConfigInfo()
617654
if srcInfo.Digest != "" {
618655
if err := ic.c.concurrentBlobCopiesSemaphore.Acquire(ctx, 1); err != nil {
619656
// This can only fail with ctx.Err(), so no need to blame acquiring the semaphore.
620-
return fmt.Errorf("copying config: %w", err)
657+
return nil, fmt.Errorf("copying config: %w", err)
621658
}
622659
defer ic.c.concurrentBlobCopiesSemaphore.Release(1)
623660

@@ -645,13 +682,19 @@ func (ic *imageCopier) copyConfig(ctx context.Context, src types.Image) error {
645682
return destInfo, nil
646683
}()
647684
if err != nil {
648-
return err
685+
return nil, err
649686
}
650687
if destInfo.Digest != srcInfo.Digest {
651-
return fmt.Errorf("Internal error: copying uncompressed config blob %s changed digest to %s", srcInfo.Digest, destInfo.Digest)
688+
// Allow digest algorithm changes when forcing a specific digest algorithm
689+
forcingDifferentAlgo := ic.c.options.digestOptions.MustUseSet() != "" &&
690+
destInfo.Digest.Algorithm() != srcInfo.Digest.Algorithm()
691+
if !forcingDifferentAlgo {
692+
return nil, fmt.Errorf("Internal error: copying uncompressed config blob %s changed digest to %s", srcInfo.Digest, destInfo.Digest)
693+
}
694+
return &destInfo.Digest, nil
652695
}
653696
}
654-
return nil
697+
return nil, nil
655698
}
656699

657700
// diffIDResult contains both a digest value and an error from diffIDComputationGoroutine.

0 commit comments

Comments
 (0)