From e9b5d376b7f7b2939ca44f26d1bfed503cc34593 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 1 Oct 2025 08:53:59 +0000 Subject: [PATCH] dnm: Vendor changes from containers/container-libs#312 --- go.mod | 6 + go.sum | 12 +- .../go.podman.io/common/pkg/config/config.go | 7 + .../common/pkg/config/containers.conf | 7 + .../common/pkg/config/containers.conf-freebsd | 7 + .../imagedestination/stubs/signatures.go | 2 +- .../image/v5/oci/layout/oci_delete.go | 93 +++++++- .../image/v5/oci/layout/oci_dest.go | 225 +++++++++++++++++- .../image/v5/oci/layout/oci_src.go | 47 ++-- .../image/v5/oci/layout/oci_transport.go | 133 ++++++++++- vendor/modules.txt | 9 +- 11 files changed, 508 insertions(+), 40 deletions(-) diff --git a/go.mod b/go.mod index 2520151ce..03c6b9a89 100644 --- a/go.mod +++ b/go.mod @@ -137,3 +137,9 @@ require ( sigs.k8s.io/yaml v1.6.0 // indirect tags.cncf.io/container-device-interface/specs-go v1.0.0 // indirect ) + +replace go.podman.io/common => github.com/bitoku/container-libs/common v0.0.0-20251001085230-f74ad300c684 + +replace go.podman.io/storage => github.com/bitoku/container-libs/storage v0.0.0-20251001085230-f74ad300c684 + +replace go.podman.io/image/v5 => github.com/bitoku/container-libs/image/v5 v5.0.0-20251001085230-f74ad300c684 diff --git a/go.sum b/go.sum index fa168ca2d..f6a2fd608 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,12 @@ github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6 h1:5L8Mj9Co9sJVgW3TpY github.com/aead/serpent v0.0.0-20160714141033-fba169763ea6/go.mod h1:3HgLJ9d18kXMLQlJvIY3+FszZYMxCz8WfE2MQ7hDY0w= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/bitoku/container-libs/common v0.0.0-20251001085230-f74ad300c684 h1:9nKP5MUjA0tyrqUqvEzeHpYLBGkK+qh+EdVXfHC0kfc= +github.com/bitoku/container-libs/common v0.0.0-20251001085230-f74ad300c684/go.mod h1:DyOdwtkwzYA8lE0TueJnxRju4Lmsrx6ZAC/ATAkYYck= +github.com/bitoku/container-libs/image/v5 v5.0.0-20251001085230-f74ad300c684 h1:no+ZS52NRYbenjo2Hte4UCdFH9PMV8gIDt7yXfAAF2E= +github.com/bitoku/container-libs/image/v5 v5.0.0-20251001085230-f74ad300c684/go.mod h1:cGWb3IyBziJGxhFikTOlt9Ap+zo6s3rz9Qd1rbzqs4s= +github.com/bitoku/container-libs/storage v0.0.0-20251001085230-f74ad300c684 h1:ZPtKzmDeEUf4RU/I77Xb1jASYLTYj5h9/325ymQH624= +github.com/bitoku/container-libs/storage v0.0.0-20251001085230-f74ad300c684/go.mod h1:AeZXAN8Qu1gTlAEHIc6mVhxk+61oMSM3K3iLx5UAQWE= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -317,12 +323,6 @@ go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKr go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= -go.podman.io/common v0.65.1-0.20250916163606-92222dcd3da4 h1:YjBqTOxz4cqfpifcd71VoBl1FTQL2U2La5NgMqmRRqU= -go.podman.io/common v0.65.1-0.20250916163606-92222dcd3da4/go.mod h1:DyOdwtkwzYA8lE0TueJnxRju4Lmsrx6ZAC/ATAkYYck= -go.podman.io/image/v5 v5.37.1-0.20250916163606-92222dcd3da4 h1:hfc3lZaxi6KGnWN3IusIaCkcMPR4rTR+vWZzakeD1EA= -go.podman.io/image/v5 v5.37.1-0.20250916163606-92222dcd3da4/go.mod h1:cGWb3IyBziJGxhFikTOlt9Ap+zo6s3rz9Qd1rbzqs4s= -go.podman.io/storage v1.60.1-0.20250916163606-92222dcd3da4 h1:jo0PSKh6muU7rmhXXqOV9aK+HrA8koqs47KhBsZf6LY= -go.podman.io/storage v1.60.1-0.20250916163606-92222dcd3da4/go.mod h1:AeZXAN8Qu1gTlAEHIc6mVhxk+61oMSM3K3iLx5UAQWE= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= diff --git a/vendor/go.podman.io/common/pkg/config/config.go b/vendor/go.podman.io/common/pkg/config/config.go index 2a11ccb00..8bc23deba 100644 --- a/vendor/go.podman.io/common/pkg/config/config.go +++ b/vendor/go.podman.io/common/pkg/config/config.go @@ -707,6 +707,13 @@ type Destination struct { // Identity file with ssh key, optional Identity string `json:",omitempty" toml:"identity,omitempty"` + // Path to TLS client certificate PEM file, optional + TLSCert string `json:",omitempty" toml:"tls_cert,omitempty"` + // Path to TLS client certificate private key PEM file, optional + TLSKey string `json:",omitempty" toml:"tls_key,omitempty"` + // Path to TLS certificate authority PEM file, optional + TLSCA string `json:",omitempty" toml:"tls_ca,omitempty"` + // isMachine describes if the remote destination is a machine. IsMachine bool `json:",omitempty" toml:"is_machine,omitempty"` } diff --git a/vendor/go.podman.io/common/pkg/config/containers.conf b/vendor/go.podman.io/common/pkg/config/containers.conf index 57a9fcfa4..2e392d048 100644 --- a/vendor/go.podman.io/common/pkg/config/containers.conf +++ b/vendor/go.podman.io/common/pkg/config/containers.conf @@ -779,10 +779,17 @@ default_sysctls = [ # rootful "unix:///run/podman/podman.sock (Default) # remote rootless ssh://engineering.lab.company.com/run/user/1000/podman/podman.sock # remote rootful ssh://root@10.10.1.136:22/run/podman/podman.sock +# tcp/tls remote tcp://10.10.1.136:9443 # # uri = "ssh://user@production.example.com/run/user/1001/podman/podman.sock" # Path to file containing ssh identity key # identity = "~/.ssh/id_rsa" +# Path to PEM file containing TLS client certificate +# tls_cert = "/path/to/certs/podman/tls.crt" +# Path to PEM file containing TLS client certificate private key +# tls_key = "/path/to/certs/podman/tls.key" +# Path to PEM file containing TLS certificate authority (CA) bundle +# tls_ca = "/path/to/certs/podman/ca.crt" # Directory for temporary files. Must be tmpfs (wiped after reboot) # diff --git a/vendor/go.podman.io/common/pkg/config/containers.conf-freebsd b/vendor/go.podman.io/common/pkg/config/containers.conf-freebsd index b57160b10..bd999c339 100644 --- a/vendor/go.podman.io/common/pkg/config/containers.conf-freebsd +++ b/vendor/go.podman.io/common/pkg/config/containers.conf-freebsd @@ -598,10 +598,17 @@ default_sysctls = [ # rootful "unix:///run/podman/podman.sock (Default) # remote rootless ssh://engineering.lab.company.com/run/user/1000/podman/podman.sock # remote rootful ssh://root@10.10.1.136:22/run/podman/podman.sock +# tcp/tls remote tcp://10.10.1.136:9443 # # uri = "ssh://user@production.example.com/run/user/1001/podman/podman.sock" # Path to file containing ssh identity key # identity = "~/.ssh/id_rsa" +# Path to PEM file containing TLS client certificate +# tls_cert = "/path/to/certs/podman/tls.crt" +# Path to PEM file containing TLS client certificate private key +# tls_key = "/path/to/certs/podman/tls.key" +# Path to PEM file containing TLS certificate authority (CA) bundle +# tls_ca = "/path/to/certs/podman/ca.crt" # Directory for temporary files. Must be tmpfs (wiped after reboot) # diff --git a/vendor/go.podman.io/image/v5/internal/imagedestination/stubs/signatures.go b/vendor/go.podman.io/image/v5/internal/imagedestination/stubs/signatures.go index c046449b1..b2d20ddf1 100644 --- a/vendor/go.podman.io/image/v5/internal/imagedestination/stubs/signatures.go +++ b/vendor/go.podman.io/image/v5/internal/imagedestination/stubs/signatures.go @@ -39,7 +39,7 @@ func (stub NoSignaturesInitialize) PutSignaturesWithFormat(ctx context.Context, return nil } -// SupportsSignatures implements SupportsSignatures() that returns nil. +// AlwaysSupportsSignatures implements SupportsSignatures() that returns nil. // Note that it might be even more useful to return a value dynamically detected based on type AlwaysSupportsSignatures struct{} diff --git a/vendor/go.podman.io/image/v5/oci/layout/oci_delete.go b/vendor/go.podman.io/image/v5/oci/layout/oci_delete.go index 7eaf6f088..84460ec6e 100644 --- a/vendor/go.podman.io/image/v5/oci/layout/oci_delete.go +++ b/vendor/go.podman.io/image/v5/oci/layout/oci_delete.go @@ -37,12 +37,22 @@ func (ref ociReference) DeleteImage(ctx context.Context, sys *types.SystemContex return err } + signaturesToDelete, err := ref.getObsoleteSignatures(blobsToDelete) + if err != nil { + return err + } + err = ref.deleteBlobs(blobsToDelete) if err != nil { return err } - return ref.deleteReferenceFromIndex(descriptorIndex) + err = ref.deleteReferenceFromIndex(descriptorIndex) + if err != nil { + return err + } + + return ref.deleteSignatures(sys, signaturesToDelete) } // countBlobsForDescriptor updates dest with usage counts of blobs required for descriptor, INCLUDING descriptor itself. @@ -148,6 +158,7 @@ func deleteBlob(blobPath string) error { } } +// deleteReferencesFromIndex deletes manifest from the root index. func (ref ociReference) deleteReferenceFromIndex(referenceIndex int) error { index, err := ref.getIndex() if err != nil { @@ -159,6 +170,25 @@ func (ref ociReference) deleteReferenceFromIndex(referenceIndex int) error { return saveJSON(ref.indexPath(), index) } +// deleteReferencesFromIndex deletes referenceIndex first, and then remove signatures. +func (ref ociReference) deleteSignaturesFromIndex(signatures []imgspecv1.Descriptor) error { + index, err := ref.getIndex() + if err != nil { + return err + } + + signaturesSet := set.New[digest.Digest]() + for _, sign := range signatures { + signaturesSet.Add(sign.Digest) + } + + index.Manifests = slices.DeleteFunc(index.Manifests, func(d imgspecv1.Descriptor) bool { + return signaturesSet.Contains(d.Digest) + }) + + return saveJSON(ref.indexPath(), index) +} + func saveJSON(path string, content any) (retErr error) { // If the file already exists, get its mode to preserve it var mode fs.FileMode @@ -187,3 +217,64 @@ func saveJSON(path string, content any) (retErr error) { return json.NewEncoder(file).Encode(content) } + +func (ref ociReference) getObsoleteSignatures(blobsToDelete *set.Set[digest.Digest]) (signaturesToDelete []imgspecv1.Descriptor, err error) { + // create a mapping from sigstore tag to its descriptor + signDigestMap := make(map[string]imgspecv1.Descriptor) + index, err := ref.getIndex() + if err != nil { + return nil, err + } + for _, m := range index.Manifests { + if isSigstoreTag(m.Annotations[imgspecv1.AnnotationRefName]) { + signDigestMap[m.Annotations[imgspecv1.AnnotationRefName]] = m + } + } + + for dgst := range blobsToDelete.All() { + sigstoreTag, err := sigstoreAttachmentTag(dgst) + if err != nil { + // This shouldn't happen because all digests in the root index should be valid. + continue + } + signDesc, ok := signDigestMap[sigstoreTag] + if !ok { + // No signature found for this digest + continue + } + signaturesToDelete = append(signaturesToDelete, signDesc) + } + return signaturesToDelete, nil +} + +// deleteSignatures delete sigstore signatures of the given manifest digest. +func (ref ociReference) deleteSignatures(sys *types.SystemContext, signaturesToDelete []imgspecv1.Descriptor) error { + sharedBlobsDir := "" + if sys != nil && sys.OCISharedBlobDirPath != "" { + sharedBlobsDir = sys.OCISharedBlobDirPath + } + + blobsUsedByImage := make(map[digest.Digest]int) + for _, descriptor := range signaturesToDelete { + if err := ref.countBlobsForDescriptor(blobsUsedByImage, &descriptor, sharedBlobsDir); err != nil { + return err + } + } + + blobsToDelete, err := ref.getBlobsToDelete(blobsUsedByImage, sharedBlobsDir) + if err != nil { + return err + } + + err = ref.deleteBlobs(blobsToDelete) + if err != nil { + return err + } + + err = ref.deleteSignaturesFromIndex(signaturesToDelete) + if err != nil { + return err + } + + return nil +} diff --git a/vendor/go.podman.io/image/v5/oci/layout/oci_dest.go b/vendor/go.podman.io/image/v5/oci/layout/oci_dest.go index 48fe812df..efa5ed66a 100644 --- a/vendor/go.podman.io/image/v5/oci/layout/oci_dest.go +++ b/vendor/go.podman.io/image/v5/oci/layout/oci_dest.go @@ -1,25 +1,32 @@ package layout import ( + "bytes" "context" "encoding/json" "errors" "fmt" "io" "io/fs" + "maps" "os" "path/filepath" "runtime" "slices" - digest "github.com/opencontainers/go-digest" + "github.com/opencontainers/go-digest" imgspec "github.com/opencontainers/image-spec/specs-go" imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/sirupsen/logrus" "go.podman.io/image/v5/internal/imagedestination/impl" "go.podman.io/image/v5/internal/imagedestination/stubs" - "go.podman.io/image/v5/internal/manifest" + "go.podman.io/image/v5/internal/iolimits" "go.podman.io/image/v5/internal/private" "go.podman.io/image/v5/internal/putblobdigest" + "go.podman.io/image/v5/internal/set" + "go.podman.io/image/v5/internal/signature" + "go.podman.io/image/v5/manifest" + "go.podman.io/image/v5/pkg/blobinfocache/none" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/fileutils" ) @@ -27,13 +34,17 @@ import ( type ociImageDestination struct { impl.Compat impl.PropertyMethodsInitialize + stubs.AlwaysSupportsSignatures stubs.IgnoresOriginalOCIConfig stubs.NoPutBlobPartialInitialize - stubs.NoSignaturesInitialize - ref ociReference - index imgspecv1.Index - sharedBlobDir string + ref ociReference + index imgspecv1.Index + sharedBlobDir string + manifestDigest digest.Digest // or "" if not yet known. + // blobDeleteCandidates is a set of digests which may be deleted _if_ we find no other references to them; + // it’s safe to optimistically include entries which may have other references + blobDeleteCandidates *set.Set[digest.Digest] } // newImageDestination returns an ImageDestination for writing to an existing directory. @@ -75,10 +86,10 @@ func newImageDestination(sys *types.SystemContext, ref ociReference) (private.Im HasThreadSafePutBlob: true, }), NoPutBlobPartialInitialize: stubs.NoPutBlobPartial(ref), - NoSignaturesInitialize: stubs.NoSignatures("Pushing signatures for OCI images is not supported"), - ref: ref, - index: *index, + ref: ref, + index: *index, + blobDeleteCandidates: set.New[digest.Digest](), } d.Compat = impl.AddCompat(d) if sys != nil { @@ -255,6 +266,9 @@ func (d *ociImageDestination) PutManifest(ctx context.Context, m []byte, instanc if instanceDigest != nil { return nil } + // d.manifestDigest is used for a single image (not a manifest list). + // This should be placed after checking instanceDigest is nil. + d.manifestDigest = digest // If we had platform information, we'd build an imgspecv1.Platform structure here. @@ -312,6 +326,26 @@ func (d *ociImageDestination) CommitWithOptions(ctx context.Context, options pri if err != nil { return err } + // Delete unreferenced blobs (e.g. old signature manifest and its config) + if !d.blobDeleteCandidates.Empty() { + blobsUsedInRootIndex := make(map[digest.Digest]int) + err = d.ref.countBlobsReferencedByIndex(blobsUsedInRootIndex, &d.index, d.sharedBlobDir) + if err != nil { + return fmt.Errorf("error counting blobs to delete: %w", err) + } + // Don't delete blobs which are referenced + actualBlobsToDelete := set.New[digest.Digest]() + for dgst := range d.blobDeleteCandidates.All() { + if blobsUsedInRootIndex[dgst] == 0 { + actualBlobsToDelete.Add(dgst) + } + } + err := d.ref.deleteBlobs(actualBlobsToDelete) + if err != nil { + return fmt.Errorf("error deleting blobs: %w", err) + } + d.blobDeleteCandidates = set.New[digest.Digest]() + } if err := os.WriteFile(d.ref.ociLayoutPath(), layoutBytes, 0644); err != nil { return err } @@ -322,6 +356,164 @@ func (d *ociImageDestination) CommitWithOptions(ctx context.Context, options pri return os.WriteFile(d.ref.indexPath(), indexJSON, 0644) } +func (d *ociImageDestination) PutSignaturesWithFormat(ctx context.Context, signatures []signature.Signature, instanceDigest *digest.Digest) error { + if instanceDigest == nil { + if d.manifestDigest == "" { + // This shouldn’t happen, ImageDestination users are required to call PutManifest before PutSignatures + return errors.New("unknown manifest digest, can't add signatures") + } + instanceDigest = &d.manifestDigest + } + + var sigstoreSignatures []signature.Sigstore + for _, sig := range signatures { + if sigstoreSig, ok := sig.(signature.Sigstore); ok { + sigstoreSignatures = append(sigstoreSignatures, sigstoreSig) + } else { + return errors.New("oci: layout only supports sigstore signatures") + } + } + + if err := d.putSignaturesToSigstoreAttachment(ctx, sigstoreSignatures, *instanceDigest); err != nil { + return err + } + + return nil +} + +func (d *ociImageDestination) putSignaturesToSigstoreAttachment(ctx context.Context, signatures []signature.Sigstore, manifestDigest digest.Digest) error { + var signConfig imgspecv1.Image // Most fields empty by default + + signManifest, signDesc, err := d.ref.getSigstoreAttachmentManifest(manifestDigest, &d.index, d.sharedBlobDir) + if err != nil { + return err + } + if signManifest == nil { + signManifest = manifest.OCI1FromComponents(imgspecv1.Descriptor{ + MediaType: imgspecv1.MediaTypeImageConfig, + Digest: "", // We will fill this in later. + Size: 0, + }, nil) + signConfig.RootFS.Type = "layers" + } else { + logrus.Debugf("Fetching sigstore attachment config %s", signManifest.Config.Digest.String()) + configBlob, err := d.ref.getOCIDescriptorContents(signManifest.Config.Digest, iolimits.MaxConfigBodySize, d.sharedBlobDir) + if err != nil { + return err + } + if err := json.Unmarshal(configBlob, &signConfig); err != nil { + return fmt.Errorf("parsing sigstore attachment config %s in %s: %w", signManifest.Config.Digest.String(), + d.ref.StringWithinTransport(), err) + } + // The signature manifest and its config may be updated and unreferenced when a new config is created. + d.blobDeleteCandidates.Add(signDesc.Digest) + d.blobDeleteCandidates.Add(signManifest.Config.Digest) + } + + desc, err := d.getDescriptor(manifestDigest) + if err != nil { + return err + } + signManifest.Subject = desc + + // To make sure we can safely append to the slices of signManifest, without adding a remote dependency on the code that creates it. + signManifest.Layers = slices.Clone(signManifest.Layers) + for _, sig := range signatures { + mimeType := sig.UntrustedMIMEType() + payloadBlob := sig.UntrustedPayload() + annotations := sig.UntrustedAnnotations() + + // Skip if the signature is already on the registry. + if slices.ContainsFunc(signManifest.Layers, func(layer imgspecv1.Descriptor) bool { + return layerMatchesSigstoreSignature(layer, mimeType, payloadBlob, annotations) + }) { + continue + } + + signDesc, err := d.putBlobBytesAsOCI(ctx, payloadBlob, mimeType, private.PutBlobOptions{ + Cache: none.NoCache, + IsConfig: false, + EmptyLayer: false, + LayerIndex: nil, + }) + if err != nil { + return err + } + signDesc.Annotations = annotations + signManifest.Layers = append(signManifest.Layers, signDesc) + signConfig.RootFS.DiffIDs = append(signConfig.RootFS.DiffIDs, signDesc.Digest) + logrus.Debugf("Adding new signature, digest %s", signDesc.Digest.String()) + } + + configBlob, err := json.Marshal(signConfig) + if err != nil { + return err + } + logrus.Debugf("Creating updated sigstore attachment config") + configDesc, err := d.putBlobBytesAsOCI(ctx, configBlob, imgspecv1.MediaTypeImageConfig, private.PutBlobOptions{ + Cache: none.NoCache, + IsConfig: true, + EmptyLayer: false, + LayerIndex: nil, + }) + if err != nil { + return err + } + + signManifest.Config = configDesc + signManifestBlob, err := signManifest.Serialize() + if err != nil { + return err + } + logrus.Debugf("Creating sigstore attachment manifest") + signDigest := digest.FromBytes(signManifestBlob) + if err = d.PutManifest(ctx, signManifestBlob, &signDigest); err != nil { + return err + } + signTag, err := sigstoreAttachmentTag(manifestDigest) + if err != nil { + return err + } + d.addManifest(&imgspecv1.Descriptor{ + MediaType: signManifest.MediaType, + Digest: signDigest, + Size: int64(len(signManifestBlob)), + Annotations: map[string]string{ + imgspecv1.AnnotationRefName: signTag, + }, + }) + return nil +} + +func (d *ociImageDestination) getDescriptor(digest digest.Digest) (*imgspecv1.Descriptor, error) { + for _, desc := range d.index.Manifests { + if desc.Digest == digest { + return &desc, nil + } + } + return nil, fmt.Errorf("manifest %s not found in index", digest.String()) +} + +// putBlobBytesAsOCI uploads a blob with the specified contents, and returns an appropriate +// OCI descriptor. +func (d *ociImageDestination) putBlobBytesAsOCI(ctx context.Context, contents []byte, mimeType string, options private.PutBlobOptions) (imgspecv1.Descriptor, error) { + blobDigest := digest.FromBytes(contents) + info, err := d.PutBlobWithOptions(ctx, bytes.NewReader(contents), + types.BlobInfo{ + Digest: blobDigest, + Size: int64(len(contents)), + MediaType: mimeType, + }, options) + if err != nil { + return imgspecv1.Descriptor{}, fmt.Errorf("writing blob %s: %w", blobDigest.String(), err) + } + return imgspecv1.Descriptor{ + MediaType: mimeType, + Digest: info.Digest, + Size: info.Size, + }, nil +} + // PutBlobFromLocalFileOption is unused but may receive functionality in the future. type PutBlobFromLocalFileOption struct{} @@ -412,3 +604,18 @@ func indexExists(ref ociReference) bool { } return true } + +func layerMatchesSigstoreSignature(layer imgspecv1.Descriptor, mimeType string, + payloadBlob []byte, annotations map[string]string) bool { + if layer.MediaType != mimeType || + layer.Size != int64(len(payloadBlob)) || + // This is not quite correct, we should use the layer’s digest algorithm. + // But right now we don’t want to deal with corner cases like bad digest formats + // or unavailable algorithms; in the worst case we end up with duplicate signature + // entries. + layer.Digest.String() != digest.FromBytes(payloadBlob).String() || + !maps.Equal(layer.Annotations, annotations) { + return false + } + return true +} diff --git a/vendor/go.podman.io/image/v5/oci/layout/oci_src.go b/vendor/go.podman.io/image/v5/oci/layout/oci_src.go index f265a21d7..2fd1da7db 100644 --- a/vendor/go.podman.io/image/v5/oci/layout/oci_src.go +++ b/vendor/go.podman.io/image/v5/oci/layout/oci_src.go @@ -16,8 +16,10 @@ import ( imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" "go.podman.io/image/v5/internal/imagesource/impl" "go.podman.io/image/v5/internal/imagesource/stubs" - "go.podman.io/image/v5/internal/manifest" + "go.podman.io/image/v5/internal/iolimits" "go.podman.io/image/v5/internal/private" + "go.podman.io/image/v5/internal/signature" + "go.podman.io/image/v5/manifest" "go.podman.io/image/v5/pkg/tlsclientconfig" "go.podman.io/image/v5/types" "go.podman.io/storage/pkg/fileutils" @@ -37,7 +39,6 @@ func (e ImageNotFoundError) Error() string { type ociImageSource struct { impl.Compat impl.PropertyMethodsInitialize - impl.NoSignatures impl.DoesNotAffectLayerInfosForCopy stubs.NoGetBlobAtInitialize @@ -158,20 +159,7 @@ func (s *ociImageSource) GetBlob(ctx context.Context, info types.BlobInfo, cache } } - path, err := s.ref.blobPath(info.Digest, s.sharedBlobDir) - if err != nil { - return nil, 0, err - } - - r, err := os.Open(path) - if err != nil { - return nil, 0, err - } - fi, err := r.Stat() - if err != nil { - return nil, 0, err - } - return r, fi.Size(), nil + return s.ref.getBlob(info.Digest, s.sharedBlobDir) } // getExternalBlob returns the reader of the first available blob URL from urls, which must not be empty. @@ -246,3 +234,30 @@ func GetLocalBlobPath(ctx context.Context, src types.ImageSource, digest digest. return path, nil } + +func (s *ociImageSource) GetSignaturesWithFormat(ctx context.Context, instanceDigest *digest.Digest) ([]signature.Signature, error) { + if instanceDigest == nil { + instanceDigest = &s.descriptor.Digest + } + + ociManifest, _, err := s.ref.getSigstoreAttachmentManifest(*instanceDigest, s.index, s.sharedBlobDir) + if err != nil { + return nil, err + } + if ociManifest == nil { + // No signature found + return nil, nil + } + + signatures := make([]signature.Signature, 0, len(ociManifest.Layers)) + for _, layer := range ociManifest.Layers { + // Note that this copies all kinds of attachments: attestations, and whatever else is there, + // not just signatures. We leave the signature consumers to decide based on the MIME type. + payload, err := s.ref.getOCIDescriptorContents(layer.Digest, iolimits.MaxSignatureBodySize, s.sharedBlobDir) + if err != nil { + return nil, err + } + signatures = append(signatures, signature.SigstoreFromComponents(layer.MediaType, payload, layer.Annotations)) + } + return signatures, nil +} diff --git a/vendor/go.podman.io/image/v5/oci/layout/oci_transport.go b/vendor/go.podman.io/image/v5/oci/layout/oci_transport.go index 7b5086cd8..8f5134e9b 100644 --- a/vendor/go.podman.io/image/v5/oci/layout/oci_transport.go +++ b/vendor/go.podman.io/image/v5/oci/layout/oci_transport.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "os" "path/filepath" "strings" @@ -14,7 +15,8 @@ import ( "go.podman.io/image/v5/directory/explicitfilepath" "go.podman.io/image/v5/docker/reference" "go.podman.io/image/v5/internal/image" - "go.podman.io/image/v5/internal/manifest" + "go.podman.io/image/v5/internal/iolimits" + "go.podman.io/image/v5/manifest" "go.podman.io/image/v5/oci/internal" "go.podman.io/image/v5/transports" "go.podman.io/image/v5/types" @@ -28,6 +30,9 @@ var ( // Transport is an ImageTransport for OCI directories. Transport = ociTransport{} + // ErrEmptyIndex is an error returned when the index includes no image. + ErrEmptyIndex = errors.New("no image in oci") + // ErrMoreThanOneImage is an error returned when the manifest includes // more than one image and the user should choose which one to use. ErrMoreThanOneImage = errors.New("more than one image in oci, choose an image") @@ -248,11 +253,33 @@ func (ref ociReference) getManifestDescriptor() (imgspecv1.Descriptor, int, erro default: // return manifest if only one image is in the oci directory - if len(index.Manifests) != 1 { - // ask user to choose image when more than one image in the oci directory + if len(index.Manifests) == 0 { + return imgspecv1.Descriptor{}, -1, ErrEmptyIndex + } + // if there's one image return it, even if it is a signature + if len(index.Manifests) == 1 { + return index.Manifests[0], 0, nil + } + // when there's more than one image, try to get a non-signature image + var desc imgspecv1.Descriptor + idx := -1 + for i, md := range index.Manifests { + if isSigstoreTag(md.Annotations[imgspecv1.AnnotationRefName]) { + continue + } + // More than one non-signature image was found + if idx != -1 { + // ask user to choose image when more than one image in the oci directory + return imgspecv1.Descriptor{}, -1, ErrMoreThanOneImage + } + desc = md + idx = i + } + // there's only multiple signature images + if idx == -1 { return imgspecv1.Descriptor{}, -1, ErrMoreThanOneImage } - return index.Manifests[0], 0, nil + return desc, idx, nil } } @@ -302,3 +329,101 @@ func (ref ociReference) blobPath(digest digest.Digest, sharedBlobDir string) (st } return filepath.Join(blobDir, digest.Algorithm().String(), digest.Encoded()), nil } + +// sigstoreAttachmentTag returns a sigstore attachment tag for the specified digest. +func sigstoreAttachmentTag(d digest.Digest) (string, error) { + if err := d.Validate(); err != nil { // Make sure d.String() doesn’t contain any unexpected characters + return "", err + } + return strings.Replace(d.String(), ":", "-", 1) + ".sig", nil +} + +func (ref ociReference) getSigstoreAttachmentManifest(d digest.Digest, idx *imgspecv1.Index, sharedBlobDir string) (*manifest.OCI1, *imgspecv1.Descriptor, error) { + signTag, err := sigstoreAttachmentTag(d) + if err != nil { + return nil, nil, err + } + var signDesc *imgspecv1.Descriptor + for _, m := range idx.Manifests { + if m.Annotations[imgspecv1.AnnotationRefName] == signTag { + signDesc = &m + break + } + } + if signDesc == nil { + // No signature found + return nil, nil, nil + } + if signDesc.MediaType != imgspecv1.MediaTypeImageManifest { + return nil, nil, fmt.Errorf("unexpected MIME type for sigstore attachment manifest %s: %q", + signTag, signDesc.MediaType) + } + blobReader, _, err := ref.getBlob(signDesc.Digest, sharedBlobDir) + if err != nil { + return nil, nil, fmt.Errorf("failed to get Blob %s: %w", signTag, err) + } + defer blobReader.Close() + signBlob, err := iolimits.ReadAtMost(blobReader, iolimits.MaxManifestBodySize) + if err != nil { + return nil, nil, fmt.Errorf("failed to read blob: %w", err) + } + res, err := manifest.OCI1FromManifest(signBlob) + if err != nil { + return nil, nil, fmt.Errorf("parsing manifest %s: %w", signDesc.Digest, err) + } + return res, signDesc, nil +} + +func (ref ociReference) getBlob(d digest.Digest, sharedBlobDir string) (io.ReadCloser, int64, error) { + path, err := ref.blobPath(d, sharedBlobDir) + if err != nil { + return nil, 0, err + } + + r, err := os.Open(path) + if err != nil { + return nil, 0, err + } + fi, err := r.Stat() + if err != nil { + _ = r.Close() // Avoid leak r. + return nil, 0, err + } + return r, fi.Size(), nil +} + +func (ref ociReference) getOCIDescriptorContents(dgst digest.Digest, maxSize int, sharedBlobDir string) ([]byte, error) { + if err := dgst.Validate(); err != nil { // .Algorithm() might panic without this check + return nil, fmt.Errorf("invalid digest %q: %w", dgst.String(), err) + } + digestAlgorithm := dgst.Algorithm() + if !digestAlgorithm.Available() { + return nil, fmt.Errorf("invalid digest %q: unsupported digest algorithm %q", dgst.String(), digestAlgorithm.String()) + } + + reader, _, err := ref.getBlob(dgst, sharedBlobDir) + if err != nil { + return nil, err + } + defer reader.Close() + payload, err := iolimits.ReadAtMost(reader, maxSize) + if err != nil { + return nil, fmt.Errorf("reading blob %s in %s: %w", dgst.String(), ref.image, err) + } + actualDigest := digestAlgorithm.FromBytes(payload) + if actualDigest != dgst { + return nil, fmt.Errorf("digest mismatch, expected %q, got %q", dgst.String(), actualDigest.String()) + } + return payload, nil +} + +// isSigstoreTag returns true if the tag is sigstore signature tag. +func isSigstoreTag(tag string) bool { + digestPart, found := strings.CutSuffix(tag, ".sig") + if !found { + return false + } + digestPart = strings.Replace(digestPart, "-", ":", 1) + _, err := digest.Parse(digestPart) + return err == nil +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 72f946f1f..ff7c63ba3 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -474,7 +474,7 @@ go.opentelemetry.io/otel/trace go.opentelemetry.io/otel/trace/embedded go.opentelemetry.io/otel/trace/internal/telemetry go.opentelemetry.io/otel/trace/noop -# go.podman.io/common v0.65.1-0.20250916163606-92222dcd3da4 +# go.podman.io/common v0.65.1-0.20250916163606-92222dcd3da4 => github.com/bitoku/container-libs/common v0.0.0-20251001085230-f74ad300c684 ## explicit; go 1.24.2 go.podman.io/common/internal go.podman.io/common/internal/attributedstring @@ -528,7 +528,7 @@ go.podman.io/common/pkg/umask go.podman.io/common/pkg/util go.podman.io/common/pkg/version go.podman.io/common/version -# go.podman.io/image/v5 v5.37.1-0.20250916163606-92222dcd3da4 +# go.podman.io/image/v5 v5.37.1-0.20250916163606-92222dcd3da4 => github.com/bitoku/container-libs/image/v5 v5.0.0-20251001085230-f74ad300c684 ## explicit; go 1.24.0 go.podman.io/image/v5/copy go.podman.io/image/v5/directory @@ -596,7 +596,7 @@ go.podman.io/image/v5/transports go.podman.io/image/v5/transports/alltransports go.podman.io/image/v5/types go.podman.io/image/v5/version -# go.podman.io/storage v1.60.1-0.20250916163606-92222dcd3da4 +# go.podman.io/storage v1.60.1-0.20250916163606-92222dcd3da4 => github.com/bitoku/container-libs/storage v0.0.0-20251001085230-f74ad300c684 ## explicit; go 1.24.0 go.podman.io/storage go.podman.io/storage/drivers @@ -836,3 +836,6 @@ tags.cncf.io/container-device-interface/pkg/parser # tags.cncf.io/container-device-interface/specs-go v1.0.0 ## explicit; go 1.19 tags.cncf.io/container-device-interface/specs-go +# go.podman.io/common => github.com/bitoku/container-libs/common v0.0.0-20251001085230-f74ad300c684 +# go.podman.io/storage => github.com/bitoku/container-libs/storage v0.0.0-20251001085230-f74ad300c684 +# go.podman.io/image/v5 => github.com/bitoku/container-libs/image/v5 v5.0.0-20251001085230-f74ad300c684