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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions src/pkg/images/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ import (
"github.com/zarf-dev/zarf/src/pkg/logger"
"github.com/zarf-dev/zarf/src/pkg/state"
"oras.land/oras-go/v2/content"
orasRemote "oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/credentials"
"oras.land/oras-go/v2/registry/remote/retry"
)

Expand Down Expand Up @@ -187,6 +189,48 @@ func saveIndexToOCILayout(dir string, idx ocispec.Index) error {
return nil
}

// NewAuthClientFromDocker creates an ORAS auth client from the default Docker credentials store.
func NewAuthClientFromDocker(ctx context.Context, insecureSkipTLSVerify bool, responseHeaderTimeout time.Duration, preAuthHosts map[string]struct{}) (_ *auth.Client, err error) {
credStore, err := credentials.NewStoreFromDocker(credentials.StoreOptions{})
if err != nil {
return nil, fmt.Errorf("failed to get credentials: %w", err)
}

transport, err := orasTransport(insecureSkipTLSVerify, responseHeaderTimeout)
if err != nil {
return nil, err
}

client := &auth.Client{
Client: &http.Client{
Transport: transport,
},
Cache: auth.NewCache(),
Credential: credentials.Credential(credStore),
}

logger.From(ctx).Debug("gathering credentials from default Docker config file", "credentialsConfigured", credStore.IsAuthConfigured())

// We ping registries to pre-authenticate as some auth mechanisms open up a browser.
// When this happens concurrently a browser tab is opened for each image from that host and authenticating to one tab will not propagate creds.
// Instead we auth synchronously with ping so the auth is cached before concurrent fetch.
if credStore.IsAuthConfigured() {
for host := range preAuthHosts {
if host == "" {
continue
}
registry, err := orasRemote.NewRegistry(host)
if err != nil {
return nil, fmt.Errorf("failed to create registry: %w", err)
}
registry.Client = client
_ = registry.Ping(ctx) //nolint: errcheck
}
}

return client, nil
}

