diff --git a/cmd/cosign/cli/attest.go b/cmd/cosign/cli/attest.go index 66334471780..0a1e6120f3e 100644 --- a/cmd/cosign/cli/attest.go +++ b/cmd/cosign/cli/attest.go @@ -87,6 +87,7 @@ func Attest() *cobra.Command { OIDCProvider: o.OIDC.Provider, SkipConfirmation: o.SkipConfirmation, TSAServerURL: o.TSAServerURL, + NewBundleFormat: o.NewBundleFormat, } attestCommand := attest.AttestCommand{ KeyOpts: ko, diff --git a/cmd/cosign/cli/attest/attest.go b/cmd/cosign/cli/attest/attest.go index b23d587fc33..b51e33a5542 100644 --- a/cmd/cosign/cli/attest/attest.go +++ b/cmd/cosign/cli/attest/attest.go @@ -49,7 +49,7 @@ import ( type tlogUploadFn func(*client.Rekor, []byte) (*models.LogEntryAnon, error) -func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, upload tlogUploadFn) (*cbundle.RekorBundle, error) { +func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, upload tlogUploadFn) (*models.LogEntryAnon, error) { rekorBytes, err := sv.Bytes(ctx) if err != nil { return nil, err @@ -64,7 +64,7 @@ func uploadToTlog(ctx context.Context, sv *sign.SignerVerifier, rekorURL string, return nil, err } fmt.Fprintln(os.Stderr, "tlog entry created with index:", *entry.LogIndex) - return cbundle.EntryToBundle(entry), nil + return entry, nil } // nolint @@ -174,20 +174,28 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { if sv.Cert != nil { opts = append(opts, static.WithCertChain(sv.Cert, sv.Chain)) } + var timestampBytes []byte + var tsaPayload []byte if c.KeyOpts.TSAServerURL != "" { - // TODO - change this when we implement protobuf / new bundle support + // We need to decide what signature to send to the timestamp authority. // - // Historically, cosign sent the entire JSON DSSE Envelope to the - // timestamp authority. However, when sigstore clients are verifying a - // bundle they will use the DSSE Sig field, so we choose what signature - // to send to the timestamp authority based on our output format. - // - // See cmd/cosign/cli/attest/attest_blob.go - responseBytes, err := tsa.GetTimestampedSignature(signedPayload, tsaclient.NewTSAClient(c.KeyOpts.TSAServerURL)) + // Historically, cosign sent `signedPayload`, which is the entire JSON DSSE + // Envelope. However, when sigstore clients are verifying a bundle they + // will use the DSSE Sig field, so we choose what signature to send to + // the timestamp authority based on our output format. + if c.KeyOpts.NewBundleFormat { + tsaPayload, err = getEnvelopeSigBytes(signedPayload) + if err != nil { + return err + } + } else { + tsaPayload = signedPayload + } + timestampBytes, err = tsa.GetTimestampedSignature(tsaPayload, tsaclient.NewTSAClient(c.KeyOpts.TSAServerURL)) if err != nil { return err } - bundle := cbundle.TimestampToRFC3161Timestamp(responseBytes) + bundle := cbundle.TimestampToRFC3161Timestamp(timestampBytes) opts = append(opts, static.WithRFC3161Timestamp(bundle)) } @@ -208,8 +216,9 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { if err != nil { return fmt.Errorf("should upload to tlog: %w", err) } + var rekorEntry *models.LogEntryAnon if shouldUpload { - bundle, err := uploadToTlog(ctx, sv, c.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) { + rekorEntry, err = uploadToTlog(ctx, sv, c.RekorURL, func(r *client.Rekor, b []byte) (*models.LogEntryAnon, error) { if c.RekorEntryType == "intoto" { return cosign.TLogUploadInTotoAttestation(ctx, r, signedPayload, b) } else { @@ -220,7 +229,7 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { if err != nil { return err } - opts = append(opts, static.WithBundle(bundle)) + opts = append(opts, static.WithBundle(cbundle.EntryToBundle(rekorEntry))) } sig, err := static.NewAttestation(signedPayload, opts...) @@ -228,6 +237,18 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { return err } + if c.KeyOpts.NewBundleFormat { + signerBytes, err := sv.Bytes(ctx) + if err != nil { + return err + } + bundleBytes, err := makeNewBundle(sv, rekorEntry, payload, signedPayload, signerBytes, timestampBytes) + if err != nil { + return err + } + return ociremote.WriteAttestationNewBundleFormat(digest.Repository, bundleBytes, predicateType, ociremoteOpts...) + } + // We don't actually need to access the remote entity to attach things to it // so we use a placeholder here. se := ociremote.SignedUnknown(digest, ociremoteOpts...) diff --git a/cmd/cosign/cli/attest/attest_blob.go b/cmd/cosign/cli/attest/attest_blob.go index beb8ecf0bf9..02f0db16940 100644 --- a/cmd/cosign/cli/attest/attest_blob.go +++ b/cmd/cosign/cli/attest/attest_blob.go @@ -162,6 +162,7 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error var rfc3161Timestamp *cbundle.RFC3161Timestamp var timestampBytes []byte + var tsaPayload []byte var rekorEntry *models.LogEntryAnon if c.TSAServerURL != "" { @@ -172,28 +173,16 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error // will use the DSSE Sig field, so we choose what signature to send to // the timestamp authority based on our output format. if c.NewBundleFormat { - var envelope dsse.Envelope - err = json.Unmarshal(sig, &envelope) - if err != nil { - return err - } - if len(envelope.Signatures) == 0 { - return fmt.Errorf("envelope has no signatures") - } - envelopeSigBytes, err := base64.StdEncoding.DecodeString(envelope.Signatures[0].Sig) - if err != nil { - return err - } - - timestampBytes, err = tsa.GetTimestampedSignature(envelopeSigBytes, client.NewTSAClient(c.TSAServerURL)) + tsaPayload, err = getEnvelopeSigBytes(sig) if err != nil { return err } } else { - timestampBytes, err = tsa.GetTimestampedSignature(sig, client.NewTSAClient(c.TSAServerURL)) - if err != nil { - return err - } + tsaPayload = sig + } + timestampBytes, err = tsa.GetTimestampedSignature(tsaPayload, client.NewTSAClient(c.TSAServerURL)) + if err != nil { + return err } rfc3161Timestamp = cbundle.TimestampToRFC3161Timestamp(timestampBytes) // TODO: Consider uploading RFC3161 TS to Rekor diff --git a/cmd/cosign/cli/attest/common.go b/cmd/cosign/cli/attest/common.go index b9bb6dcebcc..e5f4589a343 100644 --- a/cmd/cosign/cli/attest/common.go +++ b/cmd/cosign/cli/attest/common.go @@ -15,9 +15,13 @@ package attest import ( + "encoding/base64" + "encoding/json" "fmt" "io" "os" + + "github.com/secure-systems-lab/go-securesystemslib/dsse" ) func predicateReader(predicatePath string) (io.ReadCloser, error) { @@ -33,3 +37,15 @@ func predicateReader(predicatePath string) (io.ReadCloser, error) { } return f, nil } + +func getEnvelopeSigBytes(envelopeBytes []byte) ([]byte, error) { + var envelope dsse.Envelope + err := json.Unmarshal(envelopeBytes, &envelope) + if err != nil { + return nil, err + } + if len(envelope.Signatures) == 0 { + return nil, fmt.Errorf("envelope has no signatures") + } + return base64.StdEncoding.DecodeString(envelope.Signatures[0].Sig) +} diff --git a/cmd/cosign/cli/options/attest.go b/cmd/cosign/cli/options/attest.go index 8139cddaefa..06bab74ce98 100644 --- a/cmd/cosign/cli/options/attest.go +++ b/cmd/cosign/cli/options/attest.go @@ -32,6 +32,7 @@ type AttestOptions struct { TSAServerURL string RekorEntryType string RecordCreationTimestamp bool + NewBundleFormat bool Rekor RekorOptions Fulcio FulcioOptions @@ -90,4 +91,6 @@ func (o *AttestOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVar(&o.RecordCreationTimestamp, "record-creation-timestamp", false, "set the createdAt timestamp in the attestation artifact to the time it was created; by default, cosign sets this to the zero value") + + cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", false, "attach a Sigstore bundle using OCI referrers API") } diff --git a/cmd/cosign/cli/options/certificate.go b/cmd/cosign/cli/options/certificate.go index 3df7b4b962e..a11e98364a4 100644 --- a/cmd/cosign/cli/options/certificate.go +++ b/cmd/cosign/cli/options/certificate.go @@ -38,6 +38,8 @@ type CertVerifyOptions struct { CertChain string SCT string IgnoreSCT bool + NewBundleFormat bool + TrustedRootPath string } var _ Interface = (*RekorOptions)(nil) @@ -103,6 +105,8 @@ func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVar(&o.IgnoreSCT, "insecure-ignore-sct", false, "when set, verification will not check that a certificate contains an embedded SCT, a proof of "+ "inclusion in a certificate transparency log") + cmd.Flags().StringVar(&o.TrustedRootPath, "trusted-root", "", "Path to a Sigstore TrustedRoot JSON file.") + cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", false, "expect the signature/attestation to be packaged in a Sigstore bundle") } func (o *CertVerifyOptions) Identities() ([]cosign.Identity, error) { diff --git a/cmd/cosign/cli/options/verify.go b/cmd/cosign/cli/options/verify.go index 3cdbb0e8a62..2502bd785c2 100644 --- a/cmd/cosign/cli/options/verify.go +++ b/cmd/cosign/cli/options/verify.go @@ -42,7 +42,7 @@ func (o *CommonVerifyOptions) AddFlags(cmd *cobra.Command) { "Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp") cmd.Flags().BoolVar(&o.UseSignedTimestamps, "use-signed-timestamps", false, - "use signed timestamps if available") + "verify rfc3161 timestamps") cmd.Flags().BoolVar(&o.IgnoreTlog, "insecure-ignore-tlog", false, "ignore transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. Artifacts "+ @@ -158,11 +158,9 @@ func (o *VerifyAttestationOptions) AddFlags(cmd *cobra.Command) { // VerifyBlobOptions is the top level wrapper for the `verify blob` command. type VerifyBlobOptions struct { - Key string - Signature string - BundlePath string - NewBundleFormat bool - TrustedRootPath string + Key string + Signature string + BundlePath string SecurityKey SecurityKeyOptions CertVerify CertVerifyOptions @@ -190,13 +188,6 @@ func (o *VerifyBlobOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.BundlePath, "bundle", "", "path to bundle FILE") - // TODO: have this default to true as a breaking change - cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", false, - "output bundle in new format that contains all verification material") - - cmd.Flags().StringVar(&o.TrustedRootPath, "trusted-root", "", - "path to trusted root FILE") - cmd.Flags().StringVar(&o.RFC3161TimestampPath, "rfc3161-timestamp", "", "path to RFC3161 timestamp FILE") } @@ -222,7 +213,6 @@ type VerifyBlobAttestationOptions struct { Key string SignaturePath string BundlePath string - NewBundleFormat bool TrustedRootPath string PredicateOptions @@ -255,13 +245,6 @@ func (o *VerifyBlobAttestationOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.BundlePath, "bundle", "", "path to bundle FILE") - // TODO: have this default to true as a breaking change - cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", false, - "output bundle in new format that contains all verification material") - - cmd.Flags().StringVar(&o.TrustedRootPath, "trusted-root", "", - "path to trusted root FILE") - cmd.Flags().BoolVar(&o.CheckClaims, "check-claims", true, "if true, verifies the provided blob's sha256 digest exists as an in-toto subject within the attestation. If false, only the DSSE envelope is verified.") diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index 703898523b7..1ce85817f99 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -137,6 +137,7 @@ against the transparency log.`, Offline: o.CommonVerifyOptions.Offline, TSACertChainPath: o.CommonVerifyOptions.TSACertChainPath, IgnoreTlog: o.CommonVerifyOptions.IgnoreTlog, + UseSignedTimestamps: o.CommonVerifyOptions.UseSignedTimestamps, MaxWorkers: o.CommonVerifyOptions.MaxWorkers, ExperimentalOCI11: o.CommonVerifyOptions.ExperimentalOCI11, } @@ -244,6 +245,7 @@ against the transparency log.`, Offline: o.CommonVerifyOptions.Offline, TSACertChainPath: o.CommonVerifyOptions.TSACertChainPath, IgnoreTlog: o.CommonVerifyOptions.IgnoreTlog, + UseSignedTimestamps: o.CommonVerifyOptions.UseSignedTimestamps, MaxWorkers: o.CommonVerifyOptions.MaxWorkers, } @@ -333,7 +335,7 @@ The blob may be specified as a path to a file or - for stdin.`, Slot: o.SecurityKey.Slot, RekorURL: o.Rekor.URL, BundlePath: o.BundlePath, - NewBundleFormat: o.NewBundleFormat, + NewBundleFormat: o.CertVerify.NewBundleFormat, RFC3161TimestampPath: o.RFC3161TimestampPath, TSACertChainPath: o.CommonVerifyOptions.TSACertChainPath, } @@ -345,7 +347,7 @@ The blob may be specified as a path to a file or - for stdin.`, CARoots: o.CertVerify.CARoots, CAIntermediates: o.CertVerify.CAIntermediates, SigRef: o.Signature, - TrustedRootPath: o.TrustedRootPath, + TrustedRootPath: o.CertVerify.TrustedRootPath, CertGithubWorkflowTrigger: o.CertVerify.CertGithubWorkflowTrigger, CertGithubWorkflowSHA: o.CertVerify.CertGithubWorkflowSha, CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName, @@ -404,7 +406,7 @@ The blob may be specified as a path to a file.`, Slot: o.SecurityKey.Slot, RekorURL: o.Rekor.URL, BundlePath: o.BundlePath, - NewBundleFormat: o.NewBundleFormat, + NewBundleFormat: o.CertVerify.NewBundleFormat, RFC3161TimestampPath: o.RFC3161TimestampPath, TSACertChainPath: o.CommonVerifyOptions.TSACertChainPath, } diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 17fd63e8330..04f26724510 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -41,6 +41,7 @@ import ( "github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v2/pkg/oci" sigs "github.com/sigstore/cosign/v2/pkg/signature" + "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" "github.com/sigstore/sigstore/pkg/signature/payload" @@ -142,9 +143,19 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { Identities: identities, Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, + UseSignedTimestamps: c.UseSignedTimestamps, MaxWorkers: c.MaxWorkers, ExperimentalOCI11: c.ExperimentalOCI11, + NewBundleFormat: c.NewBundleFormat, } + + if c.TrustedRootPath != "" { + co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath) + if err != nil { + return fmt.Errorf("loading trusted root: %w", err) + } + } + if c.CheckClaims { co.ClaimVerifier = cosign.SimpleClaimVerifier } diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index 93c27690455..dd6fa763b50 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -37,6 +37,7 @@ import ( "github.com/sigstore/cosign/v2/pkg/oci" "github.com/sigstore/cosign/v2/pkg/policy" sigs "github.com/sigstore/cosign/v2/pkg/signature" + "github.com/sigstore/sigstore-go/pkg/root" ) // VerifyAttestationCommand verifies a signature on a supplied container image @@ -118,20 +119,29 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e Identities: identities, Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, + UseSignedTimestamps: c.UseSignedTimestamps, MaxWorkers: c.MaxWorkers, + NewBundleFormat: c.NewBundleFormat, } if c.CheckClaims { co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier } + + if c.NewBundleFormat { + if err = checkSigstoreBundleUnsupportedOptions(c); err != nil { + return err + } + } + // Ignore Signed Certificate Timestamp if the flag is set or a key is provided - if shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) { + if shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk) && !c.NewBundleFormat { co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) if err != nil { return fmt.Errorf("getting ctlog public keys: %w", err) } } - if c.TSACertChainPath != "" || c.UseSignedTimestamps { + if c.TSACertChainPath != "" || c.UseSignedTimestamps && !c.NewBundleFormat { tsaCertificates, err := c.loadTSACertificates(ctx) if err != nil { return fmt.Errorf("unable to load TSA certificates: %w", err) @@ -141,7 +151,7 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts } - if !c.IgnoreTlog { + if !c.IgnoreTlog && !co.NewBundleFormat { if c.RekorURL != "" { rekorClient, err := rekor.NewClient(c.RekorURL) if err != nil { @@ -187,6 +197,10 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e return fmt.Errorf("initializing piv token verifier: %w", err) } case c.CertRef != "": + if c.NewBundleFormat { + // This shouldn't happen because we already checked for this above in checkSigstoreBundleUnsupportedOptions + return fmt.Errorf("unsupported: certificate reference currently not supported with --new-bundle-format") + } cert, err := loadCertFromFileOrURL(c.CertRef) if err != nil { return fmt.Errorf("loading certificate from reference: %w", err) @@ -223,6 +237,19 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e } co.SCT = sct } + case c.TrustedRootPath != "": + if !c.NewBundleFormat { + return fmt.Errorf("unsupported: trusted root path currently only supported with --new-bundle-format") + } + + // If a trusted root path is provided, we will use it to verify the bundle. + // Otherwise, the verifier will default to the public good instance. + if c.TrustedRootPath == "" { + co.TrustedMaterial, err = root.NewTrustedRootFromPath(c.TrustedRootPath) + if err != nil { + return fmt.Errorf("creating trusted root from path: %w", err) + } + } case c.CARoots != "": // CA roots + possible intermediates are already loaded into co.RootCerts with the call to // loadCertsKeylessVerification above. @@ -327,3 +354,19 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e return nil } + +func checkSigstoreBundleUnsupportedOptions(c *VerifyAttestationCommand) error { + if c.Cert != "" || c.CertRef != "" { + return fmt.Errorf("unsupported: certificate may not be provided using --cert when using --new-bundle-format (cert must be in bundle)") + } + if c.CertChain != "" { + return fmt.Errorf("unsupported: certificate chain may not be provided using --cert-chain when using --new-bundle-format (cert must be in bundle)") + } + if c.CARoots != "" || c.CAIntermediates != "" { + return fmt.Errorf("unsupported: CA roots/intermediates must be provided using --trusted-root when using --new-bundle-format") + } + if c.TSACertChainPath != "" { + return fmt.Errorf("unsupported: TSA certificate chain path may only be provided using --trusted-root when using --new-bundle-format") + } + return nil +} diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 79475c90d80..1b063c95ef8 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -137,6 +137,7 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { Identities: identities, Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, + UseSignedTimestamps: c.UseSignedTimestamps, } if c.RFC3161TimestampPath != "" && !(c.TSACertChainPath != "" || c.UseSignedTimestamps) { return fmt.Errorf("either TSA certificate chain path must be provided or use-signed-timestamps must be set when using RFC3161 timestamp path") diff --git a/cmd/cosign/cli/verify/verify_blob_attestation.go b/cmd/cosign/cli/verify/verify_blob_attestation.go index 3f2c33cc63b..6b458a37a5e 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -123,6 +123,7 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st IgnoreSCT: c.IgnoreSCT, Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, + UseSignedTimestamps: c.UseSignedTimestamps, } var h v1.Hash if c.CheckClaims { diff --git a/doc/cosign_attest.md b/doc/cosign_attest.md index 19e201291d6..18e7ec8a54d 100644 --- a/doc/cosign_attest.md +++ b/doc/cosign_attest.md @@ -57,6 +57,7 @@ cosign attest [flags] --insecure-skip-verify skip verifying fulcio published to the SCT (this should only be used for testing). --k8s-keychain whether to use the kubernetes keychain instead of the default keychain (supports workload identity). --key string path to the private key file, KMS URI or Kubernetes Secret + --new-bundle-format attach a Sigstore bundle using OCI referrers API --no-upload do not upload the generated attestation --oidc-client-id string OIDC client ID for application (default "sigstore") --oidc-client-secret-file string Path to file containing OIDC client secret for application diff --git a/doc/cosign_dockerfile_verify.md b/doc/cosign_dockerfile_verify.md index aea4d4aadde..0b0cade8a62 100644 --- a/doc/cosign_dockerfile_verify.md +++ b/doc/cosign_dockerfile_verify.md @@ -77,6 +77,7 @@ cosign dockerfile verify [flags] --key string path to the public key file, KMS URI or Kubernetes Secret --local-image whether the specified image is a path to an image saved locally via 'cosign save' --max-workers int the amount of maximum workers for parallel executions (default 10) + --new-bundle-format expect the signature/attestation to be packaged in a Sigstore bundle --offline only allow offline verification -o, --output string output format for the signing image information (json|text) (default "json") --payload string payload path or remote URL @@ -91,7 +92,8 @@ cosign dockerfile verify [flags] --sk whether to use a hardware security key --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp - --use-signed-timestamps use signed timestamps if available + --trusted-root string Path to a Sigstore TrustedRoot JSON file. + --use-signed-timestamps verify rfc3161 timestamps ``` ### Options inherited from parent commands diff --git a/doc/cosign_manifest_verify.md b/doc/cosign_manifest_verify.md index dc1af148c48..b003a47596d 100644 --- a/doc/cosign_manifest_verify.md +++ b/doc/cosign_manifest_verify.md @@ -71,6 +71,7 @@ cosign manifest verify [flags] --key string path to the public key file, KMS URI or Kubernetes Secret --local-image whether the specified image is a path to an image saved locally via 'cosign save' --max-workers int the amount of maximum workers for parallel executions (default 10) + --new-bundle-format expect the signature/attestation to be packaged in a Sigstore bundle --offline only allow offline verification -o, --output string output format for the signing image information (json|text) (default "json") --payload string payload path or remote URL @@ -85,7 +86,8 @@ cosign manifest verify [flags] --sk whether to use a hardware security key --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp - --use-signed-timestamps use signed timestamps if available + --trusted-root string Path to a Sigstore TrustedRoot JSON file. + --use-signed-timestamps verify rfc3161 timestamps ``` ### Options inherited from parent commands diff --git a/doc/cosign_verify-attestation.md b/doc/cosign_verify-attestation.md index 9b747ab9ef0..0c598cb424f 100644 --- a/doc/cosign_verify-attestation.md +++ b/doc/cosign_verify-attestation.md @@ -81,6 +81,7 @@ cosign verify-attestation [flags] --key string path to the public key file, KMS URI or Kubernetes Secret --local-image whether the specified image is a path to an image saved locally via 'cosign save' --max-workers int the amount of maximum workers for parallel executions (default 10) + --new-bundle-format expect the signature/attestation to be packaged in a Sigstore bundle --offline only allow offline verification -o, --output string output format for the signing image information (json|text) (default "json") --policy strings specify CUE or Rego files with policies to be used for validation @@ -93,8 +94,9 @@ cosign verify-attestation [flags] --sk whether to use a hardware security key --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp + --trusted-root string Path to a Sigstore TrustedRoot JSON file. --type string specify a predicate type (slsaprovenance|slsaprovenance02|slsaprovenance1|link|spdx|spdxjson|cyclonedx|vuln|openvex|custom) or an URI (default "custom") - --use-signed-timestamps use signed timestamps if available + --use-signed-timestamps verify rfc3161 timestamps ``` ### Options inherited from parent commands diff --git a/doc/cosign_verify-blob-attestation.md b/doc/cosign_verify-blob-attestation.md index a7c293359d6..7e29e375a91 100644 --- a/doc/cosign_verify-blob-attestation.md +++ b/doc/cosign_verify-blob-attestation.md @@ -49,7 +49,7 @@ cosign verify-blob-attestation [flags] --insecure-ignore-tlog ignore transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. Artifacts cannot be publicly verified when not included in a log --key string path to the public key file, KMS URI or Kubernetes Secret --max-workers int the amount of maximum workers for parallel executions (default 10) - --new-bundle-format output bundle in new format that contains all verification material + --new-bundle-format expect the signature/attestation to be packaged in a Sigstore bundle --offline only allow offline verification --private-infrastructure skip transparency log verification when verifying artifacts in a privately deployed infrastructure --rekor-url string address of rekor STL server (default "https://rekor.sigstore.dev") @@ -59,9 +59,9 @@ cosign verify-blob-attestation [flags] --sk whether to use a hardware security key --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp - --trusted-root string path to trusted root FILE + --trusted-root string Path to a Sigstore TrustedRoot JSON file. --type string specify a predicate type (slsaprovenance|slsaprovenance02|slsaprovenance1|link|spdx|spdxjson|cyclonedx|vuln|openvex|custom) or an URI (default "custom") - --use-signed-timestamps use signed timestamps if available + --use-signed-timestamps verify rfc3161 timestamps ``` ### Options inherited from parent commands diff --git a/doc/cosign_verify-blob.md b/doc/cosign_verify-blob.md index 92655cccbed..ff357443fe7 100644 --- a/doc/cosign_verify-blob.md +++ b/doc/cosign_verify-blob.md @@ -84,7 +84,7 @@ cosign verify-blob [flags] --insecure-ignore-tlog ignore transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. Artifacts cannot be publicly verified when not included in a log --key string path to the public key file, KMS URI or Kubernetes Secret --max-workers int the amount of maximum workers for parallel executions (default 10) - --new-bundle-format output bundle in new format that contains all verification material + --new-bundle-format expect the signature/attestation to be packaged in a Sigstore bundle --offline only allow offline verification --private-infrastructure skip transparency log verification when verifying artifacts in a privately deployed infrastructure --rekor-url string address of rekor STL server (default "https://rekor.sigstore.dev") @@ -94,8 +94,8 @@ cosign verify-blob [flags] --sk whether to use a hardware security key --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp - --trusted-root string path to trusted root FILE - --use-signed-timestamps use signed timestamps if available + --trusted-root string Path to a Sigstore TrustedRoot JSON file. + --use-signed-timestamps verify rfc3161 timestamps ``` ### Options inherited from parent commands diff --git a/doc/cosign_verify.md b/doc/cosign_verify.md index 0852f21fc81..ff9a6a03a68 100644 --- a/doc/cosign_verify.md +++ b/doc/cosign_verify.md @@ -98,6 +98,7 @@ cosign verify [flags] --key string path to the public key file, KMS URI or Kubernetes Secret --local-image whether the specified image is a path to an image saved locally via 'cosign save' --max-workers int the amount of maximum workers for parallel executions (default 10) + --new-bundle-format expect the signature/attestation to be packaged in a Sigstore bundle --offline only allow offline verification -o, --output string output format for the signing image information (json|text) (default "json") --payload string payload path or remote URL @@ -112,7 +113,8 @@ cosign verify [flags] --sk whether to use a hardware security key --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) --timestamp-certificate-chain string path to PEM-encoded certificate chain file for the RFC3161 timestamp authority. Must contain the root CA certificate. Optionally may contain intermediate CA certificates, and may contain the leaf TSA certificate if not present in the timestamp - --use-signed-timestamps use signed timestamps if available + --trusted-root string Path to a Sigstore TrustedRoot JSON file. + --use-signed-timestamps verify rfc3161 timestamps ``` ### Options inherited from parent commands diff --git a/go.mod b/go.mod index 0257a546a4b..c5756487303 100644 --- a/go.mod +++ b/go.mod @@ -35,7 +35,7 @@ require ( github.com/sigstore/protobuf-specs v0.3.2 github.com/sigstore/rekor v1.3.6 github.com/sigstore/sigstore v1.8.9 - github.com/sigstore/sigstore-go v0.6.1 + github.com/sigstore/sigstore-go v0.6.2 github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.8 github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.8 github.com/sigstore/sigstore/pkg/signature/kms/gcp v1.8.8 diff --git a/go.sum b/go.sum index b1cafadcba3..9bfda949ac2 100644 --- a/go.sum +++ b/go.sum @@ -616,8 +616,8 @@ github.com/sigstore/rekor v1.3.6 h1:QvpMMJVWAp69a3CHzdrLelqEqpTM3ByQRt5B5Kspbi8= github.com/sigstore/rekor v1.3.6/go.mod h1:JDTSNNMdQ/PxdsS49DJkJ+pRJCO/83nbR5p3aZQteXc= github.com/sigstore/sigstore v1.8.9 h1:NiUZIVWywgYuVTxXmRoTT4O4QAGiTEKup4N1wdxFadk= github.com/sigstore/sigstore v1.8.9/go.mod h1:d9ZAbNDs8JJfxJrYmulaTazU3Pwr8uLL9+mii4BNR3w= -github.com/sigstore/sigstore-go v0.6.1 h1:tGkkv1oDIER+QYU5MrjqlttQOVDWfSkmYwMqkJhB/cg= -github.com/sigstore/sigstore-go v0.6.1/go.mod h1:Xe5GHmUeACRFbomUWzVkf/xYCn8xVifb9DgqJrV2dIw= +github.com/sigstore/sigstore-go v0.6.2 h1:8uiywjt73vzfrGfWYVwVsiB1E1Qmwmpgr1kVpl4fs6A= +github.com/sigstore/sigstore-go v0.6.2/go.mod h1:pOIUH7Jx+ctwMICo+2zNrViOJJN5sGaQgwX4yAVJkA0= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.8 h1:2zHmUvaYCwV6LVeTo+OAkTm8ykOGzA9uFlAjwDPAUWM= github.com/sigstore/sigstore/pkg/signature/kms/aws v1.8.8/go.mod h1:OEhheBplZinUsm7W9BupafztVZV3ldkAxEHbpAeC0Pk= github.com/sigstore/sigstore/pkg/signature/kms/azure v1.8.8 h1:RKk4Z+qMaLORUdT7zntwMqKiYAej1VQlCswg0S7xNSY= diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index 3ab5d76026a..7988e35cda5 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -43,8 +43,15 @@ import ( "github.com/sigstore/cosign/v2/internal/pkg/cosign" "github.com/sigstore/cosign/v2/pkg/blob" cbundle "github.com/sigstore/cosign/v2/pkg/cosign/bundle" + + "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" + "github.com/sigstore/cosign/v2/pkg/oci/static" "github.com/sigstore/cosign/v2/pkg/types" + protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" + sgbundle "github.com/sigstore/sigstore-go/pkg/bundle" + "github.com/sigstore/sigstore-go/pkg/root" + "github.com/sigstore/sigstore-go/pkg/verify" "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" "github.com/google/go-containerregistry/pkg/name" @@ -156,6 +163,9 @@ type CheckOpts struct { // IgnoreTlog skip tlog verification IgnoreTlog bool + // UseSignedTimestamps use signed timestamps if available + UseSignedTimestamps bool + // The amount of maximum workers for parallel executions. // Defaults to 10. MaxWorkers int @@ -163,6 +173,103 @@ type CheckOpts struct { // Should the experimental OCI 1.1 behaviour be enabled or not. // Defaults to false. ExperimentalOCI11 bool + + NewBundleFormat bool + + // TrustedMaterial is the trusted material to use for verification. + // Currently, this is only applicable when NewBundleFormat is true. + TrustedMaterial root.TrustedMaterial + + // TODO: Add these to replace above fields? + // // VerifierOptions are the options to be passed to the verifier. + // VerifierOptions []verify.VerifierOption + + // // PolicyOptions are the policy options to be passed to the verifier. + // PolicyOptions []verify.PolicyOption +} + +type verifyTrustedMaterial struct { + root.TrustedMaterial + keyTrustedMaterial root.TrustedMaterial +} + +func (v *verifyTrustedMaterial) PublicKeyVerifier(hint string) (root.TimeConstrainedVerifier, error) { + return v.keyTrustedMaterial.PublicKeyVerifier(hint) +} + +// SigstoreGoOptions returns the verification options for verifying with sigstore-go. +func (co *CheckOpts) SigstoreGoOptions() (trustedMaterial root.TrustedMaterial, verifierOptions []verify.VerifierOption, policyOptions []verify.PolicyOption, err error) { + policyOptions = make([]verify.PolicyOption, 0) + + if len(co.Identities) > 0 { + var sanMatcher verify.SubjectAlternativeNameMatcher + var issuerMatcher verify.IssuerMatcher + if len(co.Identities) > 1 { + return nil, nil, nil, fmt.Errorf("unsupported: multiple identities are not supported at this time") + } + sanMatcher, err = verify.NewSANMatcher(co.Identities[0].Subject, co.Identities[0].SubjectRegExp) + if err != nil { + return nil, nil, nil, err + } + + issuerMatcher, err = verify.NewIssuerMatcher(co.Identities[0].Issuer, co.Identities[0].IssuerRegExp) + if err != nil { + return nil, nil, nil, err + } + + extensions := certificate.Extensions{ + GithubWorkflowTrigger: co.CertGithubWorkflowTrigger, + GithubWorkflowSHA: co.CertGithubWorkflowSha, + GithubWorkflowName: co.CertGithubWorkflowName, + GithubWorkflowRepository: co.CertGithubWorkflowRepository, + GithubWorkflowRef: co.CertGithubWorkflowRef, + } + + certificateIdentities, err := verify.NewCertificateIdentity(sanMatcher, issuerMatcher, extensions) + if err != nil { + return nil, nil, nil, err + } + policyOptions = []verify.PolicyOption{verify.WithCertificateIdentity(certificateIdentities)} + } + + // Wrap TrustedMaterial + vTrustedMaterial := &verifyTrustedMaterial{TrustedMaterial: co.TrustedMaterial} + + // If TrustedMaterial is not set, fetch it from TUF + if vTrustedMaterial.TrustedMaterial == nil { + vTrustedMaterial.TrustedMaterial, err = root.FetchTrustedRoot() + if err != nil { + return nil, nil, nil, err + } + } + + verifierOptions = make([]verify.VerifierOption, 0) + + if co.SigVerifier != nil { + // We are verifying with a public key + policyOptions = append(policyOptions, verify.WithKey()) + newExpiringKey := root.NewExpiringKey(co.SigVerifier, time.Time{}, time.Time{}) + vTrustedMaterial.keyTrustedMaterial = root.NewTrustedPublicKeyMaterial(func(_ string) (root.TimeConstrainedVerifier, error) { + return newExpiringKey, nil + }) + } else { //nolint:gocritic + // We are verifiying with a certificate + if !co.IgnoreSCT { + verifierOptions = append(verifierOptions, verify.WithSignedCertificateTimestamps(1)) + } + } + + if !co.IgnoreTlog { + verifierOptions = append(verifierOptions, verify.WithTransparencyLog(1), verify.WithIntegratedTimestamps(1)) + } + if co.UseSignedTimestamps { + verifierOptions = append(verifierOptions, verify.WithSignedTimestamps(1)) + } + if co.IgnoreTlog && !co.UseSignedTimestamps { + verifierOptions = append(verifierOptions, verify.WithoutAnyObserverTimestampsUnsafe()) + } + + return vTrustedMaterial, verifierOptions, policyOptions, nil } // This is a substitutable signature verification function that can be used for verifying @@ -495,6 +602,10 @@ func VerifyImageSignatures(ctx context.Context, signedImgRef name.Reference, co } } + if co.NewBundleFormat { + return nil, false, errors.New("bundle support for image signatures is not yet implemented") + } + // Enforce this up front. if co.RootCerts == nil && co.SigVerifier == nil { return nil, false, errors.New("one of verifier or root certs is required") @@ -880,8 +991,11 @@ func loadSignatureFromFile(ctx context.Context, sigRef string, signedImgRef name // If there were no valid attestations, we return an error. func VerifyImageAttestations(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (checkedAttestations []oci.Signature, bundleVerified bool, err error) { // Enforce this up front. - if co.RootCerts == nil && co.SigVerifier == nil { - return nil, false, errors.New("one of verifier or root certs is required") + if co.RootCerts == nil && co.SigVerifier == nil && co.TrustedMaterial == nil { + return nil, false, errors.New("one of verifier, root certs, or TrustedMaterial is required") + } + if co.NewBundleFormat { + return verifyImageAttestationsSigstoreBundle(ctx, signedImgRef, co) } // This is a carefully optimized sequence for fetching the attestations of @@ -1413,3 +1527,128 @@ func verifyImageSignaturesExperimentalOCI(ctx context.Context, signedImgRef name return verifySignatures(ctx, sigs, h, co) } + +func getBundles(_ context.Context, signedImgRef name.Reference, co *CheckOpts) ([]*sgbundle.Bundle, *v1.Hash, error) { + // This is a carefully optimized sequence for fetching the signatures of the + // entity that minimizes registry requests when supplied with a digest input + digest, err := ociremote.ResolveDigest(signedImgRef, co.RegistryClientOpts...) + if err != nil { + if terr := (&transport.Error{}); errors.As(err, &terr) && terr.StatusCode == http.StatusNotFound { + return nil, nil, &ErrImageTagNotFound{ + fmt.Errorf("image tag not found: %w", err), + } + } + return nil, nil, err + } + h, err := v1.NewHash(digest.Identifier()) + if err != nil { + return nil, nil, err + } + + index, err := ociremote.Referrers(digest, "", co.RegistryClientOpts...) + if err != nil { + return nil, nil, err + } + var bundles = make([]*sgbundle.Bundle, 0, len(index.Manifests)) + for _, result := range index.Manifests { + st, err := name.ParseReference(fmt.Sprintf("%s@%s", digest.Repository, result.Digest.String())) + if err != nil { + return nil, nil, err + } + bundle, err := ociremote.Bundle(st, co.RegistryClientOpts...) + if err != nil { + // There may be non-Sigstore referrers in the index, so we can ignore them. + // TODO: Should we surface any errors here (e.g. if the bundle is invalid)? + continue + } + bundles = append(bundles, bundle) + } + + return bundles, &h, nil +} + +// verifyImageAttestationsSigstoreBundle verifies attestations from attached sigstore bundles +func verifyImageAttestationsSigstoreBundle(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (checkedAttestations []oci.Signature, bundleVerified bool, err error) { + bundles, hash, err := getBundles(ctx, signedImgRef, co) + if err != nil { + return nil, false, err + } + + digestBytes, err := hex.DecodeString(hash.Hex) + if err != nil { + return nil, false, err + } + + artifactPolicyOption := verify.WithArtifactDigest(hash.Algorithm, digestBytes) + + attestations := make([]oci.Signature, len(bundles)) + bundlesVerified := make([]bool, len(bundles)) + + workers := co.MaxWorkers + if co.MaxWorkers == 0 { + workers = cosign.DefaultMaxWorkers + } + t := throttler.New(workers, len(bundles)) + for i, bundle := range bundles { + go func(bundle *sgbundle.Bundle, index int) { + var att oci.Signature + if err := func(bundle *sgbundle.Bundle) error { + _, err := VerifyNewBundle(ctx, co, artifactPolicyOption, bundle) + if err != nil { + return err + } + dsse, ok := bundle.Content.(*protobundle.Bundle_DsseEnvelope) + if !ok { + return errors.New("bundle does not contain a DSSE envelope") + } + payload, err := json.Marshal(dsse.DsseEnvelope) + if err != nil { + return fmt.Errorf("marshaling DSSE envelope: %w", err) + } + + // TODO: Add additional data to oci.Signature (Cert, Rekor Bundle, Timestamp, etc) + // This can be done by passing a list of static.Option to NewAttestation (e.g. static.WithCertChain()) + // This depends on https://github.com/sigstore/sigstore-go/issues/328 + att, err = static.NewAttestation(payload) + if err != nil { + return err + } + bundlesVerified[index] = true + + return err + }(bundle); err != nil { + t.Done(err) + return + } + + attestations[index] = att + t.Done(nil) + }(bundle, i) + + // wait till workers are available + t.Throttle() + } + + for _, a := range attestations { + if a != nil { + checkedAttestations = append(checkedAttestations, a) + } + } + + for _, verified := range bundlesVerified { + bundleVerified = bundleVerified || verified + } + + if len(checkedAttestations) == 0 { + var combinedErrors []string + for _, err := range t.Errs() { + combinedErrors = append(combinedErrors, err.Error()) + } + + return nil, false, &ErrNoMatchingAttestations{ + fmt.Errorf("no matching attestations: %s", strings.Join(combinedErrors, "\n ")), + } + } + + return checkedAttestations, bundleVerified, nil +} diff --git a/pkg/cosign/verify_bundle.go b/pkg/cosign/verify_bundle.go new file mode 100644 index 00000000000..0ffb82cea18 --- /dev/null +++ b/pkg/cosign/verify_bundle.go @@ -0,0 +1,35 @@ +// +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cosign + +import ( + "context" + + "github.com/sigstore/sigstore-go/pkg/verify" +) + +// VerifyNewBundle verifies a SigstoreBundle with the given parameters +func VerifyNewBundle(_ context.Context, co *CheckOpts, artifactPolicyOption verify.ArtifactPolicyOption, bundle verify.SignedEntity) (*verify.VerificationResult, error) { + trustedMaterial, verifierOptions, policyOptions, err := co.SigstoreGoOptions() + if err != nil { + return nil, err + } + verifier, err := verify.NewSignedEntityVerifier(trustedMaterial, verifierOptions...) + if err != nil { + return nil, err + } + return verifier.Verify(bundle, verify.NewPolicy(artifactPolicyOption, policyOptions...)) +} diff --git a/pkg/cosign/verify_bundle_test.go b/pkg/cosign/verify_bundle_test.go new file mode 100644 index 00000000000..3f631672bc0 --- /dev/null +++ b/pkg/cosign/verify_bundle_test.go @@ -0,0 +1,249 @@ +// Copyright 2023 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cosign_test + +import ( + "bytes" + "context" + "crypto/sha256" + "encoding/hex" + "fmt" + "testing" + + "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/sigstore-go/pkg/testing/ca" + "github.com/sigstore/sigstore-go/pkg/tlog" + "github.com/sigstore/sigstore-go/pkg/verify" + "github.com/stretchr/testify/assert" +) + +type bundleMutator struct { + verify.SignedEntity + + eraseTSA bool + eraseTlog bool +} + +func (b *bundleMutator) Timestamps() ([][]byte, error) { + if b.eraseTSA { + return [][]byte{}, nil + } + return b.SignedEntity.Timestamps() +} + +func (b *bundleMutator) TlogEntries() ([]*tlog.Entry, error) { + if b.eraseTlog { + return []*tlog.Entry{}, nil + } + return b.SignedEntity.TlogEntries() +} + +func TestVerifyBundleAttestation(t *testing.T) { + virtualSigstore, err := ca.NewVirtualSigstore() + assert.NoError(t, err) + virtualSigstore2, err := ca.NewVirtualSigstore() // for testing invalid trusted material + assert.NoError(t, err) + + artifact := []byte("artifact") + digest := sha256.Sum256(artifact) + digestHex := hex.EncodeToString(digest[:]) + statementFmt := `{"_type":"https://in-toto.io/Statement/v0.1","predicateType":"https://example.com/predicateType","subject":[{"name":"subject","digest":{"sha256":"%s"}}],"predicate":{}}` + statementCorrect := []byte(fmt.Sprintf(statementFmt, digestHex)) + + identity := "foo@example.com" + issuer := "example issuer" + standardIdentities := []cosign.Identity{ + { + Issuer: issuer, + Subject: identity, + }, + } + + entity, err := virtualSigstore.Attest(identity, issuer, statementCorrect) + if err != nil { + t.Fatal(err) + } + + for _, tc := range []struct { + name string + checkOpts *cosign.CheckOpts + artifactPolicyOption verify.ArtifactPolicyOption + entity verify.SignedEntity + wantErr bool + }{ + { + name: "valid", + checkOpts: &cosign.CheckOpts{ + Identities: standardIdentities, + IgnoreSCT: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: entity, + wantErr: false, + }, + { + name: "invalid, wrong artifact", + checkOpts: &cosign.CheckOpts{ + Identities: standardIdentities, + IgnoreSCT: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader([]byte("not the artifact"))), + entity: entity, + wantErr: true, + }, + { + name: "valid, pattern match issuer", + checkOpts: &cosign.CheckOpts{ + Identities: []cosign.Identity{ + { + IssuerRegExp: ".*issuer", + Subject: "foo@example.com", + }, + }, + IgnoreSCT: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: entity, + wantErr: false, + }, + { + name: "valid, pattern match subject", + checkOpts: &cosign.CheckOpts{ + Identities: []cosign.Identity{ + { + Issuer: "example issuer", + SubjectRegExp: ".*@example.com", + }, + }, + IgnoreSCT: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: entity, + wantErr: false, + }, + { + name: "invalid, pattern match issuer", + checkOpts: &cosign.CheckOpts{ + Identities: []cosign.Identity{ + { + IssuerRegExp: ".* not my issuer", + Subject: "foo@example.com", + }, + }, + IgnoreSCT: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: entity, + wantErr: true, + }, + { + name: "invalid, pattern match subject", + checkOpts: &cosign.CheckOpts{ + Identities: []cosign.Identity{ + { + Issuer: "example issuer", + SubjectRegExp: ".*@otherexample.com", + }, + }, + IgnoreSCT: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: entity, + wantErr: true, + }, + { + name: "invalid trusted material", + checkOpts: &cosign.CheckOpts{ + Identities: standardIdentities, + IgnoreSCT: true, + TrustedMaterial: virtualSigstore2, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: entity, + wantErr: true, + }, + { + name: "do not require tlog, missing tlog", + checkOpts: &cosign.CheckOpts{ + Identities: standardIdentities, + IgnoreSCT: true, + IgnoreTlog: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: &bundleMutator{SignedEntity: entity, eraseTlog: true}, + wantErr: false, + }, + { + name: "do not require tsa, missing tsa", + checkOpts: &cosign.CheckOpts{ + Identities: standardIdentities, + IgnoreSCT: true, + IgnoreTlog: false, + UseSignedTimestamps: false, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: &bundleMutator{SignedEntity: entity, eraseTSA: true}, + wantErr: false, + }, + { + name: "require tlog, missing tlog", + checkOpts: &cosign.CheckOpts{ + Identities: standardIdentities, + IgnoreSCT: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: &bundleMutator{SignedEntity: entity, eraseTlog: true}, + wantErr: true, + }, + { + name: "require tsa, missing tsa", + checkOpts: &cosign.CheckOpts{ + Identities: standardIdentities, + IgnoreSCT: true, + UseSignedTimestamps: true, + TrustedMaterial: virtualSigstore, + }, + artifactPolicyOption: verify.WithArtifact(bytes.NewReader(artifact)), + entity: &bundleMutator{SignedEntity: entity, eraseTSA: true}, + wantErr: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + _, err = cosign.VerifyNewBundle(context.Background(), tc.checkOpts, tc.artifactPolicyOption, tc.entity) + if tc.wantErr { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + }) + } +} diff --git a/pkg/oci/remote/referrers.go b/pkg/oci/remote/referrers.go index 24cb802eb40..1b67cdf392a 100644 --- a/pkg/oci/remote/referrers.go +++ b/pkg/oci/remote/referrers.go @@ -25,7 +25,9 @@ import ( func Referrers(d name.Digest, artifactType string, opts ...Option) (*v1.IndexManifest, error) { o := makeOptions(name.Repository{}, opts...) rOpt := o.ROpt - rOpt = append(rOpt, remote.WithFilter("artifactType", artifactType)) + if artifactType != "" { + rOpt = append(rOpt, remote.WithFilter("artifactType", artifactType)) + } idx, err := remote.Referrers(d, rOpt...) if err != nil { return nil, err diff --git a/pkg/oci/remote/signatures.go b/pkg/oci/remote/signatures.go index 825d80727f9..be29c5beab4 100644 --- a/pkg/oci/remote/signatures.go +++ b/pkg/oci/remote/signatures.go @@ -17,7 +17,9 @@ package remote import ( "errors" + "io" "net/http" + "strings" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -26,6 +28,7 @@ import ( "github.com/sigstore/cosign/v2/pkg/oci" "github.com/sigstore/cosign/v2/pkg/oci/empty" "github.com/sigstore/cosign/v2/pkg/oci/internal/signature" + sgbundle "github.com/sigstore/sigstore-go/pkg/bundle" ) const maxLayers = 1000 @@ -49,6 +52,42 @@ func Signatures(ref name.Reference, opts ...Option) (oci.Signatures, error) { }, nil } +func Bundle(ref name.Reference, opts ...Option) (*sgbundle.Bundle, error) { + o := makeOptions(ref.Context(), opts...) + img, err := remoteImage(ref, o.ROpt...) + if err != nil { + return nil, err + } + // TODO: We can check for a specific predicate type here using + // img.Manifest() and looking at the annotations. To do so, we would need + // to thread the CLI flag through to here. + layers, err := img.Layers() + if err != nil { + return nil, err + } + if len(layers) != 1 { + return nil, errors.New("expected exactly one layer") + } + mediaType, err := layers[0].MediaType() + if err != nil { + return nil, err + } + if !strings.HasPrefix(string(mediaType), "application/vnd.dev.sigstore.bundle") { + return nil, errors.New("expected bundle layer") + } + layer0, err := layers[0].Uncompressed() + if err != nil { + return nil, err + } + bundleBytes, err := io.ReadAll(layer0) + if err != nil { + return nil, err + } + b := &sgbundle.Bundle{} + err = b.UnmarshalJSON(bundleBytes) + return b, err +} + type sigs struct { v1.Image } diff --git a/pkg/oci/remote/write.go b/pkg/oci/remote/write.go index d0614768e89..7ab96ed0408 100644 --- a/pkg/oci/remote/write.go +++ b/pkg/oci/remote/write.go @@ -20,6 +20,7 @@ import ( "encoding/json" "fmt" "os" + "time" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -29,6 +30,7 @@ import ( ociexperimental "github.com/sigstore/cosign/v2/internal/pkg/oci/remote" "github.com/sigstore/cosign/v2/pkg/oci" ctypes "github.com/sigstore/cosign/v2/pkg/types" + sgbundle "github.com/sigstore/sigstore-go/pkg/bundle" ) // WriteSignedImageIndexImages writes the images within the image index @@ -221,3 +223,126 @@ func (taggable taggableManifest) RawManifest() ([]byte, error) { func (taggable taggableManifest) MediaType() (types.MediaType, error) { return taggable.mediaType, nil } + +func WriteAttestationNewBundleFormat(d name.Repository, bundleBytes []byte, predicateType string, opts ...Option) error { + o := makeOptions(d, opts...) + + signTarget := d.String() + ref, err := name.ParseReference(signTarget, o.NameOpts...) + if err != nil { + return err + } + desc, err := remote.Head(ref, o.ROpt...) + if err != nil { + return err + } + + // Write the empty config layer + configLayer := static.NewLayer([]byte("{}"), "application/vnd.oci.image.config.v1+json") + configDigest, err := configLayer.Digest() + if err != nil { + return fmt.Errorf("failed to calculate digest: %w", err) + } + configSize, err := configLayer.Size() + if err != nil { + return fmt.Errorf("failed to calculate size: %w", err) + } + err = remote.WriteLayer(d, configLayer, o.ROpt...) + if err != nil { + return fmt.Errorf("failed to upload layer: %w", err) + } + + // generate bundle media type string + bundleMediaType, err := sgbundle.MediaTypeString("0.3") + if err != nil { + return fmt.Errorf("failed to generate bundle media type string: %w", err) + } + + // Write the bundle layer + layer := static.NewLayer(bundleBytes, types.MediaType(bundleMediaType)) + blobDigest, err := layer.Digest() + if err != nil { + return fmt.Errorf("failed to calculate digest: %w", err) + } + + blobSize, err := layer.Size() + if err != nil { + return fmt.Errorf("failed to calculate size: %w", err) + } + + err = remote.WriteLayer(d, layer, o.ROpt...) + if err != nil { + return fmt.Errorf("failed to upload layer: %w", err) + } + + // Create a manifest that includes the blob as a layer + manifest := referrerManifest{v1.Manifest{ + SchemaVersion: 2, + MediaType: types.OCIManifestSchema1, + Config: v1.Descriptor{ + MediaType: types.MediaType("application/vnd.oci.empty.v1+json"), + ArtifactType: bundleMediaType, + Digest: configDigest, + Size: configSize, + }, + Layers: []v1.Descriptor{ + { + MediaType: types.MediaType(bundleMediaType), + Digest: blobDigest, + Size: blobSize, + }, + }, + Subject: &v1.Descriptor{ + MediaType: types.OCIManifestSchema1, + Digest: desc.Digest, + Size: desc.Size, + }, + Annotations: map[string]string{ + "org.opencontainers.image.created": time.Now().UTC().Format(time.RFC3339), + "dev.sigstore.bundle.content": "dsse-envelope", + "dev.sigstore.bundle.predicateType": predicateType, + }, + }, bundleMediaType} + + targetRef, err := manifest.targetRef(d) + if err != nil { + return fmt.Errorf("failed to create target reference: %w", err) + } + + if err := remote.Put(targetRef, manifest, o.ROpt...); err != nil { + return fmt.Errorf("failed to upload manifest: %w", err) + } + + return nil +} + +// referrerManifest implements Taggable for use in remote.Put. +// This type also augments the built-in v1.Manifest with an ArtifactType field +// which is part of the OCI 1.1 Image Manifest spec but is unsupported by +// go-containerregistry at this time. +// See https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#image-manifest-property-descriptions +// and https://github.com/google/go-containerregistry/pull/1931 +type referrerManifest struct { + v1.Manifest + ArtifactType string `json:"artifactType,omitempty"` +} + +func (r referrerManifest) RawManifest() ([]byte, error) { + return json.Marshal(r) +} + +func (r referrerManifest) targetRef(repo name.Repository) (name.Reference, error) { + manifestBytes, err := r.RawManifest() + if err != nil { + return nil, err + } + digest, _, err := v1.SHA256(bytes.NewReader(manifestBytes)) + if err != nil { + return nil, err + } + return name.ParseReference(fmt.Sprintf("%s/%s@%s", repo.RegistryStr(), repo.RepositoryStr(), digest.String())) +} + +func (r referrerManifest) MediaType() (types.MediaType, error) { + return types.OCIManifestSchema1, nil +} diff --git a/test/e2e_test.go b/test/e2e_test.go index 598d7faa5c0..7ec814da58b 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -277,12 +277,15 @@ func TestImportSignVerifyClean(t *testing.T) { } func TestAttestVerify(t *testing.T) { - attestVerify(t, - "slsaprovenance", - `{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }`, - `predicate: builder: id: "2"`, - `predicate: builder: id: "1"`, - ) + for _, newBundleFormat := range []bool{false, true} { + attestVerify(t, + newBundleFormat, + "slsaprovenance", + `{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }`, + `predicate: builder: id: "2"`, + `predicate: builder: id: "1"`, + ) + } } func TestAttestVerifySPDXJSON(t *testing.T) { @@ -290,12 +293,15 @@ func TestAttestVerifySPDXJSON(t *testing.T) { if err != nil { t.Fatal(err) } - attestVerify(t, - "spdxjson", - string(attestationBytes), - `predicate: spdxVersion: "SPDX-2.2"`, - `predicate: spdxVersion: "SPDX-9.9"`, - ) + for _, newBundleFormat := range []bool{false, true} { + attestVerify(t, + newBundleFormat, + "spdxjson", + string(attestationBytes), + `predicate: spdxVersion: "SPDX-2.2"`, + `predicate: spdxVersion: "SPDX-9.9"`, + ) + } } func TestAttestVerifyCycloneDXJSON(t *testing.T) { @@ -303,12 +309,15 @@ func TestAttestVerifyCycloneDXJSON(t *testing.T) { if err != nil { t.Fatal(err) } - attestVerify(t, - "cyclonedx", - string(attestationBytes), - `predicate: specVersion: "1.4"`, - `predicate: specVersion: "7.7"`, - ) + for _, newBundleFormat := range []bool{false, true} { + attestVerify(t, + newBundleFormat, + "cyclonedx", + string(attestationBytes), + `predicate: specVersion: "1.4"`, + `predicate: specVersion: "7.7"`, + ) + } } func TestAttestVerifyURI(t *testing.T) { @@ -316,15 +325,18 @@ func TestAttestVerifyURI(t *testing.T) { if err != nil { t.Fatal(err) } - attestVerify(t, - "https://example.com/TestResult/v1", - string(attestationBytes), - `predicate: passed: true`, - `predicate: passed: false"`, - ) + for _, newBundleFormat := range []bool{false, true} { + attestVerify(t, + newBundleFormat, + "https://example.com/TestResult/v1", + string(attestationBytes), + `predicate: passed: true`, + `predicate: passed: false"`, + ) + } } -func attestVerify(t *testing.T, predicateType, attestation, goodCue, badCue string) { +func attestVerify(t *testing.T, newBundleFormat bool, predicateType, attestation, goodCue, badCue string) { repo, stop := reg(t) defer stop() td := t.TempDir() @@ -353,6 +365,10 @@ func attestVerify(t *testing.T, predicateType, attestation, goodCue, badCue stri MaxWorkers: 10, } + if newBundleFormat { + verifyAttestation.NewBundleFormat = true + } + // Fail case when using without type and policy flag mustErr(verifyAttestation.Exec(ctx, []string{imgName}), t) @@ -361,7 +377,7 @@ func attestVerify(t *testing.T, predicateType, attestation, goodCue, badCue stri } // Now attest the image - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc, NewBundleFormat: newBundleFormat} attestCmd := attest.AttestCommand{ KeyOpts: ko, PredicatePath: attestationPath,