From 8d17896080cb6e0b117203be9c7014b7f3a533f0 Mon Sep 17 00:00:00 2001 From: ejahnGithub Date: Mon, 5 Aug 2024 12:25:52 -0700 Subject: [PATCH] refactor the logic and logging --- pkg/cmd/attestation/artifact/artifact.go | 4 +- pkg/cmd/attestation/artifact/image.go | 35 +++++- pkg/cmd/attestation/artifact/oci/client.go | 114 ++++++------------ pkg/cmd/attestation/download/download.go | 2 +- pkg/cmd/attestation/inspect/inspect.go | 2 +- .../attestation/verification/attestation.go | 18 ++- pkg/cmd/attestation/verify/options.go | 43 +++---- pkg/cmd/attestation/verify/verify.go | 30 ++--- 8 files changed, 120 insertions(+), 128 deletions(-) diff --git a/pkg/cmd/attestation/artifact/artifact.go b/pkg/cmd/attestation/artifact/artifact.go index f9302b439..e8e0ea095 100644 --- a/pkg/cmd/attestation/artifact/artifact.go +++ b/pkg/cmd/attestation/artifact/artifact.go @@ -54,14 +54,14 @@ func normalizeReference(reference string, pathSeparator rune) (normalized string return filepath.Clean(reference), fileArtifactType, nil } -func NewDigestedArtifact(client oci.Client, reference, digestAlg string) (artifact *DigestedArtifact, err error) { +func NewDigestedArtifact(client oci.Client, reference, digestAlg string, useBundleFromRegistry bool) (artifact *DigestedArtifact, err error) { normalized, artifactType, err := normalizeReference(reference, os.PathSeparator) if err != nil { return nil, err } if artifactType == ociArtifactType { // TODO: should we allow custom digestAlg for OCI artifacts? - return digestContainerImageArtifact(normalized, client) + return digestContainerImageArtifact(normalized, client, useBundleFromRegistry) } return digestLocalFileArtifact(normalized, digestAlg) } diff --git a/pkg/cmd/attestation/artifact/image.go b/pkg/cmd/attestation/artifact/image.go index 45e3104c2..e1fba97bf 100644 --- a/pkg/cmd/attestation/artifact/image.go +++ b/pkg/cmd/attestation/artifact/image.go @@ -7,7 +7,7 @@ import ( "github.com/distribution/reference" ) -func digestContainerImageArtifact(url string, client oci.Client) (*DigestedArtifact, error) { +func digestContainerImageArtifact(url string, client oci.Client, useBundleFromRegistry bool) (*DigestedArtifact, error) { // try to parse the url as a valid registry reference named, err := reference.Parse(url) if err != nil { @@ -15,15 +15,38 @@ func digestContainerImageArtifact(url string, client oci.Client) (*DigestedArtif return nil, fmt.Errorf("artifact %s is not a valid registry reference: %v", url, err) } - digest, attestations, err := client.GetImageDigest(named.String()) + name, err := client.ParseReference(named.String()) + if err != nil { + return nil, err + } + + digest, err := client.GetImageDigest(name) + + if err != nil { + return nil, err + } + if useBundleFromRegistry { + attestations, err := client.GetAttestations(name, digest) + + if err != nil { + return nil, err + } + + return &DigestedArtifact{ + URL: fmt.Sprintf("oci://%s", named.String()), + digest: digest.Hex, + digestAlg: digest.Algorithm, + attestations: attestations, + }, nil + } + if err != nil { return nil, err } return &DigestedArtifact{ - URL: fmt.Sprintf("oci://%s", named.String()), - digest: digest.Hex, - digestAlg: digest.Algorithm, - attestations: attestations, + URL: fmt.Sprintf("oci://%s", named.String()), + digest: digest.Hex, + digestAlg: digest.Algorithm, }, nil } diff --git a/pkg/cmd/attestation/artifact/oci/client.go b/pkg/cmd/attestation/artifact/oci/client.go index e0a497e40..9e53c6f2b 100644 --- a/pkg/cmd/attestation/artifact/oci/client.go +++ b/pkg/cmd/attestation/artifact/oci/client.go @@ -1,7 +1,6 @@ package oci import ( - "bytes" "errors" "fmt" "io" @@ -21,7 +20,9 @@ var ErrDenied = errors.New("the provided token was denied access to the requeste var ErrRegistryAuthz = errors.New("remote registry authorization failed, please authenticate with the registry and try again") type Client interface { - GetImageDigest(imgName string) (*v1.Hash, []*api.Attestation, error) + GetImageDigest(name name.Reference) (*v1.Hash, error) + GetAttestations(name name.Reference, digest *v1.Hash) ([]*api.Attestation, error) + ParseReference(ref string) (name.Reference, error) } func checkForUnauthorizedOrDeniedErr(err transport.Error) error { @@ -41,36 +42,40 @@ type LiveClient struct { get func(name.Reference, ...remote.Option) (*remote.Descriptor, error) } +func (c LiveClient) ParseReference(ref string) (name.Reference, error) { + return c.parseReference(ref) +} + // where name is formed like ghcr.io/github/my-image-repo -func (c LiveClient) GetImageDigest(imgName string) (*v1.Hash, []*api.Attestation, error) { - nameFirst, err := c.parseReference(imgName) - var buf bytes.Buffer - attestations := []*api.Attestation{} - - if err != nil { - return nil, attestations, fmt.Errorf("failed to create image tag: %v", err) - } - +func (c LiveClient) GetImageDigest(name name.Reference) (*v1.Hash, error) { // The user must already be authenticated with the container registry // The authn.DefaultKeychain argument indicates that Get should checks the // user's configuration for the registry credentials - desc, err := c.get(nameFirst, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + desc, err := c.get(name, remote.WithAuthFromKeychain(authn.DefaultKeychain)) if err != nil { var transportErr *transport.Error if errors.As(err, &transportErr) { if accessErr := checkForUnauthorizedOrDeniedErr(*transportErr); accessErr != nil { - return nil, attestations, accessErr + return nil, accessErr } } - return nil, attestations, fmt.Errorf("failed to fetch remote image: %v", err) + return nil, fmt.Errorf("failed to fetch remote image: %v", err) } - dgst := nameFirst.Context().Digest(desc.Digest.String()) + return &desc.Digest, nil +} - ref, err := remote.Referrers(dgst, remote.WithAuthFromKeychain(authn.DefaultKeychain)) +func (c LiveClient) GetAttestations(name name.Reference, digest *v1.Hash) ([]*api.Attestation, error) { + attestations := []*api.Attestation{} + nameDegist := name.Context().Digest(digest.String()) + + ref, err := remote.Referrers(nameDegist, remote.WithAuthFromKeychain(authn.DefaultKeychain)) + if err != nil { + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) + } indexManifest, err := ref.IndexManifest() if err != nil { - return nil, attestations, fmt.Errorf("failed to fetch remote image: %v", err) + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) } manifests := indexManifest.Manifests @@ -81,94 +86,55 @@ func (c LiveClient) GetImageDigest(imgName string) (*v1.Hash, []*api.Attestation if allowedType == m.ArtifactType { manifestDigest := m.Digest.String() - manifestURL := fmt.Sprintf("%s/manifests/%s", imgName, manifestDigest) - fmt.Println(manifestURL) - - digest2 := nameFirst.Context().Digest(manifestDigest) + digest2 := nameDegist.Context().Digest(manifestDigest) // replace to use GET for more correc type img2, err := remote.Image(digest2, remote.WithAuthFromKeychain(authn.DefaultKeychain)) if err != nil { - return nil, attestations, fmt.Errorf("failed to fetch remote image: %v", err) + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) } - // manifest2, err := ref2.Manifest() - // if err != nil { - // return nil, fmt.Errorf("failed to fetch remote image: %v", err) - // } - // fmt.Println(manifest2.MediaType) // Step 4: Get the layers layers, err := img2.Layers() if err != nil { - fmt.Println("Error getting layers: %v", err) - return nil, attestations, fmt.Errorf("failed to fetch remote image: %v", err) + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) } // For simplicity, we'll just fetch the first layer if len(layers) > 0 { - fmt.Println("how many layers?", len(layers)) layer := layers[0] // Step 5: Read the blob (layer) content rc, err := layer.Compressed() if err != nil { - fmt.Println("Error getting compressed layer: %v", err) - return nil, attestations, fmt.Errorf("failed to fetch remote image: %v", err) - + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) } defer rc.Close() layerBytes, err := io.ReadAll(rc) if err != nil { - // If creating a gzip reader fails, it might not be compressed - fmt.Println("Layer is not gzip-compressed. Reading raw content.") - // fmt.Println("Blob content:", buf.String()) - - var bundle bundle.ProtobufBundle - bundle.Bundle = new(protobundle.Bundle) - err = bundle.UnmarshalJSON(layerBytes) - fmt.Println("") - fmt.Println("JSON Content:", string(layerBytes)) - fmt.Println("") - - if err != nil { - fmt.Println("failed to unmarshal bundle from JSON: %v", err) - } else { - fmt.Println("Bundle content:", bundle.String()) - } - - a := api.Attestation{Bundle: &bundle} - attestations = append(attestations, &a) - - } else { - defer gz.Close() - - var decompressed bytes.Buffer - if _, err := io.Copy(&decompressed, gz); err != nil { - fmt.Println("Error decompressing layer content: %v", err) - } - - // Now you have the decompressed blob content in 'decompressed' buffer - fmt.Println("Decompressed blob content:", decompressed.String()) + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) } - // Now you have the decompressed blob content in 'decompressed' buffer - // fmt.Println("Blob content:", decompressed.String()) + var bundle bundle.ProtobufBundle + bundle.Bundle = new(protobundle.Bundle) + err = bundle.UnmarshalJSON(layerBytes) + + if err != nil { + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) + } + + a := api.Attestation{Bundle: &bundle} + attestations = append(attestations, &a) + } else { - fmt.Println("No layers found in the image.") + return attestations, fmt.Errorf("failed to fetch remote image: %v", err) } } } } - - // msgName, err := c.parseReference(msg) - // fmt.Println() - // if err != nil { - // return nil, err - // } - - return &desc.Digest, attestations, nil + return attestations, nil } // Unlike other parts of this command set, we cannot pass a custom HTTP client diff --git a/pkg/cmd/attestation/download/download.go b/pkg/cmd/attestation/download/download.go index 3af3b6200..6d74027ae 100644 --- a/pkg/cmd/attestation/download/download.go +++ b/pkg/cmd/attestation/download/download.go @@ -111,7 +111,7 @@ func NewDownloadCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Comman } func runDownload(opts *Options) error { - artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) + artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm, false) if err != nil { return fmt.Errorf("failed to digest artifact: %v", err) } diff --git a/pkg/cmd/attestation/inspect/inspect.go b/pkg/cmd/attestation/inspect/inspect.go index 2731ee7a4..f38181b77 100644 --- a/pkg/cmd/attestation/inspect/inspect.go +++ b/pkg/cmd/attestation/inspect/inspect.go @@ -97,7 +97,7 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command } func runInspect(opts *Options) error { - artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) + artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm, false) if err != nil { return fmt.Errorf("failed to digest artifact: %s", err) } diff --git a/pkg/cmd/attestation/verification/attestation.go b/pkg/cmd/attestation/verification/attestation.go index 52a8ff025..902657d41 100644 --- a/pkg/cmd/attestation/verification/attestation.go +++ b/pkg/cmd/attestation/verification/attestation.go @@ -16,12 +16,13 @@ import ( var ErrUnrecognisedBundleExtension = errors.New("bundle file extension not supported, must be json or jsonl") type FetchAttestationsConfig struct { - APIClient api.Client - BundlePath string - Digest string - Limit int - Owner string - Repo string + APIClient api.Client + BundlePath string + Digest string + Limit int + Owner string + Repo string + AttestationsFromOCI []*api.Attestation } func (c *FetchAttestationsConfig) IsBundleProvided() bool { @@ -32,6 +33,11 @@ func GetAttestations(c FetchAttestationsConfig) ([]*api.Attestation, error) { if c.IsBundleProvided() { return GetLocalAttestations(c.BundlePath) } + + if len(c.AttestationsFromOCI) > 0 { + return c.AttestationsFromOCI, nil + } + return GetRemoteAttestations(c) } diff --git a/pkg/cmd/attestation/verify/options.go b/pkg/cmd/attestation/verify/options.go index da2c7bb4e..902024b90 100644 --- a/pkg/cmd/attestation/verify/options.go +++ b/pkg/cmd/attestation/verify/options.go @@ -15,27 +15,28 @@ import ( // Options captures the options for the verify command type Options struct { - ArtifactPath string - BundlePath string - Config func() (gh.Config, error) - TrustedRoot string - DenySelfHostedRunner bool - DigestAlgorithm string - Limit int - NoPublicGood bool - OIDCIssuer string - Owner string - PredicateType string - Repo string - SAN string - SANRegex string - SignerRepo string - SignerWorkflow string - APIClient api.Client - Logger *io.Handler - OCIClient oci.Client - SigstoreVerifier verification.SigstoreVerifier - exporter cmdutil.Exporter + ArtifactPath string + BundlePath string + UseBundleFromRegistry bool + Config func() (gh.Config, error) + TrustedRoot string + DenySelfHostedRunner bool + DigestAlgorithm string + Limit int + NoPublicGood bool + OIDCIssuer string + Owner string + PredicateType string + Repo string + SAN string + SANRegex string + SignerRepo string + SignerWorkflow string + APIClient api.Client + Logger *io.Handler + OCIClient oci.Client + SigstoreVerifier verification.SigstoreVerifier + exporter cmdutil.Exporter } // Clean cleans the file path option values diff --git a/pkg/cmd/attestation/verify/verify.go b/pkg/cmd/attestation/verify/verify.go index 990de3985..ffa87a383 100644 --- a/pkg/cmd/attestation/verify/verify.go +++ b/pkg/cmd/attestation/verify/verify.go @@ -150,6 +150,7 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command // general flags verifyCmd.Flags().StringVarP(&opts.BundlePath, "bundle", "b", "", "Path to bundle on disk, either a single bundle in a JSON file or a JSON lines file with multiple bundles") cmdutil.DisableAuthCheckFlag(verifyCmd.Flags().Lookup("bundle")) + verifyCmd.Flags().BoolVarP(&opts.UseBundleFromRegistry, "bundle-from-registry", "", false, "Use the bundle from the OCI registry") cmdutil.StringEnumFlag(verifyCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact") verifyCmd.Flags().StringVarP(&opts.Owner, "owner", "o", "", "GitHub organization to scope attestation lookup by") verifyCmd.Flags().StringVarP(&opts.Repo, "repo", "R", "", "Repository name in the format /") @@ -173,7 +174,7 @@ func NewVerifyCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command } func runVerify(opts *Options) error { - artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) + artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm, opts.UseBundleFromRegistry) if err != nil { opts.Logger.Printf(opts.Logger.ColorScheme.Red("✗ Loading digest for %s failed\n"), opts.ArtifactPath) return err @@ -182,12 +183,13 @@ func runVerify(opts *Options) error { opts.Logger.Printf("Loaded digest %s for %s\n", artifact.DigestWithAlg(), artifact.URL) c := verification.FetchAttestationsConfig{ - APIClient: opts.APIClient, - BundlePath: opts.BundlePath, - Digest: artifact.DigestWithAlg(), - Limit: opts.Limit, - Owner: opts.Owner, - Repo: opts.Repo, + APIClient: opts.APIClient, + BundlePath: opts.BundlePath, + Digest: artifact.DigestWithAlg(), + Limit: opts.Limit, + Owner: opts.Owner, + Repo: opts.Repo, + AttestationsFromOCI: artifact.Attestations(), } attestations, err := verification.GetAttestations(c) if err != nil { @@ -198,25 +200,19 @@ func runVerify(opts *Options) error { if c.IsBundleProvided() { opts.Logger.Printf(opts.Logger.ColorScheme.Red("✗ Loading attestations from %s failed\n"), artifact.URL) + } else if opts.UseBundleFromRegistry { + opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Loading attestations from OCI registry failed")) } else { opts.Logger.Println(opts.Logger.ColorScheme.Red("✗ Loading attestations from GitHub API failed")) } return err } - attestationsFromOCI := artifact.Attestations() - if len(attestationsFromOCI) > 0 { - attestations = append(attestations, attestationsFromOCI...) - - for _, attestation := range attestations { - // ja, err := attestation.Bundle.String() - opts.Logger.Printf("Loaded attestation from OCI registry: %s\n", attestation.Bundle.String()) - } - } - pluralAttestation := text.Pluralize(len(attestations), "attestation") if c.IsBundleProvided() { opts.Logger.Printf("Loaded %s from %s\n", pluralAttestation, opts.BundlePath) + } else if opts.UseBundleFromRegistry { + opts.Logger.Printf("Loaded %s from %s\n", pluralAttestation, opts.ArtifactPath) } else { opts.Logger.Printf("Loaded %s from GitHub API\n", pluralAttestation) }