func orasTransport(insecureSkipTLSVerify bool, responseHeaderTimeout time.Duration) (*retry.Transport, error) {
transport, ok := http.DefaultTransport.(*http.Transport)
if !ok {
Expand Down
36 changes: 4 additions & 32 deletions src/pkg/images/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import (
"context"
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -40,7 +39,6 @@ import (
"github.com/zarf-dev/zarf/src/pkg/utils"
orasRemote "oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
"oras.land/oras-go/v2/registry/remote/credentials"
)

// PullOptions is the configuration for pulling images.
Expand Down Expand Up @@ -115,42 +113,16 @@ func Pull(ctx context.Context, imageList []transform.Image, destinationDirectory

imageFetchStart := time.Now()
l.Info("fetching info for images", "count", imageCount, "destination", destinationDirectory)
storeOpts := credentials.StoreOptions{}
credStore, err := credentials.NewStoreFromDocker(storeOpts)
if err != nil {
return nil, fmt.Errorf("failed to get credentials: %w", err)
}
transport, err := orasTransport(opts.InsecureSkipTLSVerify, opts.ResponseHeaderTimeout)
if err != nil {
return nil, err
}
client := &auth.Client{
Client: &http.Client{
Transport: transport,
},
Cache: auth.NewCache(),
Credential: credentials.Credential(credStore),
}

uniqueHosts := map[string]struct{}{}
for _, v := range imagesWithOverride {
uniqueHosts[v.overridden.Host] = struct{}{}
}
// We ping registries to pre-authenticate as some auth mechanisms open up a browser.
// When this happens concurrently a browser tab is opened for each image from that host and authenticating to one tab will not propagate creds
// Instead we auth synchronously with ping so the auth is cached before concurrent fetch.
if credStore.IsAuthConfigured() {
for host := range uniqueHosts {
registry, err := orasRemote.NewRegistry(host)
if err != nil {
return nil, fmt.Errorf("failed to create registry: %w", err)
}
registry.Client = client
// we can't error here because there may be a faked registry used for the docker fallback mechanism
_ = registry.Ping(ctx) //nolint: errcheck
}
client, err := NewAuthClientFromDocker(ctx, opts.InsecureSkipTLSVerify, opts.ResponseHeaderTimeout, uniqueHosts)
if err != nil {
return nil, err
}

l.Debug("gathering credentials from default Docker config file", "credentialsConfigured", credStore.IsAuthConfigured())
platform := &ocispec.Platform{
Architecture: opts.Arch,
// TODO: in the future we could support Windows images
Expand Down
28 changes: 26 additions & 2 deletions src/pkg/packager/find_images.go
Original file line number Diff line number Diff line change
Expand Up @@ -400,14 +400,38 @@ func findImages(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath strin
"duration", time.Since(imgCompStart))

if !opts.SkipCosign {
cosignHosts := map[string]struct{}{}
addCosignHosts := func(images []string) error {
for _, image := range images {
parsed, parseErr := transform.ParseImageRef(image)
if parseErr != nil {
return fmt.Errorf("could not parse image reference for cosign pre-auth %s: %w", image, parseErr)
}
cosignHosts[parsed.Host] = struct{}{}
}

return nil
}
if err := addCosignHosts(scan.Matches); err != nil {
return nil, err
}
if err := addCosignHosts(scan.PotentialMatches); err != nil {
return nil, err
}

cosignClient, err := images.NewAuthClientFromDocker(ctx, opts.InsecureSkipTLSVerify, 0, cosignHosts)
if err != nil {
return nil, err
}

// Handle cosign artifact lookups
if len(scan.Matches) > 0 || len(scan.PotentialMatches) > 0 {
imgStart := time.Now()
l.Info("looking up cosign artifacts for discovered images", "count", len(scan.Matches)+len(scan.PotentialMatches))

for _, image := range scan.Matches {
l.Debug("looking up cosign artifacts for image", "name", image)
cosignArtifacts, err := utils.GetCosignArtifacts(image)
cosignArtifacts, err := utils.GetCosignArtifacts(ctx, image, cosignClient)
if err != nil {
return nil, fmt.Errorf("could not lookup the cosign artifacts for image %s: %w", image, err)
}
Expand All @@ -416,7 +440,7 @@ func findImages(ctx context.Context, pkg v1alpha1.ZarfPackage, packagePath strin

for _, image := range scan.PotentialMatches {
l.Debug("looking up cosign artifacts for image", "name", image)
cosignArtifacts, err := utils.GetCosignArtifacts(image)
cosignArtifacts, err := utils.GetCosignArtifacts(ctx, image, cosignClient)
if err != nil {
return nil, fmt.Errorf("could not lookup the cosign artifacts for image %s: %w", image, err)
}
Expand Down
99 changes: 82 additions & 17 deletions src/pkg/utils/oci_artifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,52 +5,117 @@
package utils

import (
"context"
"errors"
"fmt"

"github.com/google/go-containerregistry/pkg/name"
ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote"
"github.com/zarf-dev/zarf/src/pkg/logger"
"oras.land/oras-go/v2"
"oras.land/oras-go/v2/errdef"
"oras.land/oras-go/v2/registry"
orasRemote "oras.land/oras-go/v2/registry/remote"
"oras.land/oras-go/v2/registry/remote/auth"
)

// GetCosignArtifacts returns signatures and attestations for the given image.
func GetCosignArtifacts(image string) ([]string, error) {
func GetCosignArtifacts(ctx context.Context, image string, client *auth.Client) ([]string, error) {
l := logger.From(ctx)

var nameOpts []name.Option
if client == nil {
return nil, fmt.Errorf("auth client is required")
}

ref, err := name.ParseReference(image, nameOpts...)
if err != nil {
return nil, err
}

var remoteOpts []ociremote.Option
simg, _ := ociremote.SignedEntity(ref, remoteOpts...) //nolint:errcheck
if simg == nil {
// We get the digest reference for the image specifically so that we can short circuit the
// `crane` lookup that would otherwise happen in ociremote.SignatureTag and ociremote.AttestationTag
digestRef, err := imageDigestRef(ctx, image, ref, client)
if err != nil {
// If we can't get the digest reference, we can't get the cosign artifacts so log the error and skip it
l.Debug("could not get digest reference for image", "image", image, "error", err)
return nil, nil
}

sigRef, _ := ociremote.SignatureTag(ref, remoteOpts...) //nolint:errcheck
attRef, _ := ociremote.AttestationTag(ref, remoteOpts...) //nolint:errcheck

ss, err := simg.Signatures()
sigTag, err := ociremote.SignatureTag(digestRef)
if err != nil {
return nil, err
}
ssLayers, err := ss.Layers()
attTag, err := ociremote.AttestationTag(digestRef)
if err != nil {
return nil, err
}

var cosignArtifactList = make([]string, 0)
if 0 < len(ssLayers) {
cosignArtifactList = append(cosignArtifactList, sigRef.String())
}
var cosignArtifactList = make([]string, 0, 2)

atts, err := simg.Attestations()
sigExists, err := existsInRemote(ctx, sigTag.String(), client)
if err != nil {
return nil, err
}
aLayers, err := atts.Layers()
if sigExists {
cosignArtifactList = append(cosignArtifactList, sigTag.String())
}

attExists, err := existsInRemote(ctx, attTag.String(), client)
if err != nil {
return nil, err
}
if 0 < len(aLayers) {
cosignArtifactList = append(cosignArtifactList, attRef.String())
if attExists {
cosignArtifactList = append(cosignArtifactList, attTag.String())
}

return cosignArtifactList, nil
}

func imageDigestRef(ctx context.Context, reference string, parsedRef name.Reference, client *auth.Client) (name.Digest, error) {
if digestRef, ok := parsedRef.(name.Digest); ok {
return digestRef, nil
}

repo := &orasRemote.Repository{}
orasRef, err := registry.ParseReference(reference)
if err != nil {
return name.Digest{}, err
}
repo.Reference = orasRef
repo.Client = client

desc, err := oras.Resolve(ctx, repo, reference, oras.DefaultResolveOptions)
if err != nil {
return name.Digest{}, err
}

digestRef, err := name.NewDigest(fmt.Sprintf("%s@%s", parsedRef.Context().Name(), desc.Digest.String()))
if err != nil {
return name.Digest{}, err
}

return digestRef, nil
}

func existsInRemote(ctx context.Context, reference string, client *auth.Client) (bool, error) {
repo := &orasRemote.Repository{}

ref, err := registry.ParseReference(reference)
if err != nil {
return false, err
}
repo.Reference = ref
repo.Client = client

_, err = oras.Resolve(ctx, repo, reference, oras.DefaultResolveOptions)
if err != nil {
if errors.Is(err, errdef.ErrNotFound) {
return false, nil
}

return false, err
}

return true, nil
}
Loading