diff --git a/pkg/cmd/attestation/inspect/inspect.go b/pkg/cmd/attestation/inspect/inspect.go index c139a5af2..56a8c1f63 100644 --- a/pkg/cmd/attestation/inspect/inspect.go +++ b/pkg/cmd/attestation/inspect/inspect.go @@ -1,18 +1,20 @@ package inspect import ( + "encoding/json" "fmt" + "time" "github.com/cli/cli/v2/internal/ghinstance" - "github.com/cli/cli/v2/internal/tableprinter" "github.com/cli/cli/v2/pkg/cmd/attestation/api" - "github.com/cli/cli/v2/pkg/cmd/attestation/artifact" - "github.com/cli/cli/v2/pkg/cmd/attestation/artifact/oci" - "github.com/cli/cli/v2/pkg/cmd/attestation/auth" "github.com/cli/cli/v2/pkg/cmd/attestation/io" "github.com/cli/cli/v2/pkg/cmd/attestation/verification" "github.com/cli/cli/v2/pkg/cmdutil" ghauth "github.com/cli/go-gh/v2/pkg/auth" + "github.com/digitorus/timestamp" + in_toto "github.com/in-toto/attestation/go/v1" + "github.com/sigstore/sigstore-go/pkg/bundle" + "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" @@ -28,18 +30,19 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command Long: heredoc.Docf(` ### NOTE: This feature is currently in public preview, and subject to change. - Inspect a downloaded Sigstore bundle for a given artifact. + Inspect a Sigstore bundle that has been downloaded to disk. See the %[1]sdownload%[1]s + command. - The command requires either: - * a relative path to a local artifact, or - * a container image URI (e.g. %[1]soci://%[1]s) + // The command requires either: + // * a relative path to a local artifact, or + // * a container image URI (e.g. %[1]soci://%[1]s) - Note that if you provide an OCI URI for the artifact you must already - be authenticated with a container registry. + // Note that if you provide an OCI URI for the artifact you must already + // be authenticated with a container registry. - The command also requires the %[1]s--bundle%[1]s flag, which provides a file - path to a previously downloaded Sigstore bundle. (See also the %[1]sdownload%[1]s - command). + // The command also requires the %[1]s--bundle%[1]s flag, which provides a file + // path to a previously downloaded Sigstore bundle. (See also the %[1]sdownload%[1]s + // command). By default, the command will print information about the bundle in a table format. If the %[1]s--json-result%[1]s flag is provided, the command will print the @@ -47,36 +50,36 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command `, "`"), Example: heredoc.Doc(` # Inspect a Sigstore bundle and print the results in table format - $ gh attestation inspect --bundle + $ gh attestation inspect # Inspect a Sigstore bundle and print the results in JSON format - $ gh attestation inspect --bundle --json-result + $ gh attestation inspect --json-result - # Inspect a Sigsore bundle for an OCI artifact, and print the results in table format - $ gh attestation inspect oci:// --bundle + // # Inspect a Sigsore bundle for an OCI artifact, and print the results in table format + // $ gh attestation inspect oci:// --bundle `), PreRunE: func(cmd *cobra.Command, args []string) error { // Create a logger for use throughout the inspect command opts.Logger = io.NewHandler(f.IOStreams) // set the artifact path - opts.ArtifactPath = args[0] + opts.BundlePath = args[0] // Clean file path options - // opts.Clean() + opts.Clean() return nil }, RunE: func(cmd *cobra.Command, args []string) error { - opts.OCIClient = oci.NewLiveClient() - if opts.Hostname == "" { - opts.Hostname, _ = ghauth.DefaultHost() - } - - if err := auth.IsHostSupported(opts.Hostname); err != nil { - return err - } - + // opts.OCIClient = oci.NewLiveClient() + // if opts.Hostname == "" { + // opts.Hostname, _ = ghauth.DefaultHost() + // } + // + // if err := auth.IsHostSupported(opts.Hostname); err != nil { + // return err + // } + // if runF != nil { return runF(opts) } @@ -84,7 +87,8 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command config := verification.SigstoreConfig{ Logger: opts.Logger, } - // Prepare for tenancy if detected + + // fetch the correct trust domain so we can verify the bundle if ghauth.IsTenancy(opts.Hostname) { hc, err := f.HttpClient() if err != nil { @@ -115,7 +119,7 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command } inspectCmd.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") - inspectCmd.MarkFlagRequired("bundle") //nolint:errcheck + // inspectCmd.MarkFlagRequired("bundle") //nolint:errcheck inspectCmd.Flags().StringVarP(&opts.Hostname, "hostname", "", "", "Configure host to use") cmdutil.StringEnumFlag(inspectCmd, &opts.DigestAlgorithm, "digest-alg", "d", "sha256", []string{"sha256", "sha512"}, "The algorithm used to compute a digest of the artifact") cmdutil.AddFormatFlags(inspectCmd, &opts.exporter) @@ -123,66 +127,201 @@ func NewInspectCmd(f *cmdutil.Factory, runF func(*Options) error) *cobra.Command return inspectCmd } -func runInspect(opts *Options) error { - artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) - if err != nil { - return fmt.Errorf("failed to digest artifact: %s", err) - } +type BundleInspection struct { + Certificate CertificateInspection + Statement in_toto.Statement + TransparencyLogEntries []TlogEntryInspection + SignedTimestamps []time.Time +} - opts.Logger.Printf("Verifying attestations for the artifact found at %s\n\n", artifact.URL) +type CertificateInspection struct { + certificate.Summary + NotBefore time.Time + NotAfter time.Time +} + +type TlogEntryInspection struct { + IntegratedTime time.Time + LogID string +} + +func runInspect(opts *Options) error { + // artifact, err := artifact.NewDigestedArtifact(opts.OCIClient, opts.ArtifactPath, opts.DigestAlgorithm) + // if err != nil { + // return fmt.Errorf("failed to digest artifact: %s", err) + // } + // + // opts.Logger.Printf("Verifying attestations for the artifact found at %s\n\n", artifact.URL) attestations, err := verification.GetLocalAttestations(opts.BundlePath) if err != nil { - return fmt.Errorf("failed to read attestations for subject: %s", artifact.DigestWithAlg()) + return fmt.Errorf("failed to read attestations") } - policy, err := buildPolicy(*artifact) - if err != nil { - return fmt.Errorf("failed to build policy: %v", err) - } + inspectedBundles := []BundleInspection{} - res := opts.SigstoreVerifier.Verify(attestations, policy) - if res.Error != nil { - return fmt.Errorf("at least one attestation failed to verify against Sigstore: %v", res.Error) - } + for _, a := range attestations { + inspectedBundle := BundleInspection{} - opts.Logger.VerbosePrint(opts.Logger.ColorScheme.Green( - "Successfully verified all attestations against Sigstore!\n\n", - )) - - // If the user provides the --format=json flag, print the results in JSON format - if opts.exporter != nil { - details, err := getAttestationDetails(opts.Tenant, res.VerifyResults) + entity := a.Bundle + verificationContent, err := entity.VerificationContent() if err != nil { - return fmt.Errorf("failed to get attestation detail: %v", err) + return fmt.Errorf("failed to fetch verification content: %w", err) } - // print the results to the terminal as an array of JSON objects - if err = opts.exporter.Write(opts.Logger.IO, details); err != nil { - return fmt.Errorf("failed to write JSON output") + if leafCert := verificationContent.GetCertificate(); leafCert != nil { + + certSummary, err := certificate.SummarizeCertificate(leafCert) + if err != nil { + return fmt.Errorf("failed to summarize certificate: %w", err) + } + + inspectedCert := CertificateInspection{ + Summary: certSummary, + NotBefore: leafCert.NotBefore, + NotAfter: leafCert.NotAfter, + } + + inspectedBundle.Certificate = inspectedCert + PrettyPrint(inspectedCert) } - return nil - } - // otherwise, print results in a table - details, err := getDetailsAsSlice(opts.Tenant, res.VerifyResults) - if err != nil { - return fmt.Errorf("failed to parse attestation details: %v", err) - } - - headers := []string{"Repo Name", "Repo ID", "Org Name", "Org ID", "Workflow ID"} - t := tableprinter.New(opts.Logger.IO, tableprinter.WithHeader(headers...)) - - for _, row := range details { - for _, field := range row { - t.AddField(field, tableprinter.WithTruncate(nil)) + sigContent, err := entity.SignatureContent() + if err != nil { + return fmt.Errorf("failed to fetch signature content: %w", err) } - t.EndRow() + + if envelope := sigContent.EnvelopeContent(); envelope != nil { + stmt, err := envelope.Statement() + if err != nil { + return fmt.Errorf("failed to fetch envelope statement: %w", err) + } + + inspectedBundle.Statement = *stmt + PrettyPrint(stmt) + } + + tlogTimestamps, err := dumpTlogs(entity) + if err != nil { + return fmt.Errorf("failed to dump tlog: %w", err) + } + inspectedBundle.TransparencyLogEntries = tlogTimestamps + PrettyPrint(tlogTimestamps) + + signedTimestamps, err := dumpSignedTimestamps(entity) + if err != nil { + return fmt.Errorf("faield to dump tsa: %w", err) + } + inspectedBundle.SignedTimestamps = signedTimestamps + PrettyPrint(signedTimestamps) + + // collect timestamps + + // fmt.Println(a.Bundle) + inspectedBundles = append(inspectedBundles, inspectedBundle) } - if err = t.Render(); err != nil { - return fmt.Errorf("failed to print output: %v", err) - } + // policy, err := buildPolicy(*artifact) + // if err != nil { + // return fmt.Errorf("failed to build policy: %v", err) + // } + // + // res := opts.SigstoreVerifier.Verify(attestations, policy) + // if res.Error != nil { + // return fmt.Errorf("at least one attestation failed to verify against Sigstore: %v", res.Error) + // } + // + // opts.Logger.VerbosePrint(opts.Logger.ColorScheme.Green( + // "Successfully verified all attestations against Sigstore!\n\n", + // )) + // + // If the user provides the --format=json flag, print the results in JSON format + // if opts.exporter != nil { + // details, err := getAttestationDetails(opts.Tenant, res.VerifyResults) + // if err != nil { + // return fmt.Errorf("failed to get attestation detail: %v", err) + // } + // + // // print the results to the terminal as an array of JSON objects + // if err = opts.exporter.Write(opts.Logger.IO, details); err != nil { + // return fmt.Errorf("failed to write JSON output") + // } + // return nil + // } + // + // // otherwise, print results in a table + // details, err := getDetailsAsSlice(opts.Tenant, res.VerifyResults) + // if err != nil { + // return fmt.Errorf("failed to parse attestation details: %v", err) + // } + // + // headers := []string{"Repo Name", "Repo ID", "Org Name", "Org ID", "Workflow ID"} + // t := tableprinter.New(opts.Logger.IO, tableprinter.WithHeader(headers...)) + // + // for _, row := range details { + // for _, field := range row { + // t.AddField(field, tableprinter.WithTruncate(nil)) + // } + // t.EndRow() + // } + // + // if err = t.Render(); err != nil { + // return fmt.Errorf("failed to print output: %v", err) + // } + // + PrettyPrint(inspectedBundles) return nil } + +func dumpTlogs(entity *bundle.Bundle) ([]TlogEntryInspection, error) { + + inspectedTlogEntries := []TlogEntryInspection{} + + entries, err := entity.TlogEntries() + if err != nil { + return nil, err + } + + for _, entry := range entries { + inspectedEntry := TlogEntryInspection{ + IntegratedTime: entry.IntegratedTime(), + LogID: entry.LogKeyID(), + } + + inspectedTlogEntries = append(inspectedTlogEntries, inspectedEntry) + } + + return inspectedTlogEntries, nil +} + +func dumpSignedTimestamps(entity *bundle.Bundle) ([]time.Time, error) { + timestamps := []time.Time{} + + signedTimestamps, err := entity.Timestamps() + if err != nil { + return nil, err + } + + for _, signedTsBytes := range signedTimestamps { + tsaTime, err := timestamp.ParseResponse(signedTsBytes) + + if err != nil { + return nil, err + } + + timestamps = append(timestamps, tsaTime.Time) + } + + return timestamps, nil +} + +func PrettyPrint(v interface{}) (err error) { + b, err := json.MarshalIndent(v, "", " ") + + if err == nil { + fmt.Println(string(b)) + return nil + } + return err +}