This commit is contained in:
github-actions[bot] 2025-10-01 08:53:59 +00:00
parent ae46726d30
commit e9b5d376b7
11 changed files with 508 additions and 40 deletions

6
go.mod
View File

@ -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

12
go.sum
View File

@ -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=

View File

@ -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"`
}

View File

@ -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)
#

View File

@ -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)
#

View File

@ -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{}

View File

@ -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
}

View File

@ -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;
// its 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 shouldnt 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 layers digest algorithm.
// But right now we dont 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
}

View File

@ -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
}

View File

@ -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() doesnt 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
}

9
vendor/modules.txt vendored
View File

@ -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