mirror of
https://github.com/hauler-dev/hauler.git
synced 2026-05-16 22:26:58 +00:00
Signed-off-by: Adam Martin <adam.martin@ranchergovernment.com> Co-authored-by: Zack Brady <zackbrady123@gmail.com>
214 lines
5.7 KiB
Go
214 lines
5.7 KiB
Go
package cosign
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/sigstore/cosign/v3/cmd/cosign/cli"
|
|
"github.com/sigstore/cosign/v3/cmd/cosign/cli/options"
|
|
"github.com/sigstore/cosign/v3/cmd/cosign/cli/verify"
|
|
"hauler.dev/go/hauler/internal/flags"
|
|
"hauler.dev/go/hauler/pkg/artifacts/image"
|
|
"hauler.dev/go/hauler/pkg/consts"
|
|
"hauler.dev/go/hauler/pkg/log"
|
|
"hauler.dev/go/hauler/pkg/store"
|
|
"oras.land/oras-go/pkg/content"
|
|
)
|
|
|
|
// VerifySignature verifies the digital signature of a file using Sigstore/Cosign.
|
|
func VerifySignature(ctx context.Context, s *store.Layout, keyPath string, useTlog bool, ref string, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error {
|
|
l := log.FromContext(ctx)
|
|
operation := func() error {
|
|
v := &verify.VerifyCommand{
|
|
KeyRef: keyPath,
|
|
IgnoreTlog: true, // Ignore transparency log by default.
|
|
NewBundleFormat: true,
|
|
}
|
|
|
|
// if the user wants to use the transparency log, set the flag to false
|
|
if useTlog {
|
|
v.IgnoreTlog = false
|
|
}
|
|
|
|
err := log.CaptureOutput(l, true, func() error {
|
|
return v.Exec(ctx, []string{ref})
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return RetryOperation(ctx, rso, ro, operation)
|
|
}
|
|
|
|
// VerifyKeylessSignature verifies the digital signature of a file using Sigstore/Cosign.
|
|
func VerifyKeylessSignature(ctx context.Context, s *store.Layout, identity string, identityRegexp string, oidcIssuer string, oidcIssuerRegexp string, ghWorkflowRepository string, useTlog bool, ref string, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error {
|
|
l := log.FromContext(ctx)
|
|
operation := func() error {
|
|
|
|
certVerifyOptions := options.CertVerifyOptions{
|
|
CertOidcIssuer: oidcIssuer,
|
|
CertOidcIssuerRegexp: oidcIssuerRegexp,
|
|
CertIdentity: identity,
|
|
CertIdentityRegexp: identityRegexp,
|
|
CertGithubWorkflowRepository: ghWorkflowRepository,
|
|
}
|
|
|
|
v := &verify.VerifyCommand{
|
|
CertVerifyOptions: certVerifyOptions,
|
|
IgnoreTlog: false, // Ignore transparency log is set to false by default for keyless signature verification
|
|
CertGithubWorkflowRepository: ghWorkflowRepository,
|
|
NewBundleFormat: true,
|
|
}
|
|
|
|
// if the user wants to use the transparency log, set the flag to false
|
|
if useTlog {
|
|
v.IgnoreTlog = false
|
|
}
|
|
|
|
err := log.CaptureOutput(l, true, func() error {
|
|
return v.Exec(ctx, []string{ref})
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
return RetryOperation(ctx, rso, ro, operation)
|
|
}
|
|
|
|
// SaveImage saves image and any signatures/attestations to the store.
|
|
func SaveImage(ctx context.Context, s *store.Layout, ref string, platform string, rso *flags.StoreRootOpts, ro *flags.CliRootOpts) error {
|
|
l := log.FromContext(ctx)
|
|
|
|
if !ro.IgnoreErrors {
|
|
envVar := os.Getenv(consts.HaulerIgnoreErrors)
|
|
if envVar == "true" {
|
|
ro.IgnoreErrors = true
|
|
}
|
|
}
|
|
|
|
operation := func() error {
|
|
o := &options.SaveOptions{
|
|
Directory: s.Root,
|
|
}
|
|
|
|
// check to see if the image is multi-arch
|
|
isMultiArch, err := image.IsMultiArchImage(ref)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
l.Debugf("multi-arch image [%v]", isMultiArch)
|
|
|
|
// Conditionally add platform.
|
|
if platform != "" && isMultiArch {
|
|
l.Debugf("platform for image [%s]", platform)
|
|
o.Platform = platform
|
|
}
|
|
|
|
err = cli.SaveCmd(ctx, *o, ref)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
return RetryOperation(ctx, rso, ro, operation)
|
|
}
|
|
|
|
// LoadImage loads store to a remote registry.
|
|
func LoadImages(ctx context.Context, s *store.Layout, registry string, only string, ropts content.RegistryOptions, ro *flags.CliRootOpts) error {
|
|
l := log.FromContext(ctx)
|
|
|
|
o := &options.LoadOptions{
|
|
Directory: s.Root,
|
|
Registry: options.RegistryOptions{
|
|
Name: registry,
|
|
},
|
|
}
|
|
|
|
// Conditionally add extra flags.
|
|
if len(only) > 0 {
|
|
o.LoadOnly = only
|
|
}
|
|
|
|
if ropts.Insecure {
|
|
o.Registry.AllowInsecure = true
|
|
}
|
|
|
|
if ropts.PlainHTTP {
|
|
o.Registry.AllowHTTPRegistry = true
|
|
}
|
|
|
|
if ropts.Username != "" {
|
|
o.Registry.AuthConfig.Username = ropts.Username
|
|
o.Registry.AuthConfig.Password = ropts.Password
|
|
}
|
|
|
|
// execute the cosign load and capture the output in our logger
|
|
err := log.CaptureOutput(l, false, func() error {
|
|
return cli.LoadCmd(ctx, *o, "")
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func RetryOperation(ctx context.Context, rso *flags.StoreRootOpts, ro *flags.CliRootOpts, operation func() error) error {
|
|
l := log.FromContext(ctx)
|
|
|
|
if !ro.IgnoreErrors {
|
|
envVar := os.Getenv(consts.HaulerIgnoreErrors)
|
|
if envVar == "true" {
|
|
ro.IgnoreErrors = true
|
|
}
|
|
}
|
|
|
|
// Validate retries and fall back to a default
|
|
retries := rso.Retries
|
|
if retries <= 0 {
|
|
retries = consts.DefaultRetries
|
|
}
|
|
|
|
for attempt := 1; attempt <= rso.Retries; attempt++ {
|
|
err := operation()
|
|
if err == nil {
|
|
// If the operation succeeds, return nil (no error)
|
|
return nil
|
|
}
|
|
|
|
if ro.IgnoreErrors {
|
|
if strings.HasPrefix(err.Error(), "function execution failed: no matching signatures: rekor client not provided for online verification") {
|
|
l.Warnf("warning (attempt %d/%d)... failed tlog verification", attempt, rso.Retries)
|
|
} else {
|
|
l.Warnf("warning (attempt %d/%d)... %v", attempt, rso.Retries, err)
|
|
}
|
|
} else {
|
|
if strings.HasPrefix(err.Error(), "function execution failed: no matching signatures: rekor client not provided for online verification") {
|
|
l.Errorf("error (attempt %d/%d)... failed tlog verification", attempt, rso.Retries)
|
|
} else {
|
|
l.Errorf("error (attempt %d/%d)... %v", attempt, rso.Retries, err)
|
|
}
|
|
}
|
|
|
|
// If this is not the last attempt, wait before retrying
|
|
if attempt < rso.Retries {
|
|
time.Sleep(time.Second * consts.RetriesInterval)
|
|
}
|
|
}
|
|
|
|
// If all attempts fail, return an error
|
|
return fmt.Errorf("operation unsuccessful after %d attempts", rso.Retries)
|
|
}
|