diff --git a/.github/workflows/e2e-tests.yml b/.github/workflows/e2e-tests.yml index 50bb2af40f7..ae78def2809 100644 --- a/.github/workflows/e2e-tests.yml +++ b/.github/workflows/e2e-tests.yml @@ -98,6 +98,7 @@ jobs: VAULT_ADDR: "http://localhost:8200" COSIGN_YES: "true" SCAFFOLDING_RELEASE_VERSION: "v0.7.24" + TUF_ROOT_JSON: ${{ github.workspace }}/root.json steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 diff --git a/cmd/cosign/cli/attest.go b/cmd/cosign/cli/attest.go index e3c6c34b7ac..3cfa99206a8 100644 --- a/cmd/cosign/cli/attest.go +++ b/cmd/cosign/cli/attest.go @@ -31,7 +31,7 @@ func Attest() *cobra.Command { cmd := &cobra.Command{ Use: "attest", Short: "Attest the supplied container image", - Example: ` cosign attest --key | [--predicate ] [--a key=value] [--no-upload=true|false] [--record-creation-timestamp=true|false] [--f] [--r] + Example: ` cosign attest --key | [--predicate ] [--no-upload=true|false] [--yes] # attach an attestation to a container image Google sign-in cosign attest --timeout 90s --predicate --type @@ -61,16 +61,24 @@ func Attest() *cobra.Command { echo | cosign attest --predicate - # write attestation to stdout - cosign attest --predicate --type --key cosign.key --no-upload true - - # attach an attestation to a container image and honor the creation timestamp of the signature - cosign attest --predicate --type --key cosign.key --record-creation-timestamp `, + cosign attest --predicate --type --key cosign.key --no-upload true `, Args: cobra.MinimumNArgs(1), PersistentPreRun: options.BindViper, PreRunE: func(_ *cobra.Command, _ []string) error { - if o.NewBundleFormat && o.NoUpload && o.BundlePath == "" { - return fmt.Errorf("must enable upload to the OCI registry or specify a local --bundle path with --new-bundle-format") + if o.NoUpload && o.BundlePath == "" { + return fmt.Errorf("must enable upload to the OCI registry or specify a local --bundle path") + } + var attestType string + if o.Key == "" && !o.SecurityKey.Use { + attestType = "keyless" + } else if o.IssueCertificate { + attestType = "certificate-based" + } + if attestType != "" { + if !o.UseSigningConfig && o.SigningConfigPath == "" { + return fmt.Errorf("%s attesting requires a signing config (either from TUF via --use-signing-config or explicitly via a file with --signing-config)", attestType) + } } return nil }, @@ -85,46 +93,35 @@ func Attest() *cobra.Command { PassFunc: generate.GetPass, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, - FulcioURL: o.Fulcio.URL, IDToken: o.Fulcio.IdentityToken, FulcioAuthFlow: o.Fulcio.AuthFlow, - InsecureSkipFulcioVerify: o.Fulcio.InsecureSkipFulcioVerify, - RekorURL: o.Rekor.URL, - OIDCIssuer: o.OIDC.Issuer, OIDCClientID: o.OIDC.ClientID, OIDCClientSecret: oidcClientSecret, OIDCRedirectURL: o.OIDC.RedirectURL, + OIDCDisableProviders: o.OIDC.DisableAmbientProviders, OIDCProvider: o.OIDC.Provider, SkipConfirmation: o.SkipConfirmation, TSAClientCACert: o.TSAClientCACert, TSAClientKey: o.TSAClientKey, TSAClientCert: o.TSAClientCert, TSAServerName: o.TSAServerName, - TSAServerURL: o.TSAServerURL, IssueCertificateForExistingKey: o.IssueCertificate, BundlePath: o.BundlePath, - NewBundleFormat: o.NewBundleFormat, } if err := signcommon.LoadTrustedMaterialAndSigningConfig(cmd.Context(), &ko, o.UseSigningConfig, o.SigningConfigPath, - o.Rekor.URL, o.Fulcio.URL, o.OIDC.Issuer, o.TSAServerURL, o.TrustedRootPath, o.TlogUpload, - o.NewBundleFormat, "", o.Key, o.IssueCertificate, - "", "", "", "", "", ""); err != nil { + o.TrustedRootPath, o.Key); err != nil { return err } attestCommand := attest.AttestCommand{ - KeyOpts: ko, - RegistryOptions: o.Registry, - CertPath: o.Cert, - CertChainPath: o.CertChain, - NoUpload: o.NoUpload, - PredicatePath: o.Predicate.Path, - PredicateType: o.Predicate.Type, - Replace: o.Replace, - Timeout: ro.Timeout, - TlogUpload: o.TlogUpload, - RekorEntryType: o.RekorEntryType, - RecordCreationTimestamp: o.RecordCreationTimestamp, + KeyOpts: ko, + RegistryOptions: o.Registry, + CertPath: o.Cert, + CertChainPath: o.CertChain, + NoUpload: o.NoUpload, + PredicatePath: o.Predicate.Path, + PredicateType: o.Predicate.Type, + Timeout: ro.Timeout, } for _, img := range args { diff --git a/cmd/cosign/cli/attest/attest.go b/cmd/cosign/cli/attest/attest.go index 31e1c771c00..435bd5b7baa 100644 --- a/cmd/cosign/cli/attest/attest.go +++ b/cmd/cosign/cli/attest/attest.go @@ -24,37 +24,24 @@ import ( "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" - "google.golang.org/protobuf/encoding/protojson" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign/attestation" - cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" - cremote "github.com/sigstore/cosign/v3/pkg/cosign/remote" - "github.com/sigstore/cosign/v3/pkg/oci/mutate" ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" - "github.com/sigstore/cosign/v3/pkg/oci/static" - "github.com/sigstore/cosign/v3/pkg/types" - protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" - "github.com/sigstore/sigstore/pkg/signature" ) // nolint type AttestCommand struct { options.KeyOpts options.RegistryOptions - CertPath string - CertChainPath string - NoUpload bool - PredicatePath string - PredicateType string - Replace bool - Timeout time.Duration - TlogUpload bool - TSAServerURL string - RekorEntryType string - RecordCreationTimestamp bool + CertPath string + CertChainPath string + NoUpload bool + PredicatePath string + PredicateType string + Timeout time.Duration } // nolint @@ -68,10 +55,6 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { return fmt.Errorf("predicate cannot be empty") } - if c.RekorEntryType != "dsse" && c.RekorEntryType != "intoto" { - return fmt.Errorf("unknown value for rekor-entry-type") - } - predicateURI, err := options.ParsePredicateType(c.PredicateType) if err != nil { return err @@ -135,115 +118,34 @@ func (c *AttestCommand) Exec(ctx context.Context, imageRef string) error { } if c.SigningConfig == nil { - c.SigningConfig, err = signcommon.NewSigningConfigFromKeyOpts(c.KeyOpts, c.TlogUpload) - if err != nil { - return fmt.Errorf("creating signing config: %w", err) - } + c.SigningConfig = signcommon.NewEmptySigningConfig() } - bundleBytes, pubKey, hashAlgProto, err := signcommon.NewAttestationBundle(ctx, c.KeyOpts, c.CertPath, c.CertChainPath, bundleOpts, c.SigningConfig, c.TrustedMaterial) + shouldUpload, err := signcommon.ShouldUploadToTlog(ctx, c.KeyOpts, digest, len(c.SigningConfig.RekorLogURLs()) > 0) if err != nil { - return fmt.Errorf("creating bundle: %w", err) - } - - if c.NewBundleFormat { - if c.BundlePath != "" { - if err := os.WriteFile(c.BundlePath, bundleBytes, 0600); err != nil { - return fmt.Errorf("create bundle file: %w", err) - } - ui.Infof(ctx, "Wrote bundle to file %s", c.BundlePath) - } - - if !c.NoUpload { - if err := ociremote.WriteAttestationNewBundleFormat(digest, bundleBytes, bundleOpts.PredicateType, ociremoteOpts...); err != nil { - return fmt.Errorf("writing bundle: %w", err) - } - } - return nil + return fmt.Errorf("should upload to tlog: %w", err) } - var pb protobundle.Bundle - if err := protojson.Unmarshal(bundleBytes, &pb); err != nil { - return fmt.Errorf("unmarshalling bundle: %w", err) + if !shouldUpload { + c.SigningConfig.WithRekorLogURLs() } - bundleComponents, err := signcommon.ExtractComponentsFromProtoBundle(&pb) + bundleBytes, err := signcommon.NewAttestationBundle(ctx, c.KeyOpts, c.CertPath, c.CertChainPath, bundleOpts, c.SigningConfig, c.TrustedMaterial) if err != nil { - return fmt.Errorf("extracting components from bundle: %w", err) - } - - legacyBundleBytes, err := signcommon.NewLegacyBundleFromProtoBundleComponents(bundleComponents) - if err != nil { - return fmt.Errorf("creating legacy bundle: %w", err) + return fmt.Errorf("creating bundle: %w", err) } if c.BundlePath != "" { - if err := os.WriteFile(c.BundlePath, legacyBundleBytes, 0600); err != nil { + if err := os.WriteFile(c.BundlePath, bundleBytes, 0600); err != nil { return fmt.Errorf("create bundle file: %w", err) } ui.Infof(ctx, "Wrote bundle to file %s", c.BundlePath) } - if c.NoUpload { - return nil - } - - certPem, chainPem := signcommon.EncodeCertificatesToPEM(bundleComponents.Certificates) - - opts := []static.Option{ - static.WithLayerMediaType(types.DssePayloadType), - static.WithAnnotations(map[string]string{ - "predicateType": predicateURI, - }), - } - if certPem != nil { - opts = append(opts, static.WithCertChain(certPem, chainPem)) - } - - if len(bundleComponents.RFC3161Timestamps) > 0 { - opts = append(opts, static.WithRFC3161Timestamp(cbundle.TimestampToRFC3161Timestamp(bundleComponents.RFC3161Timestamps[0].GetSignedTimestamp()))) - } - - predicateTypeAnnotation := map[string]string{ - "predicateType": predicateURI, - } - // Add predicateType as manifest annotation - opts = append(opts, static.WithAnnotations(predicateTypeAnnotation)) - - if len(bundleComponents.RekorEntries) > 0 { - opts = append(opts, static.WithBundle(signcommon.RekorBundleFromProtoTlogEntry(bundleComponents.RekorEntries[0]))) - } - - ociSig, err := static.NewAttestation(bundleComponents.Signature, opts...) - if err != nil { - return fmt.Errorf("creating attestation: %w", err) - } - - // 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...) - - ddVerifier, err := signature.LoadVerifier(pubKey, signcommon.ProtoHashAlgoToHash(hashAlgProto)) - if err != nil { - return fmt.Errorf("loading verifier: %w", err) - } - dd := cremote.NewDupeDetector(ddVerifier) - signOpts := []mutate.SignOption{ - mutate.WithDupeDetector(dd), - mutate.WithRecordCreationTimestamp(c.RecordCreationTimestamp), - } - - if c.Replace { - ro := cremote.NewReplaceOp(predicateURI) - signOpts = append(signOpts, mutate.WithReplaceOp(ro)) - } - - // Attach the attestation to the entity. - newSE, err := mutate.AttachAttestationToEntity(se, ociSig, signOpts...) - if err != nil { - return fmt.Errorf("attaching attestation: %w", err) + if !c.NoUpload { + if err := ociremote.WriteAttestationNewBundleFormat(digest, bundleBytes, bundleOpts.PredicateType, ociremoteOpts...); err != nil { + return fmt.Errorf("writing bundle: %w", err) + } } - - // Publish the attestations associated with this entity - return ociremote.WriteAttestations(digest.Repository, newSE, ociremoteOpts...) + return nil } diff --git a/cmd/cosign/cli/attest/attest_blob.go b/cmd/cosign/cli/attest/attest_blob.go index 3b9c29930de..c0956ebaa13 100644 --- a/cmd/cosign/cli/attest/attest_blob.go +++ b/cmd/cosign/cli/attest/attest_blob.go @@ -33,10 +33,7 @@ import ( "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign/attestation" - cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" - protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" "github.com/sigstore/sigstore/pkg/signature" - "google.golang.org/protobuf/encoding/protojson" ) // nolint @@ -51,14 +48,7 @@ type AttestBlobCommand struct { PredicatePath string PredicateType string - TlogUpload bool - Timeout time.Duration - - OutputSignature string - OutputAttestation string - OutputCertificate string - - RekorEntryType string + Timeout time.Duration } // nolint @@ -72,10 +62,6 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error return fmt.Errorf("one of --predicate or --statement must be set") } - if c.RekorEntryType != "dsse" && c.RekorEntryType != "intoto" { - return fmt.Errorf("unknown value for rekor-entry-type") - } - if c.Timeout != 0 { var cancelFn context.CancelFunc ctx, cancelFn = context.WithTimeout(ctx, c.Timeout) @@ -147,89 +133,22 @@ func (c *AttestBlobCommand) Exec(ctx context.Context, artifactPath string) error } if c.SigningConfig == nil { - var err error - c.SigningConfig, err = signcommon.NewSigningConfigFromKeyOpts(c.KeyOpts, c.TlogUpload) - if err != nil { - return fmt.Errorf("creating signing config: %w", err) - } + c.SigningConfig = signcommon.NewEmptySigningConfig() } - bundleBytes, _, _, err := signcommon.NewAttestationBundle(ctx, c.KeyOpts, c.CertPath, c.CertChainPath, bundleOpts, c.SigningConfig, c.TrustedMaterial) + _, err = signcommon.ShouldUploadToTlog(ctx, c.KeyOpts, nil, len(c.SigningConfig.RekorLogURLs()) > 0) if err != nil { - return fmt.Errorf("creating bundle: %w", err) - } - - if c.NewBundleFormat { - if err := os.WriteFile(c.BundlePath, bundleBytes, 0600); err != nil { - return fmt.Errorf("create bundle file: %w", err) - } - ui.Infof(ctx, "Wrote bundle to file %s", c.BundlePath) - return nil - } - - var pb protobundle.Bundle - if err := protojson.Unmarshal(bundleBytes, &pb); err != nil { - return fmt.Errorf("unmarshalling bundle: %w", err) + return fmt.Errorf("should upload to tlog: %w", err) } - bundleComponents, err := signcommon.ExtractComponentsFromProtoBundle(&pb) + bundleBytes, err := signcommon.NewAttestationBundle(ctx, c.KeyOpts, c.CertPath, c.CertChainPath, bundleOpts, c.SigningConfig, c.TrustedMaterial) if err != nil { - return err - } - - if c.BundlePath != "" { - contents, err := signcommon.NewLegacyBundleFromProtoBundleComponents(bundleComponents) - if err != nil { - return fmt.Errorf("creating legacy bundle: %w", err) - } - - if err := os.WriteFile(c.BundlePath, contents, 0600); err != nil { - return fmt.Errorf("create bundle file: %w", err) - } - ui.Infof(ctx, "Wrote bundle to file %s", c.BundlePath) - } - - if c.OutputSignature != "" { - if err := os.WriteFile(c.OutputSignature, bundleComponents.Signature, 0600); err != nil { - return fmt.Errorf("create signature file: %w", err) - } - fmt.Fprintf(os.Stderr, "Signature written in %s\n", c.OutputSignature) - } else { - fmt.Fprintln(os.Stdout, string(bundleComponents.Signature)) - } - - if c.OutputAttestation != "" { - if err := os.WriteFile(c.OutputAttestation, payload, 0600); err != nil { - return fmt.Errorf("create attestation file: %w", err) - } - fmt.Fprintf(os.Stderr, "Attestation written in %s\n", c.OutputAttestation) - } - - if c.OutputCertificate != "" { - if len(bundleComponents.Certificates) == 0 { - return fmt.Errorf("no certificate found in bundle") - } - certPem, _ := signcommon.EncodeCertificatesToPEM(bundleComponents.Certificates) - if err := os.WriteFile(c.OutputCertificate, certPem, 0600); err != nil { - return fmt.Errorf("create certificate file: %w", err) - } - fmt.Fprintln(os.Stderr, "Certificate written to file ", c.OutputCertificate) + return fmt.Errorf("creating bundle: %w", err) } - - if c.RFC3161TimestampPath != "" { - if len(bundleComponents.RFC3161Timestamps) == 0 { - return fmt.Errorf("no RFC3161 timestamp found in bundle") - } - legacyTimestamp := cbundle.TimestampToRFC3161Timestamp(bundleComponents.RFC3161Timestamps[0].GetSignedTimestamp()) - ts, err := json.Marshal(legacyTimestamp) - if err != nil { - return fmt.Errorf("marshalling timestamp: %w", err) - } - if err := os.WriteFile(c.RFC3161TimestampPath, ts, 0600); err != nil { - return fmt.Errorf("create timestamp file: %w", err) - } - fmt.Fprintln(os.Stderr, "Timestamp wrote in the file ", c.RFC3161TimestampPath) + if err := os.WriteFile(c.BundlePath, bundleBytes, 0600); err != nil { + return fmt.Errorf("create bundle file: %w", err) } + ui.Infof(ctx, "Wrote bundle to file %s", c.BundlePath) return nil } diff --git a/cmd/cosign/cli/attest/attest_blob_test.go b/cmd/cosign/cli/attest/attest_blob_test.go index 5af54bfc445..ed0c6f2ec5c 100644 --- a/cmd/cosign/cli/attest/attest_blob_test.go +++ b/cmd/cosign/cli/attest/attest_blob_test.go @@ -19,9 +19,7 @@ import ( "context" "crypto" "crypto/x509" - "encoding/base64" "encoding/hex" - "encoding/json" "encoding/pem" "os" "path/filepath" @@ -30,6 +28,9 @@ import ( "errors" + "encoding/base64" + "encoding/json" + ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/secure-systems-lab/go-securesystemslib/encrypted" "github.com/sigstore/cosign/v3/cmd/cosign/cli/generate" @@ -148,12 +149,6 @@ func TestAttestBlobCmdLocalKeyAndCert(t *testing.T) { certref: subCertPem, errString: "public key in certificate does not match the provided public key", }, - { - name: "cert chain matches key", - keyref: keyRef, - certref: certRef, - certchainref: subCertPem, - }, { name: "cert chain partial", keyref: keyRef, @@ -175,18 +170,13 @@ func TestAttestBlobCmdLocalKeyAndCert(t *testing.T) { }, } { t.Run(tc.name, func(t *testing.T) { - keyOpts := options.KeyOpts{KeyRef: tc.keyref} - if tc.newBundle { - keyOpts.NewBundleFormat = true - keyOpts.BundlePath = filepath.Join(td, "output.bundle") - } + keyOpts := options.KeyOpts{KeyRef: tc.keyref, BundlePath: filepath.Join(td, "output.bundle")} at := AttestBlobCommand{ - KeyOpts: keyOpts, - CertPath: tc.certref, - CertChainPath: tc.certchainref, - PredicatePath: predicatePath, - PredicateType: predicateType, - RekorEntryType: "dsse", + KeyOpts: keyOpts, + CertPath: tc.certref, + CertChainPath: tc.certchainref, + PredicatePath: predicatePath, + PredicateType: predicateType, } err := at.Exec(ctx, blob) if err != nil { @@ -228,25 +218,30 @@ func TestAttestBlob(t *testing.T) { for predicateType, predicatePath := range predicates { t.Run(predicateType, func(t *testing.T) { - dssePath := filepath.Join(td, "dsse.intoto.jsonl") + bundlePath := filepath.Join(td, "bundle.json") + keyOpts := options.KeyOpts{KeyRef: keyRef, BundlePath: bundlePath} at := AttestBlobCommand{ - KeyOpts: options.KeyOpts{KeyRef: keyRef}, - PredicatePath: predicatePath, - PredicateType: predicateType, - OutputSignature: dssePath, - RekorEntryType: "dsse", + KeyOpts: keyOpts, + PredicatePath: predicatePath, + PredicateType: predicateType, } err := at.Exec(ctx, blobPath) if err != nil { t.Fatal(err) } - // Load the attestation. - dsseBytes, _ := os.ReadFile(dssePath) - env := &ssldsse.Envelope{} - if err := json.Unmarshal(dsseBytes, env); err != nil { + // Load the attestation bundle. + bundleBytes, _ := os.ReadFile(bundlePath) + var bundleJSON struct { + DsseEnvelope *ssldsse.Envelope `json:"dsseEnvelope"` + } + if err := json.Unmarshal(bundleBytes, &bundleJSON); err != nil { t.Fatal(err) } + env := bundleJSON.DsseEnvelope + if env == nil { + t.Fatal("expected dsse envelope in bundle") + } if len(env.Signatures) != 1 { t.Fatalf("expected 1 signature, got %d", len(env.Signatures)) @@ -284,38 +279,6 @@ func TestAttestBlob(t *testing.T) { } } -func TestBadRekorEntryType(t *testing.T) { - ctx := context.Background() - td := t.TempDir() - - keys, _ := cosign.GenerateKeyPair(nil) - keyRef := writeFile(t, td, string(keys.PrivateBytes), "key.pem") - - blob := []byte("foo") - blobPath := writeFile(t, td, string(blob), "foo.txt") - - predicates := map[string]string{} - predicates["slsaprovenance"] = makeSLSA02PredicateFile(t, td) - predicates["slsaprovenance1"] = makeSLSA1PredicateFile(t, td) - - for predicateType, predicatePath := range predicates { - t.Run(predicateType, func(t *testing.T) { - dssePath := filepath.Join(td, "dsse.intoto.jsonl") - at := AttestBlobCommand{ - KeyOpts: options.KeyOpts{KeyRef: keyRef}, - PredicatePath: predicatePath, - PredicateType: predicateType, - OutputSignature: dssePath, - RekorEntryType: "badvalue", - } - err := at.Exec(ctx, blobPath) - if err == nil || err.Error() != "unknown value for rekor-entry-type" { - t.Fatal("expected an error due to unknown rekor entry type") - } - }) - } -} - func TestStatementPath(t *testing.T) { ctx := context.Background() td := t.TempDir() @@ -340,10 +303,10 @@ func TestStatementPath(t *testing.T) { }` statementPath := writeFile(t, td, statement, "statement.json") + keyOpts := options.KeyOpts{KeyRef: keyRef, BundlePath: filepath.Join(td, "bundle.json")} at := AttestBlobCommand{ - KeyOpts: options.KeyOpts{KeyRef: keyRef}, - StatementPath: statementPath, - RekorEntryType: "dsse", + KeyOpts: keyOpts, + StatementPath: statementPath, } err := at.Exec(ctx, "") assert.NoError(t, err) diff --git a/cmd/cosign/cli/attest_blob.go b/cmd/cosign/cli/attest_blob.go index 9aaa604ab80..b0baf13a09c 100644 --- a/cmd/cosign/cli/attest_blob.go +++ b/cmd/cosign/cli/attest_blob.go @@ -30,30 +30,41 @@ func AttestBlob() *cobra.Command { cmd := &cobra.Command{ Use: "attest-blob", Short: "Attest the supplied blob", - Example: ` cosign attest-blob --key | [--predicate ] [--a key=value] [--f] [--r] + Example: ` cosign attest-blob --key | [--predicate ] [--yes] --bundle # attach an attestation to a blob with a local key pair file and write the bundle to a file cosign attest-blob --predicate --type --key cosign.key --bundle # attach an attestation to a blob with a key pair stored in Azure Key Vault - cosign attest-blob --predicate --type --key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] + cosign attest-blob --predicate --type --key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] --bundle # attach an attestation to a blob with a key pair stored in AWS KMS - cosign attest-blob --predicate --type --key awskms://[ENDPOINT]/[ID/ALIAS/ARN] + cosign attest-blob --predicate --type --key awskms://[ENDPOINT]/[ID/ALIAS/ARN] --bundle # attach an attestation to a blob with a key pair stored in Google Cloud KMS - cosign attest-blob --predicate --type --key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY]/versions/[VERSION] + cosign attest-blob --predicate --type --key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY]/versions/[VERSION] --bundle # attach an attestation to a blob with a key pair stored in Hashicorp Vault - cosign attest-blob --predicate --type --key hashivault://[KEY] + cosign attest-blob --predicate --type --key hashivault://[KEY] --bundle # supply attestation via stdin - echo | cosign attest-blob --predicate - --yes`, + echo | cosign attest-blob --predicate - --bundle --yes`, PersistentPreRun: options.BindViper, PreRunE: func(_ *cobra.Command, _ []string) error { - if o.NewBundleFormat && o.BundlePath == "" { - return fmt.Errorf("must specify --bundle with --new-bundle-format") + if o.BundlePath == "" { + return fmt.Errorf("must specify --bundle") + } + var attestType string + if o.Key == "" && !o.SecurityKey.Use { + attestType = "keyless" + } else if o.IssueCertificate { + attestType = "certificate-based" + } + if attestType != "" { + if !o.UseSigningConfig && o.SigningConfigPath == "" { + return fmt.Errorf("%s attesting requires a signing config (either from TUF via --use-signing-config or explicitly via a file with --signing-config)", attestType) + } } return nil }, @@ -61,6 +72,7 @@ func AttestBlob() *cobra.Command { if o.Predicate.Statement == "" && len(args) != 1 { return cobra.ExactArgs(1)(cmd, args) } + oidcClientSecret, err := o.OIDC.ClientSecret() if err != nil { return err @@ -71,48 +83,35 @@ func AttestBlob() *cobra.Command { PassFunc: generate.GetPass, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, - FulcioURL: o.Fulcio.URL, IDToken: o.Fulcio.IdentityToken, FulcioAuthFlow: o.Fulcio.AuthFlow, - InsecureSkipFulcioVerify: o.Fulcio.InsecureSkipFulcioVerify, - RekorURL: o.Rekor.URL, - OIDCIssuer: o.OIDC.Issuer, OIDCClientID: o.OIDC.ClientID, OIDCClientSecret: oidcClientSecret, OIDCRedirectURL: o.OIDC.RedirectURL, + OIDCDisableProviders: o.OIDC.DisableAmbientProviders, OIDCProvider: o.OIDC.Provider, SkipConfirmation: o.SkipConfirmation, TSAClientCACert: o.TSAClientCACert, TSAClientKey: o.TSAClientKey, TSAClientCert: o.TSAClientCert, TSAServerName: o.TSAServerName, - TSAServerURL: o.TSAServerURL, - RFC3161TimestampPath: o.RFC3161TimestampPath, IssueCertificateForExistingKey: o.IssueCertificate, BundlePath: o.BundlePath, - NewBundleFormat: o.NewBundleFormat, } if err := signcommon.LoadTrustedMaterialAndSigningConfig(cmd.Context(), &ko, o.UseSigningConfig, o.SigningConfigPath, - o.Rekor.URL, o.Fulcio.URL, o.OIDC.Issuer, o.TSAServerURL, o.TrustedRootPath, o.TlogUpload, - o.NewBundleFormat, o.BundlePath, o.Key, o.IssueCertificate, - "", o.OutputAttestation, o.OutputCertificate, "", o.OutputSignature, o.RFC3161TimestampPath); err != nil { + o.TrustedRootPath, o.Key); err != nil { return err } v := attest.AttestBlobCommand{ - KeyOpts: ko, - CertPath: o.Cert, - CertChainPath: o.CertChain, - ArtifactHash: o.Hash, - TlogUpload: o.TlogUpload, - PredicateType: o.Predicate.Type, - PredicatePath: o.Predicate.Path, - StatementPath: o.Predicate.Statement, - OutputSignature: o.OutputSignature, - OutputAttestation: o.OutputAttestation, - OutputCertificate: o.OutputCertificate, - Timeout: ro.Timeout, - RekorEntryType: o.RekorEntryType, + KeyOpts: ko, + CertPath: o.Cert, + CertChainPath: o.CertChain, + ArtifactHash: o.Hash, + PredicateType: o.Predicate.Type, + PredicatePath: o.Predicate.Path, + StatementPath: o.Predicate.Statement, + Timeout: ro.Timeout, } var artifactPath string if len(args) == 1 { diff --git a/cmd/cosign/cli/dockerfile.go b/cmd/cosign/cli/dockerfile.go index 9ef2f9c35ac..ad8d43836f8 100644 --- a/cmd/cosign/cli/dockerfile.go +++ b/cmd/cosign/cli/dockerfile.go @@ -89,24 +89,17 @@ Shell-like variables in the Dockerfile's FROM lines will be substituted with val CertVerifyOptions: o.CertVerify, CheckClaims: o.CheckClaims, KeyRef: o.Key, - CertRef: o.CertVerify.Cert, CertGithubWorkflowTrigger: o.CertVerify.CertGithubWorkflowTrigger, CertGithubWorkflowSha: o.CertVerify.CertGithubWorkflowSha, CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName, CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository, CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef, - CertChain: o.CertVerify.CertChain, IgnoreSCT: o.CertVerify.IgnoreSCT, - SCTRef: o.CertVerify.SCT, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, Output: o.Output, - RekorURL: o.Rekor.URL, - Attachment: o.Attachment, Annotations: annotations, LocalImage: o.LocalImage, - Offline: o.CommonVerifyOptions.Offline, - TSACertChainPath: o.CommonVerifyOptions.TSACertChainPath, IgnoreTlog: o.CommonVerifyOptions.IgnoreTlog, MaxWorkers: o.CommonVerifyOptions.MaxWorkers, }, diff --git a/cmd/cosign/cli/manifest.go b/cmd/cosign/cli/manifest.go index 9a852ee3ac8..89a702d0b1f 100644 --- a/cmd/cosign/cli/manifest.go +++ b/cmd/cosign/cli/manifest.go @@ -84,24 +84,17 @@ against the transparency log.`, CertVerifyOptions: o.CertVerify, CheckClaims: o.CheckClaims, KeyRef: o.Key, - CertRef: o.CertVerify.Cert, CertGithubWorkflowTrigger: o.CertVerify.CertGithubWorkflowTrigger, CertGithubWorkflowSha: o.CertVerify.CertGithubWorkflowSha, CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName, CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository, CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef, - CertChain: o.CertVerify.CertChain, IgnoreSCT: o.CertVerify.IgnoreSCT, - SCTRef: o.CertVerify.SCT, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, Output: o.Output, - RekorURL: o.Rekor.URL, - Attachment: o.Attachment, Annotations: annotations, LocalImage: o.LocalImage, - Offline: o.CommonVerifyOptions.Offline, - TSACertChainPath: o.CommonVerifyOptions.TSACertChainPath, IgnoreTlog: o.CommonVerifyOptions.IgnoreTlog, MaxWorkers: o.CommonVerifyOptions.MaxWorkers, }, diff --git a/cmd/cosign/cli/options/attest.go b/cmd/cosign/cli/options/attest.go index f142e6d4e90..3a99dee1fd1 100644 --- a/cmd/cosign/cli/options/attest.go +++ b/cmd/cosign/cli/options/attest.go @@ -16,35 +16,26 @@ package options import ( - "strings" - "github.com/spf13/cobra" ) // AttestOptions is the top level wrapper for the attest command. type AttestOptions struct { - Key string - Cert string - CertChain string - IssueCertificate bool - NoUpload bool - Replace bool - SkipConfirmation bool - TlogUpload bool - TSAClientCACert string - TSAClientCert string - TSAClientKey string - TSAServerName string - TSAServerURL string - RekorEntryType string - RecordCreationTimestamp bool - BundlePath string - NewBundleFormat bool - UseSigningConfig bool - SigningConfigPath string - TrustedRootPath string - - Rekor RekorOptions + Key string + Cert string + CertChain string + IssueCertificate bool + NoUpload bool + SkipConfirmation bool + TSAClientCACert string + TSAClientCert string + TSAClientKey string + TSAServerName string + BundlePath string + UseSigningConfig bool + SigningConfigPath string + TrustedRootPath string + Fulcio FulcioOptions OIDC OIDCOptions SecurityKey SecurityKeyOptions @@ -60,7 +51,6 @@ func (o *AttestOptions) AddFlags(cmd *cobra.Command) { o.Predicate.AddFlags(cmd) o.Fulcio.AddFlags(cmd) o.OIDC.AddFlags(cmd) - o.Rekor.AddFlags(cmd) o.Registry.AddFlags(cmd) cmd.Flags().StringVar(&o.Key, "key", "", @@ -80,22 +70,10 @@ func (o *AttestOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVar(&o.NoUpload, "no-upload", false, "do not upload the generated attestation, but send the attestation output to STDOUT") - cmd.Flags().BoolVarP(&o.Replace, "replace", "", false, - "") - _ = cmd.Flags().MarkDeprecated("replace", "not needed when OCI referrers become the default behavior") cmd.Flags().BoolVarP(&o.SkipConfirmation, "yes", "y", false, "skip confirmation prompts for non-destructive operations") - cmd.Flags().BoolVar(&o.TlogUpload, "tlog-upload", true, - "whether or not to upload to the tlog") - _ = cmd.Flags().MarkDeprecated("tlog-upload", "prefer using a --signing-config file with no transparency log services") - - cmd.Flags().StringVar(&o.RekorEntryType, "rekor-entry-type", rekorEntryTypes[0], - "specifies the type to be used for a rekor entry upload ("+strings.Join(rekorEntryTypes, "|")+")") - _ = cmd.RegisterFlagCompletionFunc("rekor-entry-type", cobra.FixedCompletions(rekorEntryTypes, cobra.ShellCompDirectiveNoFileComp)) - _ = cmd.Flags().MarkDeprecated("rekor-entry-type", "support for this flag will be removed in the future. it is strongly discouraged to rely on Rekor for attestation storage, and in future releases of Rekor, this functionality will be removed.") - cmd.Flags().StringVar(&o.TSAClientCACert, "timestamp-client-cacert", "", "path to the X.509 CA certificate file in PEM format to be used for the connection to the TSA Server") @@ -109,15 +87,6 @@ func (o *AttestOptions) AddFlags(cmd *cobra.Command) { "SAN name to use as the 'ServerName' tls.Config field to verify the mTLS connection to the TSA Server") _ = cmd.RegisterFlagCompletionFunc("timestamp-server-name", cobra.NoFileCompletions) - cmd.Flags().StringVar(&o.TSAServerURL, "timestamp-server-url", "", - "url to the Timestamp RFC3161 server, default none. Must be the path to the API to request timestamp responses, e.g. https://freetsa.org/tsr") - _ = cmd.RegisterFlagCompletionFunc("timestamp-server-url", cobra.NoFileCompletions) - _ = cmd.Flags().MarkDeprecated("timestamp-server-url", "please use a signing config to specify a timestamp server url; see `cosign signing-config --help`") - - 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().MarkDeprecated("record-creation-timestamp", "not used with the new bundle format") - cmd.Flags().BoolVar(&o.IssueCertificate, "issue-certificate", false, "issue a code signing certificate from Fulcio, even if a key is provided") _ = cmd.Flags().MarkDeprecated("issue-certificate", "support for this flag will be removed in the future") @@ -126,9 +95,7 @@ func (o *AttestOptions) AddFlags(cmd *cobra.Command) { "write everything required to verify the blob to a FILE") _ = cmd.MarkFlagFilename("bundle", bundleExts...) - cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", true, "attach a Sigstore bundle using OCI referrers API") - _ = cmd.Flags().MarkDeprecated("new-bundle-format", "this will be the only supported format in future versions") - + // TODO(#5013): Remove --use-signing-config in favor of offline signing flag cmd.Flags().BoolVar(&o.UseSigningConfig, "use-signing-config", true, "whether to use a TUF-provided signing config for the service URLs") diff --git a/cmd/cosign/cli/options/attest_blob.go b/cmd/cosign/cli/options/attest_blob.go index f2d72d845ec..bcb611edca9 100644 --- a/cmd/cosign/cli/options/attest_blob.go +++ b/cmd/cosign/cli/options/attest_blob.go @@ -15,8 +15,6 @@ package options import ( - "strings" - "github.com/spf13/cobra" ) @@ -26,28 +24,17 @@ type AttestBlobOptions struct { Cert string CertChain string IssueCertificate bool - - SkipConfirmation bool - TlogUpload bool - TSAClientCACert string - TSAClientCert string - TSAClientKey string - TSAServerName string - TSAServerURL string - RFC3161TimestampPath string + SkipConfirmation bool + TSAClientCACert string + TSAClientCert string + TSAClientKey string + TSAServerName string Hash string Predicate PredicateLocalOptions - OutputSignature string - OutputAttestation string - OutputCertificate string - BundlePath string - NewBundleFormat bool - - RekorEntryType string + BundlePath string - Rekor RekorOptions Fulcio FulcioOptions OIDC OIDCOptions SecurityKey SecurityKeyOptions @@ -62,7 +49,6 @@ var _ Interface = (*AttestOptions)(nil) // AddFlags implements Interface func (o *AttestBlobOptions) AddFlags(cmd *cobra.Command) { o.Predicate.AddFlags(cmd) - o.Rekor.AddFlags(cmd) o.Fulcio.AddFlags(cmd) o.OIDC.AddFlags(cmd) o.SecurityKey.AddFlags(cmd) @@ -82,29 +68,11 @@ func (o *AttestBlobOptions) AddFlags(cmd *cobra.Command) { "signing certificate and end with the root certificate.") _ = cmd.MarkFlagFilename("certificate-chain", certificateExts...) - cmd.Flags().StringVar(&o.OutputSignature, "output-signature", "", - "write the signature to FILE") - _ = cmd.MarkFlagFilename("output-signature", signatureExts...) - _ = cmd.Flags().MarkDeprecated("output-signature", "please use --bundle to provide the output bundle location, which will include the signature") - - cmd.Flags().StringVar(&o.OutputAttestation, "output-attestation", "", - "write the attestation to FILE") - // _ = cmd.MarkFlagFilename("output-attestation") // no typical extensions - _ = cmd.Flags().MarkDeprecated("output-attestation", "please use --bundle to provide the output bundle location, which will include the attestation") - - cmd.Flags().StringVar(&o.OutputCertificate, "output-certificate", "", - "write the certificate to FILE") - _ = cmd.MarkFlagFilename("key", certificateExts...) - _ = cmd.Flags().MarkDeprecated("output-certificate", "please use --bundle to provide the output bundle location, which will include the certificate") - cmd.Flags().StringVar(&o.BundlePath, "bundle", "", "write everything required to verify the blob to a FILE") _ = cmd.MarkFlagFilename("bundle", bundleExts...) - cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", true, - "output bundle in new format that contains all verification material") - _ = cmd.Flags().MarkDeprecated("new-bundle-format", "this will be the only supported format in future versions") - + // TODO(#5013): Remove --use-signing-config in favor of offline signing flag cmd.Flags().BoolVar(&o.UseSigningConfig, "use-signing-config", true, "whether to use a TUF-provided signing config for the service URLs. Must provide --bundle, which will output verification material in the new format") @@ -123,36 +91,21 @@ func (o *AttestBlobOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVarP(&o.SkipConfirmation, "yes", "y", false, "skip confirmation prompts for non-destructive operations") - cmd.Flags().BoolVar(&o.TlogUpload, "tlog-upload", true, - "whether or not to upload to the tlog") - _ = cmd.Flags().MarkDeprecated("tlog-upload", "prefer using a --signing-config file with no transparency log services") - - cmd.Flags().StringVar(&o.RekorEntryType, "rekor-entry-type", rekorEntryTypes[0], - "specifies the type to be used for a rekor entry upload ("+strings.Join(rekorEntryTypes, "|")+")") - _ = cmd.RegisterFlagCompletionFunc("rekor-entry-type", cobra.FixedCompletions(rekorEntryTypes, cobra.ShellCompDirectiveNoFileComp)) - _ = cmd.Flags().MarkDeprecated("rekor-entry-type", "support for this flag will be removed in the future. it is strongly discouraged to rely on Rekor for attestation storage, and in future releases of Rekor, this functionality will be removed.") - cmd.Flags().StringVar(&o.TSAClientCACert, "timestamp-client-cacert", "", "path to the X.509 CA certificate file in PEM format to be used for the connection to the TSA Server") + _ = cmd.MarkFlagFilename("timestamp-client-cacert", certificateExts...) cmd.Flags().StringVar(&o.TSAClientCert, "timestamp-client-cert", "", "path to the X.509 certificate file in PEM format to be used for the connection to the TSA Server") + _ = cmd.MarkFlagFilename("timestamp-client-cert", certificateExts...) cmd.Flags().StringVar(&o.TSAClientKey, "timestamp-client-key", "", "path to the X.509 private key file in PEM format to be used, together with the 'timestamp-client-cert' value, for the connection to the TSA Server") + _ = cmd.MarkFlagFilename("timestamp-client-key", privateKeyExts...) cmd.Flags().StringVar(&o.TSAServerName, "timestamp-server-name", "", "SAN name to use as the 'ServerName' tls.Config field to verify the mTLS connection to the TSA Server") - - cmd.Flags().StringVar(&o.TSAServerURL, "timestamp-server-url", "", - "url to the Timestamp RFC3161 server, default none. Must be the path to the API to request timestamp responses, e.g. https://freetsa.org/tsr") - _ = cmd.RegisterFlagCompletionFunc("timestamp-server-url", cobra.NoFileCompletions) - _ = cmd.Flags().MarkDeprecated("timestamp-server-url", "please use a signing config to specify a timestamp server url; see `cosign signing-config --help`") - - cmd.Flags().StringVar(&o.RFC3161TimestampPath, "rfc3161-timestamp-bundle", "", - "path to an RFC 3161 timestamp bundle FILE") - // _ = cmd.MarkFlagFilename("rfc3161-timestamp-bundle") // no typical extensions - _ = cmd.Flags().MarkDeprecated("rfc3161-timestamp-bundle", "please use --bundle to provide the output bundle location, which will include the signed timestamp") + _ = cmd.RegisterFlagCompletionFunc("timestamp-server-name", cobra.NoFileCompletions) cmd.Flags().BoolVar(&o.IssueCertificate, "issue-certificate", false, "issue a code signing certificate from Fulcio, even if a key is provided") diff --git a/cmd/cosign/cli/options/certificate.go b/cmd/cosign/cli/options/certificate.go index 33a2329aafa..cdd2d7d15bd 100644 --- a/cmd/cosign/cli/options/certificate.go +++ b/cmd/cosign/cli/options/certificate.go @@ -23,7 +23,6 @@ import ( // CertVerifyOptions is the wrapper for certificate verification. type CertVerifyOptions struct { - Cert string CertIdentity string CertIdentityRegexp string CertOidcIssuer string @@ -33,10 +32,6 @@ type CertVerifyOptions struct { CertGithubWorkflowName string CertGithubWorkflowRepository string CertGithubWorkflowRef string - CAIntermediates string - CARoots string - CertChain string - SCT string IgnoreSCT bool } @@ -44,11 +39,6 @@ var _ Interface = (*RekorOptions)(nil) // AddFlags implements Interface func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) { - cmd.Flags().StringVar(&o.Cert, "certificate", "", - "path to the public certificate. The certificate will be verified against the Fulcio roots if the --certificate-chain option is not passed.") - _ = cmd.MarkFlagFilename("certificate", certificateExts...) - _ = cmd.Flags().MarkDeprecated("certificate", "please use --bundle with --trusted-root to provide the public certificate") - cmd.Flags().StringVar(&o.CertIdentity, "certificate-identity", "", "The identity expected in a valid Fulcio certificate. Valid values include email address, DNS names, IP addresses, and URIs. Either --certificate-identity or --certificate-identity-regexp must be set for keyless flows.") _ = cmd.RegisterFlagCompletionFunc("certificate-identity", cobra.NoFileCompletions) @@ -88,35 +78,6 @@ func (o *CertVerifyOptions) AddFlags(cmd *cobra.Command) { _ = cmd.RegisterFlagCompletionFunc("certificate-github-workflow-ref", cobra.NoFileCompletions) // -- Cert extensions end -- - cmd.Flags().StringVar(&o.CAIntermediates, "ca-intermediates", "", - "path to a file of intermediate CA certificates in PEM format which will be needed "+ - "when building the certificate chains for the signing certificate. "+ - "The flag is optional and must be used together with --ca-roots, conflicts with "+ - "--certificate-chain.") - _ = cmd.MarkFlagFilename("ca-intermediates", certificateExts...) - _ = cmd.Flags().MarkDeprecated("ca-intermediates", "please use --trusted-root to provide CA certificates") - cmd.Flags().StringVar(&o.CARoots, "ca-roots", "", - "path to a bundle file of CA certificates in PEM format which will be needed "+ - "when building the certificate chains for the signing certificate. Conflicts with --certificate-chain.") - _ = cmd.MarkFlagFilename("ca-roots", certificateExts...) - _ = cmd.Flags().MarkDeprecated("ca-roots", "please use --trusted-root to provide CA certificates") - - cmd.Flags().StringVar(&o.CertChain, "certificate-chain", "", - "path to a list of CA certificates in PEM format which will be needed "+ - "when building the certificate chain for the signing certificate. "+ - "Must start with the parent intermediate CA certificate of the "+ - "signing certificate and end with the root certificate. Conflicts with --ca-roots and --ca-intermediates.") - _ = cmd.MarkFlagFilename("certificate-chain", certificateExts...) - cmd.MarkFlagsMutuallyExclusive("ca-roots", "certificate-chain") - cmd.MarkFlagsMutuallyExclusive("ca-intermediates", "certificate-chain") - _ = cmd.Flags().MarkDeprecated("certificate-chain", "please use --trusted-root to provide the certificate chain") - - cmd.Flags().StringVar(&o.SCT, "sct", "", - "path to a detached Signed Certificate Timestamp, formatted as a RFC6962 AddChainResponse struct. "+ - "If a certificate contains an SCT, verification will check both the detached and embedded SCTs.") - // _ = cmd.MarkFlagFilename("sct") // no typical extensions - _ = cmd.Flags().MarkDeprecated("sct", "a Signed Certificate Timestamp is now provided via a Sigstore bundle") - 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") diff --git a/cmd/cosign/cli/options/fulcio.go b/cmd/cosign/cli/options/fulcio.go index fabdcb9f7f1..ffc077a4668 100644 --- a/cmd/cosign/cli/options/fulcio.go +++ b/cmd/cosign/cli/options/fulcio.go @@ -23,10 +23,8 @@ const DefaultFulcioURL = "https://fulcio.sigstore.dev" // FulcioOptions is the wrapper for Fulcio related options. type FulcioOptions struct { - URL string - AuthFlow string - IdentityToken string - InsecureSkipFulcioVerify bool // Deprecated: SCT verification is no longer performed during signing/attestation. + AuthFlow string + IdentityToken string } var _ Interface = (*FulcioOptions)(nil) @@ -34,18 +32,10 @@ var _ Interface = (*FulcioOptions)(nil) // AddFlags implements Interface func (o *FulcioOptions) AddFlags(cmd *cobra.Command) { // TODO: change this back to api.SigstorePublicServerURL after the v1 migration is complete. - cmd.Flags().StringVar(&o.URL, "fulcio-url", DefaultFulcioURL, - "address of sigstore PKI server") - _ = cmd.Flags().MarkDeprecated("fulcio-url", "please use a signing config to specify a fulcio url; see `cosign signing-config --help`") - cmd.Flags().StringVar(&o.IdentityToken, "identity-token", "", "identity token to use for certificate from fulcio. the token or a path to a file containing the token is accepted.") // _ = cmd.MarkFlagFilename("identity-token") // no typical extensions cmd.Flags().StringVar(&o.AuthFlow, "fulcio-auth-flow", "", "fulcio interactive oauth2 flow to use for certificate from fulcio. Defaults to determining the flow based on the runtime environment. (options) normal|device|token|client_credentials") - - cmd.Flags().BoolVar(&o.InsecureSkipFulcioVerify, "insecure-skip-verify", false, - "skip verifying fulcio published to the SCT (this should only be used for testing).") - _ = cmd.Flags().MarkDeprecated("insecure-skip-verify", "SCT verification is no longer performed during signing/attestation.") } diff --git a/cmd/cosign/cli/options/key.go b/cmd/cosign/cli/options/key.go index 175f2280269..b0f878e019f 100644 --- a/cmd/cosign/cli/options/key.go +++ b/cmd/cosign/cli/options/key.go @@ -25,8 +25,6 @@ type KeyOpts struct { Sk bool Slot string KeyRef string - FulcioURL string - RekorURL string IDToken string PassFunc cosign.PassFunc OIDCIssuer string @@ -36,13 +34,11 @@ type KeyOpts struct { OIDCDisableProviders bool // Disable OIDC credential providers in keyless signer OIDCProvider string // Specify which OIDC credential provider to use for keyless signer BundlePath string - NewBundleFormat bool SkipConfirmation bool TSAClientCACert string TSAClientCert string TSAClientKey string TSAServerName string // expected SAN field in the TSA server's certificate - https://pkg.go.dev/crypto/tls#Config.ServerName - TSAServerURL string RFC3161TimestampPath string TSACertChainPath string // IssueCertificate controls whether to issue a certificate when a key is diff --git a/cmd/cosign/cli/options/oidc.go b/cmd/cosign/cli/options/oidc.go index cf13d39fd6b..988b2d99698 100644 --- a/cmd/cosign/cli/options/oidc.go +++ b/cmd/cosign/cli/options/oidc.go @@ -28,7 +28,6 @@ const DefaultOIDCIssuerURL = "https://oauth2.sigstore.dev/auth" // OIDCOptions is the wrapper for OIDC related options. type OIDCOptions struct { - Issuer string ClientID string clientSecretFile string RedirectURL string @@ -56,10 +55,6 @@ var _ Interface = (*OIDCOptions)(nil) // AddFlags implements Interface func (o *OIDCOptions) AddFlags(cmd *cobra.Command) { - cmd.Flags().StringVar(&o.Issuer, "oidc-issuer", DefaultOIDCIssuerURL, - "OIDC provider to be used to issue ID token") - _ = cmd.Flags().MarkDeprecated("oidc-issuer", "please use a signing config to specify an OIDC issuer; see `cosign signing-config create --help`") - cmd.Flags().StringVar(&o.ClientID, "oidc-client-id", "sigstore", "OIDC client ID for application") diff --git a/cmd/cosign/cli/options/options.go b/cmd/cosign/cli/options/options.go index be364c204ba..1a17274d35b 100644 --- a/cmd/cosign/cli/options/options.go +++ b/cmd/cosign/cli/options/options.go @@ -55,8 +55,3 @@ var signatureExts = []string{ var wasmExts = []string{ "wasm", } - -var rekorEntryTypes = []string{ - "dsse", // first one is the default - "intoto", -} diff --git a/cmd/cosign/cli/options/sign.go b/cmd/cosign/cli/options/sign.go index e65caff65da..4e00683a523 100644 --- a/cmd/cosign/cli/options/sign.go +++ b/cmd/cosign/cli/options/sign.go @@ -21,34 +21,22 @@ import ( // SignOptions is the top level wrapper for the sign command. type SignOptions struct { - Key string - Cert string - CertChain string - Upload bool - Output string // deprecated: TODO remove when the output flag is fully deprecated - OutputSignature string // TODO: this should be the root output file arg. - OutputPayload string - OutputCertificate string - BundlePath string - PayloadPath string - Recursive bool - Attachment string - SkipConfirmation bool - TlogUpload bool - TSAClientCACert string - TSAClientCert string - TSAClientKey string - TSAServerName string - TSAServerURL string - IssueCertificate bool - SignContainerIdentities []string - RecordCreationTimestamp bool - NewBundleFormat bool - UseSigningConfig bool - SigningConfigPath string - TrustedRootPath string - - Rekor RekorOptions + Key string + Cert string + CertChain string + Upload bool + BundlePath string + Recursive bool + SkipConfirmation bool + TSAClientCACert string + TSAClientCert string + TSAClientKey string + TSAServerName string + IssueCertificate bool + UseSigningConfig bool + SigningConfigPath string + TrustedRootPath string + Fulcio FulcioOptions OIDC OIDCOptions SecurityKey SecurityKeyOptions @@ -61,7 +49,6 @@ var _ Interface = (*SignOptions)(nil) // AddFlags implements Interface func (o *SignOptions) AddFlags(cmd *cobra.Command) { - o.Rekor.AddFlags(cmd) o.Fulcio.AddFlags(cmd) o.OIDC.AddFlags(cmd) o.SecurityKey.AddFlags(cmd) @@ -87,45 +74,16 @@ func (o *SignOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVar(&o.Upload, "upload", true, "whether to upload the signature") - cmd.Flags().StringVar(&o.OutputSignature, "output-signature", "", - "write the signature to FILE") - _ = cmd.MarkFlagFilename("output-signature", signatureExts...) - _ = cmd.Flags().MarkDeprecated("output-signature", "please use --bundle to provide the output bundle location, which will include the signature") - - cmd.Flags().StringVar(&o.OutputPayload, "output-payload", "", - "write the signed payload to FILE") - // _ = cmd.MarkFlagFilename("output-payload") // no typical extensions - _ = cmd.Flags().MarkDeprecated("output-payload", "please use --bundle to provide the output bundle location, which will include the payload") - - cmd.Flags().StringVar(&o.OutputCertificate, "output-certificate", "", - "write the certificate to FILE") - _ = cmd.MarkFlagFilename("output-certificate", certificateExts...) - _ = cmd.Flags().MarkDeprecated("output-certificate", "please use --bundle to provide the output bundle location, which will include the certificate") - cmd.Flags().StringVar(&o.BundlePath, "bundle", "", "write everything required to verify the image to FILE") _ = cmd.MarkFlagFilename("bundle", bundleExts...) - cmd.Flags().StringVar(&o.PayloadPath, "payload", "", - "path to a payload file to use rather than generating one") - // _ = cmd.MarkFlagFilename("payload") // no typical extensions - _ = cmd.Flags().MarkDeprecated("payload", "payload will always be generated automatically in future versions") - cmd.Flags().BoolVarP(&o.Recursive, "recursive", "r", false, "if a multi-arch image is specified, additionally sign each discrete image") - cmd.Flags().StringVar(&o.Attachment, "attachment", "", - "DEPRECATED, related image attachment to sign (sbom), default none") - _ = cmd.MarkFlagFilename("attachment", sbomExts...) - _ = cmd.Flags().MarkDeprecated("attachment", "please use OCI referrers for attachments; see `cosign attach sbom --registry-referrers-mode=oci-1-1 --help`") - cmd.Flags().BoolVarP(&o.SkipConfirmation, "yes", "y", false, "skip confirmation prompts for non-destructive operations") - cmd.Flags().BoolVar(&o.TlogUpload, "tlog-upload", true, - "whether or not to upload to the tlog") - _ = cmd.Flags().MarkDeprecated("tlog-upload", "prefer using a --signing-config file with no transparency log services") - cmd.Flags().StringVar(&o.TSAClientCACert, "timestamp-client-cacert", "", "path to the X.509 CA certificate file in PEM format to be used for the connection to the TSA Server") _ = cmd.MarkFlagFilename("timestamp-client-cacert", certificateExts...) @@ -141,26 +99,13 @@ func (o *SignOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&o.TSAServerName, "timestamp-server-name", "", "SAN name to use as the 'ServerName' tls.Config field to verify the mTLS connection to the TSA Server") - cmd.Flags().StringVar(&o.TSAServerURL, "timestamp-server-url", "", - "url to the Timestamp RFC3161 server, default none. Must be the path to the API to request timestamp responses, e.g. https://freetsa.org/tsr") - _ = cmd.Flags().MarkDeprecated("timestamp-server-url", "please use a signing config to specify a timestamp server url; see `cosign signing-config --help`") - _ = cmd.MarkFlagFilename("certificate", certificateExts...) cmd.Flags().BoolVar(&o.IssueCertificate, "issue-certificate", false, "issue a code signing certificate from Fulcio, even if a key is provided") _ = cmd.Flags().MarkDeprecated("issue-certificate", "support for this flag will be removed in the future") - cmd.Flags().StringSliceVar(&o.SignContainerIdentities, "sign-container-identity", nil, - "manually set the .critical.docker-reference field for the signed identity, which is useful when image proxies are being used where the pull reference should match the signature, this flag is comma delimited. ex: --sign-container-identity=identity1,identity2") - _ = cmd.Flags().MarkDeprecated("sign-container-identity", "not possible when OCI referrers become the default behavior") - - cmd.Flags().BoolVar(&o.RecordCreationTimestamp, "record-creation-timestamp", false, "set the createdAt timestamp in the signature artifact to the time it was created; by default, cosign sets this to the zero value") - _ = cmd.Flags().MarkDeprecated("record-creation-timestamp", "not used with the new bundle format") - - cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", true, "expect the signature/attestation to be packaged in a Sigstore bundle") - _ = cmd.Flags().MarkDeprecated("new-bundle-format", "this will be the only supported format in future versions") - + // TODO(#5013): Remove --use-signing-config in favor of offline signing flag cmd.Flags().BoolVar(&o.UseSigningConfig, "use-signing-config", true, "whether to use a TUF-provided signing config for the service URLs") diff --git a/cmd/cosign/cli/options/signature_digest.go b/cmd/cosign/cli/options/signature_digest.go deleted file mode 100644 index 8c27c9a53d1..00000000000 --- a/cmd/cosign/cli/options/signature_digest.go +++ /dev/null @@ -1,85 +0,0 @@ -// -// Copyright 2021 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 options - -import ( - "crypto" - _ "crypto/sha256" // for sha224 + sha256 - _ "crypto/sha512" // for sha384 + sha512 - "fmt" - "sort" - "strings" - - "github.com/spf13/cobra" -) - -var supportedSignatureAlgorithms = map[string]crypto.Hash{ - "sha224": crypto.SHA224, - "sha256": crypto.SHA256, - "sha384": crypto.SHA384, - "sha512": crypto.SHA512, -} - -func supportedSignatureAlgorithmNames() []string { - names := make([]string, 0, len(supportedSignatureAlgorithms)) - - for name := range supportedSignatureAlgorithms { - names = append(names, name) - } - - sort.Strings(names) - - return names -} - -// SignatureDigestOptions holds options for specifying which digest algorithm should -// be used when processing a signature. -type SignatureDigestOptions struct { - AlgorithmName string -} - -var _ Interface = (*SignatureDigestOptions)(nil) - -// AddFlags implements Interface -func (o *SignatureDigestOptions) AddFlags(cmd *cobra.Command) { - validSignatureDigestAlgorithms := strings.Join(supportedSignatureAlgorithmNames(), "|") - - cmd.Flags().StringVar(&o.AlgorithmName, "signature-digest-algorithm", "sha256", - fmt.Sprintf("digest algorithm to use when processing a signature (%s)", validSignatureDigestAlgorithms)) - _ = cmd.Flags().MarkDeprecated("signature-digest-algorithm", "please use --bundle, which already includes the digest algorithm") -} - -// HashAlgorithm converts the algorithm's name - provided as a string - into a crypto.Hash algorithm. -// Returns an error if the algorithm name doesn't match a supported algorithm, and defaults to SHA256 -// in the event that the given algorithm is invalid. -func (o *SignatureDigestOptions) HashAlgorithm() (crypto.Hash, error) { - normalizedAlgo := strings.ToLower(strings.TrimSpace(o.AlgorithmName)) - - if normalizedAlgo == "" { - return crypto.SHA256, nil - } - - algo, exists := supportedSignatureAlgorithms[normalizedAlgo] - if !exists { - return crypto.SHA256, fmt.Errorf("unknown digest algorithm: %s", o.AlgorithmName) - } - - if !algo.Available() { - return crypto.SHA256, fmt.Errorf("hash %q is not available on this platform", o.AlgorithmName) - } - - return algo, nil -} diff --git a/cmd/cosign/cli/options/signblob.go b/cmd/cosign/cli/options/signblob.go index d2bc53bc395..9b6496d3dc0 100644 --- a/cmd/cosign/cli/options/signblob.go +++ b/cmd/cosign/cli/options/signblob.go @@ -26,32 +26,21 @@ import ( ) // SignBlobOptions is the top level wrapper for the sign-blob command. -// The new output-certificate flag is only in use when COSIGN_EXPERIMENTAL is enabled type SignBlobOptions struct { - Key string - Cert string - CertChain string - Base64Output bool - Output string // deprecated: TODO remove when the output flag is fully deprecated - OutputSignature string // TODO: this should be the root output file arg. - OutputCertificate string - SecurityKey SecurityKeyOptions - Fulcio FulcioOptions - Rekor RekorOptions - OIDC OIDCOptions - Registry RegistryOptions - BundlePath string - NewBundleFormat bool - SkipConfirmation bool - TlogUpload bool - TSAClientCACert string - TSAClientCert string - TSAClientKey string - TSAServerName string - TSAServerURL string - RFC3161TimestampPath string - IssueCertificate bool - SigningAlgorithm string + Key string + Cert string + CertChain string + SecurityKey SecurityKeyOptions + Fulcio FulcioOptions + OIDC OIDCOptions + BundlePath string + SkipConfirmation bool + TSAClientCACert string + TSAClientCert string + TSAClientKey string + TSAServerName string + IssueCertificate bool + SigningAlgorithm string UseSigningConfig bool SigningConfigPath string @@ -64,7 +53,6 @@ var _ Interface = (*SignBlobOptions)(nil) func (o *SignBlobOptions) AddFlags(cmd *cobra.Command) { o.SecurityKey.AddFlags(cmd) o.Fulcio.AddFlags(cmd) - o.Rekor.AddFlags(cmd) o.OIDC.AddFlags(cmd) cmd.Flags().StringVar(&o.Key, "key", "", @@ -82,33 +70,11 @@ func (o *SignBlobOptions) AddFlags(cmd *cobra.Command) { "signing certificate and end with the root certificate.") _ = cmd.MarkFlagFilename("certificate-chain", certificateExts...) - cmd.Flags().BoolVar(&o.Base64Output, "b64", true, - "whether to base64 encode the output") - _ = cmd.Flags().MarkDeprecated("b64", "please use --bundle, which already base64 encodes content as appropriate") - - cmd.Flags().StringVar(&o.OutputSignature, "output-signature", "", - "write the signature to FILE") - _ = cmd.MarkFlagFilename("output-signature", signatureExts...) - _ = cmd.Flags().MarkDeprecated("output-signature", "please use --bundle to provide the output bundle location, which will include the signature") - - // TODO: remove when output flag is fully deprecated - cmd.Flags().StringVar(&o.Output, "output", "", "write the signature to FILE") - _ = cmd.MarkFlagFilename("output", signatureExts...) - _ = cmd.Flags().MarkDeprecated("output", "please use --bundle to provide the output bundle location, which will include the signature") - - cmd.Flags().StringVar(&o.OutputCertificate, "output-certificate", "", - "write the certificate to FILE") - _ = cmd.MarkFlagFilename("output-certificate", certificateExts...) - _ = cmd.Flags().MarkDeprecated("output-certificate", "please use --bundle to provide the output bundle location, which will include the certificate") - cmd.Flags().StringVar(&o.BundlePath, "bundle", "", "write everything required to verify the blob to a FILE") _ = cmd.MarkFlagFilename("bundle", bundleExts...) - cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", true, - "output bundle in new format that contains all verification material") - _ = cmd.Flags().MarkDeprecated("new-bundle-format", "this will be the only supported format in future versions") - + // TODO(#5013): Remove --use-signing-config in favor of offline signing flag cmd.Flags().BoolVar(&o.UseSigningConfig, "use-signing-config", true, "whether to use a TUF-provided signing config for the service URLs. Must provide --bundle, which will output verification material in the new format") @@ -123,10 +89,6 @@ func (o *SignBlobOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVarP(&o.SkipConfirmation, "yes", "y", false, "skip confirmation prompts for non-destructive operations") - cmd.Flags().BoolVar(&o.TlogUpload, "tlog-upload", true, - "whether or not to upload to the tlog") - _ = cmd.Flags().MarkDeprecated("tlog-upload", "prefer using a --signing-config file with no transparency log services") - cmd.Flags().StringVar(&o.TSAClientCACert, "timestamp-client-cacert", "", "path to the X.509 CA certificate file in PEM format to be used for the connection to the TSA Server") _ = cmd.MarkFlagFilename("timestamp-client-cacert", certificateExts...) @@ -143,16 +105,6 @@ func (o *SignBlobOptions) AddFlags(cmd *cobra.Command) { "SAN name to use as the 'ServerName' tls.Config field to verify the mTLS connection to the TSA Server") _ = cmd.RegisterFlagCompletionFunc("timestamp-server-name", cobra.NoFileCompletions) - cmd.Flags().StringVar(&o.TSAServerURL, "timestamp-server-url", "", - "url to the Timestamp RFC3161 server, default none. Must be the path to the API to request timestamp responses, e.g. https://freetsa.org/tsr") - _ = cmd.RegisterFlagCompletionFunc("timestamp-server-url", cobra.NoFileCompletions) - _ = cmd.Flags().MarkDeprecated("timestamp-server-url", "please use a signing config to specify a timestamp server url; see `cosign signing-config --help`") - - cmd.Flags().StringVar(&o.RFC3161TimestampPath, "rfc3161-timestamp", "", - "write the RFC3161 timestamp to a file") - // _ = cmd.MarkFlagFilename("rfc3161-timestamp") // no typical extensions - _ = cmd.Flags().MarkDeprecated("rfc3161-timestamp", "please use --bundle to provide the output bundle location, which will include the signed timestamp") - cmd.Flags().BoolVar(&o.IssueCertificate, "issue-certificate", false, "issue a code signing certificate from Fulcio, even if a key is provided") _ = cmd.Flags().MarkDeprecated("issue-certificate", "support for this flag will be removed in the future") diff --git a/cmd/cosign/cli/options/verify.go b/cmd/cosign/cli/options/verify.go index 918e226e776..ded61d2e45a 100644 --- a/cmd/cosign/cli/options/verify.go +++ b/cmd/cosign/cli/options/verify.go @@ -24,31 +24,16 @@ import ( ) type CommonVerifyOptions struct { - Offline bool // Force offline verification - TSACertChainPath string - IgnoreTlog bool - MaxWorkers int + IgnoreTlog bool + MaxWorkers int // This is added to CommonVerifyOptions to provide a path to support // it for other verify options. - ExperimentalOCI11 bool - PrivateInfrastructure bool UseSignedTimestamps bool - NewBundleFormat bool TrustedRootPath string AllowCertificateChain bool } func (o *CommonVerifyOptions) AddFlags(cmd *cobra.Command) { - cmd.Flags().BoolVar(&o.Offline, "offline", false, - "only verify an artifact's inclusion in a transparency log using a provided proof, rather than querying the log. May still include network requests to retrieve service keys from a TUF repository") - _ = cmd.Flags().MarkDeprecated("offline", "To verify in an airgapped environment, provide a --bundle with the signature and verification material, and a --trusted-root file with the service keys and certificates") - - cmd.Flags().StringVar(&o.TSACertChainPath, "timestamp-certificate-chain", "", - "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") - _ = cmd.MarkFlagFilename("timestamp-certificate-chain", certificateExts...) - _ = cmd.Flags().MarkDeprecated("timestamp-certificate-chain", "please use --trusted-root to provide the timestamp authority certificate chain") - cmd.Flags().BoolVar(&o.UseSignedTimestamps, "use-signed-timestamps", false, "verify rfc3161 timestamps") @@ -56,14 +41,6 @@ func (o *CommonVerifyOptions) AddFlags(cmd *cobra.Command) { "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") - cmd.Flags().BoolVar(&o.PrivateInfrastructure, "private-infrastructure", false, - "skip transparency log verification when verifying artifacts in a privately deployed infrastructure") - _ = cmd.Flags().MarkDeprecated("private-infrastructure", "please use --insecure-ignore-tlog instead") - - cmd.Flags().BoolVar(&o.ExperimentalOCI11, "experimental-oci11", false, - "set to true to enable experimental OCI 1.1 behaviour (unrelated to bundle format)") - _ = cmd.Flags().MarkDeprecated("experimental-oci11", "OCI referrers will be the default behavior in future versions") - cmd.Flags().IntVar(&o.MaxWorkers, "max-workers", cosign.DefaultMaxWorkers, "the amount of maximum workers for parallel executions") _ = cmd.RegisterFlagCompletionFunc("max-workers", cobra.NoFileCompletions) @@ -72,10 +49,6 @@ func (o *CommonVerifyOptions) AddFlags(cmd *cobra.Command) { "Path to a Sigstore TrustedRoot JSON file") _ = cmd.MarkFlagFilename("trusted-root", "json") - cmd.Flags().BoolVar(&o.NewBundleFormat, "new-bundle-format", true, - "expect the signature/attestation to be packaged in a Sigstore bundle") - _ = cmd.Flags().MarkDeprecated("new-bundle-format", "this will be the only supported format in future versions") - cmd.Flags().BoolVar(&o.AllowCertificateChain, "allow-certificate-chain", false, "allow X.509 certificate chains in bundle verification material for v0.3+ bundles") } @@ -84,20 +57,15 @@ var verifyOutputTypes = []string{"json", "text"} // First one is the default // VerifyOptions is the top level wrapper for the `verify` command. type VerifyOptions struct { - Key string - CheckClaims bool - Attachment string - Output string - SignatureRef string - PayloadRef string - LocalImage bool + Key string + CheckClaims bool + Output string + LocalImage bool CommonVerifyOptions CommonVerifyOptions SecurityKey SecurityKeyOptions CertVerify CertVerifyOptions - Rekor RekorOptions Registry RegistryOptions - SignatureDigest SignatureDigestOptions AnnotationOptions } @@ -107,15 +75,11 @@ var _ Interface = (*VerifyOptions)(nil) // AddFlags implements Interface func (o *VerifyOptions) AddFlags(cmd *cobra.Command) { o.SecurityKey.AddFlags(cmd) - o.Rekor.AddFlags(cmd) o.CertVerify.AddFlags(cmd) o.Registry.AddFlags(cmd) - o.SignatureDigest.AddFlags(cmd) o.AnnotationOptions.AddFlags(cmd) o.CommonVerifyOptions.AddFlags(cmd) - _ = cmd.Flags().MarkDeprecated("rekor-url", "please use --bundle, which includes the Rekor inclusion proof") - cmd.Flags().StringVar(&o.Key, "key", "", "path to the public key file, KMS URI or Kubernetes Secret") _ = cmd.MarkFlagFilename("key", publicKeyExts...) @@ -123,25 +87,10 @@ func (o *VerifyOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVar(&o.CheckClaims, "check-claims", true, "whether to check the claims found") - cmd.Flags().StringVar(&o.Attachment, "attachment", "", - "DEPRECATED, related image attachment to verify (sbom), default none") - _ = cmd.MarkFlagFilename("attachment", sbomExts...) - _ = cmd.Flags().MarkDeprecated("attachment", "please use OCI referrers for attachments and verify with `--experimental-oci11`") - cmd.Flags().StringVarP(&o.Output, "output", "o", verifyOutputTypes[0], "output format for the signing image information ("+strings.Join(verifyOutputTypes, "|")+")") _ = cmd.RegisterFlagCompletionFunc("output", cobra.FixedCompletions(verifyOutputTypes, cobra.ShellCompDirectiveNoFileComp)) - cmd.Flags().StringVar(&o.SignatureRef, "signature", "", - "signature content or path or remote URL") - _ = cmd.MarkFlagFilename("signature", signatureExts...) - _ = cmd.Flags().MarkDeprecated("signature", "signatures are automatically fetched from the OCI registry during image verification") - - cmd.Flags().StringVar(&o.PayloadRef, "payload", "", - "payload path or remote URL") - // _ = cmd.MarkFlagFilename("payload") // no typical extensions - _ = cmd.Flags().MarkDeprecated("payload", "payload will always be verified from the bundle in future versions") - cmd.Flags().BoolVar(&o.LocalImage, "local-image", false, "whether the specified image is a path to an image saved locally via 'cosign save'") } @@ -154,11 +103,9 @@ type VerifyAttestationOptions struct { CommonVerifyOptions CommonVerifyOptions SecurityKey SecurityKeyOptions - Rekor RekorOptions CertVerify CertVerifyOptions Registry RegistryOptions Predicate PredicateRemoteOptions - SignatureDigest SignatureDigestOptions Policies []string LocalImage bool } @@ -168,14 +115,10 @@ var _ Interface = (*VerifyAttestationOptions)(nil) // AddFlags implements Interface func (o *VerifyAttestationOptions) AddFlags(cmd *cobra.Command) { o.SecurityKey.AddFlags(cmd) - o.Rekor.AddFlags(cmd) o.CertVerify.AddFlags(cmd) o.Registry.AddFlags(cmd) o.Predicate.AddFlags(cmd) o.CommonVerifyOptions.AddFlags(cmd) - o.SignatureDigest.AddFlags(cmd) - - _ = cmd.Flags().MarkDeprecated("rekor-url", "please use --bundle, which includes the Rekor inclusion proof") cmd.Flags().StringVar(&o.Key, "key", "", "path to the public key file, KMS URI or Kubernetes Secret") @@ -199,16 +142,11 @@ 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 SecurityKey SecurityKeyOptions CertVerify CertVerifyOptions - Rekor RekorOptions CommonVerifyOptions CommonVerifyOptions - SignatureDigest SignatureDigestOptions - - RFC3161TimestampPath string } var _ Interface = (*VerifyBlobOptions)(nil) @@ -216,30 +154,16 @@ var _ Interface = (*VerifyBlobOptions)(nil) // AddFlags implements Interface func (o *VerifyBlobOptions) AddFlags(cmd *cobra.Command) { o.SecurityKey.AddFlags(cmd) - o.Rekor.AddFlags(cmd) o.CertVerify.AddFlags(cmd) o.CommonVerifyOptions.AddFlags(cmd) - o.SignatureDigest.AddFlags(cmd) - - _ = cmd.Flags().MarkDeprecated("rekor-url", "please use --bundle, which includes the Rekor inclusion proof") cmd.Flags().StringVar(&o.Key, "key", "", "path to the public key file, KMS URI or Kubernetes Secret") _ = cmd.MarkFlagFilename("key", publicKeyExts...) - cmd.Flags().StringVar(&o.Signature, "signature", "", - "signature content or path or remote URL") - _ = cmd.MarkFlagFilename("signature", signatureExts...) - _ = cmd.Flags().MarkDeprecated("signature", "please use --bundle to provide a signature") - cmd.Flags().StringVar(&o.BundlePath, "bundle", "", "path to bundle FILE") _ = cmd.MarkFlagFilename("bundle", bundleExts...) - - cmd.Flags().StringVar(&o.RFC3161TimestampPath, "rfc3161-timestamp", "", - "path to RFC3161 timestamp FILE") - // _ = cmd.MarkFlagFilename("rfc3161-timestamp") // no typical extensions - _ = cmd.Flags().MarkDeprecated("rfc3161-timestamp", "please use --bundle to provide the output bundle location, which will include the signed timestamp") } // VerifyDockerfileOptions is the top level wrapper for the `dockerfile verify` command. @@ -260,20 +184,15 @@ func (o *VerifyDockerfileOptions) AddFlags(cmd *cobra.Command) { // VerifyBlobAttestationOptions is the top level wrapper for the `verify-blob-attestation` command. type VerifyBlobAttestationOptions struct { - Key string - SignaturePath string - BundlePath string + Key string + BundlePath string PredicateOptions CheckClaims bool SecurityKey SecurityKeyOptions CertVerify CertVerifyOptions - Rekor RekorOptions CommonVerifyOptions CommonVerifyOptions - SignatureDigest SignatureDigestOptions - - RFC3161TimestampPath string Digest string DigestAlg string @@ -285,22 +204,13 @@ var _ Interface = (*VerifyBlobOptions)(nil) func (o *VerifyBlobAttestationOptions) AddFlags(cmd *cobra.Command) { o.PredicateOptions.AddFlags(cmd) o.SecurityKey.AddFlags(cmd) - o.Rekor.AddFlags(cmd) o.CertVerify.AddFlags(cmd) o.CommonVerifyOptions.AddFlags(cmd) - o.SignatureDigest.AddFlags(cmd) - - _ = cmd.Flags().MarkDeprecated("rekor-url", "please use --bundle, which includes the Rekor inclusion proof") cmd.Flags().StringVar(&o.Key, "key", "", "path to the public key file, KMS URI or Kubernetes Secret") _ = cmd.MarkFlagFilename("key", publicKeyExts...) - cmd.Flags().StringVar(&o.SignaturePath, "signature", "", - "path to base64-encoded signature over attestation in DSSE format") - _ = cmd.MarkFlagFilename("signature", signatureExts...) - _ = cmd.Flags().MarkDeprecated("signature", "please use --bundle to provide a signature") - cmd.Flags().StringVar(&o.BundlePath, "bundle", "", "path to bundle FILE") _ = cmd.MarkFlagFilename("bundle", bundleExts...) @@ -308,11 +218,6 @@ func (o *VerifyBlobAttestationOptions) AddFlags(cmd *cobra.Command) { cmd.Flags().BoolVar(&o.CheckClaims, "check-claims", true, "if true, verifies the digest exists in the in-toto subject (using either the provided digest and digest algorithm or the provided blob's sha256 digest). If false, only the DSSE envelope is verified.") - cmd.Flags().StringVar(&o.RFC3161TimestampPath, "rfc3161-timestamp", "", - "path to RFC3161 timestamp FILE") - // _ = cmd.MarkFlagFilename("rfc3161-timestamp") // no typical extensions - _ = cmd.Flags().MarkDeprecated("rfc3161-timestamp", "please use --bundle to provide the output bundle location, which will include the signed timestamp") - cmd.Flags().StringVar(&o.Digest, "digest", "", "Digest to use for verifying in-toto subject (instead of providing a blob)") _ = cmd.RegisterFlagCompletionFunc("digest", cobra.NoFileCompletions) diff --git a/cmd/cosign/cli/sign.go b/cmd/cosign/cli/sign.go index 47ff4cf0e4a..0e0ca36122b 100644 --- a/cmd/cosign/cli/sign.go +++ b/cmd/cosign/cli/sign.go @@ -17,7 +17,6 @@ package cli import ( "fmt" - "os" "github.com/sigstore/cosign/v3/cmd/cosign/cli/generate" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" @@ -38,7 +37,7 @@ Make sure to sign the image by its digest (@sha256:...) rather than by tag (:latest) so that you actually sign what you think you're signing! This prevents race conditions or (worse) malicious tampering. `, - Example: ` cosign sign --key | [-a key=value] [--upload=true|false] [-f] [-r] + Example: ` cosign sign --key | [-a key=value] [--upload=true|false] [-y] [-r] # sign a container image with the Sigstore OIDC flow cosign sign @@ -88,20 +87,23 @@ race conditions or (worse) malicious tampering. Args: cobra.MinimumNArgs(1), PersistentPreRun: options.BindViper, PreRunE: func(_ *cobra.Command, _ []string) error { - if o.NewBundleFormat && !o.Upload && o.BundlePath == "" { - return fmt.Errorf("must enable upload to the OCI registry or specify a local --bundle path with --new-bundle-format") + if !o.Upload && o.BundlePath == "" { + return fmt.Errorf("must enable upload to the OCI registry or specify a local --bundle path") + } + var signType string + if o.Key == "" && !o.SecurityKey.Use { + signType = "keyless" + } else if o.IssueCertificate { + signType = "certificate-based" + } + if signType != "" { + if !o.UseSigningConfig && o.SigningConfigPath == "" { + return fmt.Errorf("%s signing requires a signing config (either from TUF via --use-signing-config or explicitly via a file with --signing-config)", signType) + } } return nil }, RunE: func(cmd *cobra.Command, args []string) error { - switch o.Attachment { - case "sbom": - fmt.Fprintln(os.Stderr, options.SBOMAttachmentDeprecation) - case "": - break - default: - return fmt.Errorf("specified image attachment %s not specified. Can be 'sbom'", o.Attachment) - } oidcClientSecret, err := o.OIDC.ClientSecret() if err != nil { return err @@ -112,12 +114,8 @@ race conditions or (worse) malicious tampering. PassFunc: generate.GetPass, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, - FulcioURL: o.Fulcio.URL, IDToken: o.Fulcio.IdentityToken, FulcioAuthFlow: o.Fulcio.AuthFlow, - InsecureSkipFulcioVerify: o.Fulcio.InsecureSkipFulcioVerify, - RekorURL: o.Rekor.URL, - OIDCIssuer: o.OIDC.Issuer, OIDCClientID: o.OIDC.ClientID, OIDCClientSecret: oidcClientSecret, OIDCRedirectURL: o.OIDC.RedirectURL, @@ -128,21 +126,15 @@ race conditions or (worse) malicious tampering. TSAClientCert: o.TSAClientCert, TSAClientKey: o.TSAClientKey, TSAServerName: o.TSAServerName, - TSAServerURL: o.TSAServerURL, IssueCertificateForExistingKey: o.IssueCertificate, - NewBundleFormat: o.NewBundleFormat, } if err := signcommon.LoadTrustedMaterialAndSigningConfig(cmd.Context(), &ko, o.UseSigningConfig, o.SigningConfigPath, - o.Rekor.URL, o.Fulcio.URL, o.OIDC.Issuer, o.TSAServerURL, o.TrustedRootPath, o.TlogUpload, - o.NewBundleFormat, "", o.Key, o.IssueCertificate, o.Output, "", o.OutputCertificate, o.OutputPayload, o.OutputSignature, ""); err != nil { + o.TrustedRootPath, o.Key); err != nil { return err } if err := sign.SignCmd(cmd.Context(), ro, ko, *o, args); err != nil { - if o.Attachment == "" { - return fmt.Errorf("signing %v: %w", args, err) - } - return fmt.Errorf("signing attachment %s for image %v: %w", o.Attachment, args, err) + return fmt.Errorf("signing %v: %w", args, err) } return nil }, diff --git a/cmd/cosign/cli/sign/sign.go b/cmd/cosign/cli/sign/sign.go index 8fcde26f271..d90d0d3e638 100644 --- a/cmd/cosign/cli/sign/sign.go +++ b/cmd/cosign/cli/sign/sign.go @@ -16,37 +16,22 @@ package sign import ( - "bytes" "context" - "encoding/base64" - "encoding/json" "fmt" - "net/http" "os" - "path/filepath" "strings" - "time" "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" intotov1 "github.com/in-toto/attestation/go/v1" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" - "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa/client" "github.com/sigstore/cosign/v3/internal/ui" - "github.com/sigstore/cosign/v3/pkg/cosign" - cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" - cremote "github.com/sigstore/cosign/v3/pkg/cosign/remote" "github.com/sigstore/cosign/v3/pkg/oci" "github.com/sigstore/cosign/v3/pkg/oci/mutate" ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" - "github.com/sigstore/cosign/v3/pkg/oci/static" "github.com/sigstore/cosign/v3/pkg/oci/walk" "github.com/sigstore/cosign/v3/pkg/types" - protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" - "github.com/sigstore/sigstore-go/pkg/sign" - "github.com/sigstore/sigstore/pkg/signature" - sigPayload "github.com/sigstore/sigstore/pkg/signature/payload" "google.golang.org/protobuf/encoding/protojson" "google.golang.org/protobuf/types/known/structpb" @@ -73,16 +58,6 @@ func SignCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpts, s ctx, cancel := context.WithTimeout(ctx, ro.Timeout) defer cancel() - var staticPayload []byte - var err error - if signOpts.PayloadPath != "" { - ui.Infof(ctx, "Using payload from: %s", signOpts.PayloadPath) - staticPayload, err = os.ReadFile(filepath.Clean(signOpts.PayloadPath)) - if err != nil { - return fmt.Errorf("payload from file: %w", err) - } - } - // Set up an ErrDone consideration to return along "success" paths var ErrDone error if !signOpts.Recursive { @@ -103,23 +78,9 @@ func SignCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpts, s if err != nil { return err } - ref, err = GetAttachedImageRef(ref, signOpts.Attachment, opts...) - if err != nil { - return fmt.Errorf("unable to resolve attachment %s for image %s", signOpts.Attachment, inputImg) - } if digest, ok := ref.(name.Digest); ok && !signOpts.Recursive { - se, err := ociremote.SignedEntity(ref, opts...) - if _, isEntityNotFoundErr := err.(*ociremote.EntityNotFoundError); isEntityNotFoundErr { - se = ociremote.SignedUnknown(digest) - } else if err != nil { - return fmt.Errorf("accessing image: %w", err) - } - if signOpts.NewBundleFormat { - err = signDigestBundle(ctx, digest, ko, signOpts, annotations) - } else { - err = signDigest(ctx, digest, staticPayload, ko, signOpts, annotations, se) - } + err = signDigestBundle(ctx, digest, ko, signOpts, annotations) if err != nil { return fmt.Errorf("signing digest: %w", err) } @@ -138,11 +99,7 @@ func SignCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpts, s return fmt.Errorf("computing digest: %w", err) } digest := ref.Context().Digest(d.String()) - if signOpts.NewBundleFormat { - err = signDigestBundle(ctx, digest, ko, signOpts, annotations) - } else { - err = signDigest(ctx, digest, staticPayload, ko, signOpts, annotations, se) - } + err = signDigestBundle(ctx, digest, ko, signOpts, annotations) if err != nil { return fmt.Errorf("signing digest: %w", err) } @@ -198,17 +155,19 @@ func signDigestBundle(ctx context.Context, digest name.Digest, ko options.KeyOpt } if ko.SigningConfig == nil { - shouldUpload, err := signcommon.ShouldUploadToTlog(ctx, ko, digest, signOpts.TlogUpload) - if err != nil { - return fmt.Errorf("should upload to tlog: %w", err) - } - ko.SigningConfig, err = signcommon.NewSigningConfigFromKeyOpts(ko, shouldUpload) - if err != nil { - return fmt.Errorf("creating signing config: %w", err) - } + ko.SigningConfig = signcommon.NewEmptySigningConfig() + } + + shouldUpload, err := signcommon.ShouldUploadToTlog(ctx, ko, digest, len(ko.SigningConfig.RekorLogURLs()) > 0) + if err != nil { + return fmt.Errorf("should upload to tlog: %w", err) } - bundleBytes, _, _, err := signcommon.NewAttestationBundle(ctx, ko, signOpts.Cert, signOpts.CertChain, bundleOpts, ko.SigningConfig, ko.TrustedMaterial) + if !shouldUpload { + ko.SigningConfig.WithRekorLogURLs() + } + + bundleBytes, err := signcommon.NewAttestationBundle(ctx, ko, signOpts.Cert, signOpts.CertChain, bundleOpts, ko.SigningConfig, ko.TrustedMaterial) if err != nil { return err } @@ -229,249 +188,3 @@ func signDigestBundle(ctx context.Context, digest name.Digest, ko options.KeyOpt return nil } - -func signDigest(ctx context.Context, digest name.Digest, payload []byte, ko options.KeyOpts, signOpts options.SignOptions, - annotations map[string]interface{}, se oci.SignedEntity) error { - var err error - var payloads [][]byte - // The payload can be passed to skip generation. - if len(payload) == 0 { - identities := signOpts.SignContainerIdentities - if len(identities) == 0 { - identities = append(identities, "") - } - for _, identity := range identities { - payload, err = (&sigPayload.Cosign{ - Image: digest, - ClaimedIdentity: identity, - Annotations: annotations, - }).MarshalJSON() - if err != nil { - return fmt.Errorf("payload: %w", err) - } - payloads = append(payloads, payload) - } - } else { - payloads = append(payloads, payload) - } - - if ko.SigningConfig == nil { - shouldUpload, err := signcommon.ShouldUploadToTlog(ctx, ko, digest, signOpts.TlogUpload) - if err != nil { - return fmt.Errorf("should upload to tlog: %w", err) - } - ko.SigningConfig, err = signcommon.NewSigningConfigFromKeyOpts(ko, shouldUpload) - if err != nil { - return fmt.Errorf("creating signing config: %w", err) - } - } - - keypair, certBytes, chainBytes, idToken, err := signcommon.GetKeypairAndToken(ctx, ko, signOpts.Cert, signOpts.CertChain) - if err != nil { - return fmt.Errorf("getting keypair and token: %w", err) - } - if closer, ok := keypair.(interface{ Close() }); ok { - defer closer.Close() - } - - var tsaClientTransport http.RoundTripper - if ko.TSAClientCACert != "" || (ko.TSAClientCert != "" && ko.TSAClientKey != "") { - tsaClientTransport, err = client.GetHTTPTransport(ko.TSAClientCACert, ko.TSAClientCert, ko.TSAClientKey, ko.TSAServerName, 30*time.Second) - if err != nil { - return fmt.Errorf("getting TSA client transport: %w", err) - } - } - var certProvider sign.CertificateProvider - if idToken != "" { - certProvider, err = cbundle.NewCachingFulcioProvider(ko.SigningConfig) - if err != nil { - return fmt.Errorf("creating caching Fulcio provider: %w", err) - } - } - - cbundleOpts := cbundle.SignOptions{ - TSAClientTransport: tsaClientTransport, - CertificateProvider: certProvider, - } - - ociSigs := make([]oci.Signature, len(payloads)) - b64sigs := make([]string, len(payloads)) - - var leafCertPem []byte - - for i, payload := range payloads { - content := &sign.PlainData{ - Data: payload, - } - - bundleBytes, err := cbundle.SignData(ctx, content, keypair, idToken, certBytes, chainBytes, ko.SigningConfig, ko.TrustedMaterial, cbundleOpts) - if err != nil { - return fmt.Errorf("signing bundle: %w", err) - } - - var pb protobundle.Bundle - if err := protojson.Unmarshal(bundleBytes, &pb); err != nil { - return fmt.Errorf("unmarshalling bundle: %w", err) - } - - bundleComponents, err := signcommon.ExtractComponentsFromProtoBundle(&pb) - if err != nil { - return fmt.Errorf("extracting components from bundle: %w", err) - } - - certPem, chainPem := signcommon.EncodeCertificatesToPEM(bundleComponents.Certificates) - if i == 0 { - leafCertPem = certPem - } - - b64sig := base64.StdEncoding.EncodeToString(bundleComponents.Signature) - b64sigs[i] = b64sig - - var opts []static.Option - if certPem != nil { - opts = append(opts, static.WithCertChain(certPem, chainPem)) - } - - if len(bundleComponents.RFC3161Timestamps) > 0 { - opts = append(opts, static.WithRFC3161Timestamp(cbundle.TimestampToRFC3161Timestamp(bundleComponents.RFC3161Timestamps[0].GetSignedTimestamp()))) - } - - if len(bundleComponents.RekorEntries) > 0 { - opts = append(opts, static.WithBundle(signcommon.RekorBundleFromProtoTlogEntry(bundleComponents.RekorEntries[0]))) - } - - ociSig, err := static.NewSignature(payload, b64sig, opts...) - if err != nil { - return fmt.Errorf("creating signature: %w", err) - } - - ociSigs[i] = ociSig - } - - outputSignature := signOpts.OutputSignature - if outputSignature != "" { - // Add digest to suffix to differentiate each image during recursive signing - if signOpts.Recursive { - outputSignature = fmt.Sprintf("%s-%s", outputSignature, strings.Replace(digest.DigestStr(), ":", "-", 1)) - } - if err := os.WriteFile(outputSignature, []byte(strings.Join(b64sigs, "\n")), 0600); err != nil { - return fmt.Errorf("create signature file: %w", err) - } - } - outputPayload := signOpts.OutputPayload - if outputPayload != "" { - // Add digest to suffix to differentiate each image during recursive signing - if signOpts.Recursive { - outputPayload = fmt.Sprintf("%s-%s", outputPayload, strings.Replace(digest.DigestStr(), ":", "-", 1)) - } - if err := os.WriteFile(outputPayload, bytes.Join(payloads, []byte("\n")), 0600); err != nil { // #nosec G703 -- user-supplied output path is intentional - return fmt.Errorf("create payload file: %w", err) - } - } - - if signOpts.OutputCertificate != "" { - var outBytes []byte - if len(leafCertPem) > 0 { - outBytes = leafCertPem - } else { - pubPem, err := keypair.GetPublicKeyPem() - if err != nil { - return fmt.Errorf("getting public key pem: %w", err) - } - outBytes = []byte(pubPem) - } - - if err := os.WriteFile(signOpts.OutputCertificate, outBytes, 0600); err != nil { - return fmt.Errorf("create certificate file: %w", err) - } - // TODO: maybe accept a --b64 flag as well? - ui.Infof(ctx, "Certificate wrote in the file %s", signOpts.OutputCertificate) - } - - if ko.BundlePath != "" { - var contents [][]byte - for _, ociSig := range ociSigs { - signedPayload, err := fetchLocalSignedPayload(ociSig) - if err != nil { - return fmt.Errorf("failed to fetch signed payload: %w", err) - } - - content, err := json.Marshal(signedPayload) - if err != nil { - return fmt.Errorf("failed to marshal signed payload: %w", err) - } - contents = append(contents, content) - } - if err := os.WriteFile(ko.BundlePath, bytes.Join(contents, []byte("\n")), 0600); err != nil { - return fmt.Errorf("create bundle file: %w", err) - } - ui.Infof(ctx, "Wrote bundle to file %s", ko.BundlePath) - } - - if !signOpts.Upload { - return nil - } - - hashAlgo := signcommon.ProtoHashAlgoToHash(keypair.GetHashAlgorithm()) - ddVerifier, err := signature.LoadVerifier(keypair.GetPublicKey(), hashAlgo) - if err != nil { - return fmt.Errorf("loading verifier: %w", err) - } - dd := cremote.NewDupeDetector(ddVerifier) - - // Attach the signature to the entity. - var newSE oci.SignedEntity - for _, ociSig := range ociSigs { - newSE, err = mutate.AttachSignatureToEntity(se, ociSig, mutate.WithDupeDetector(dd), mutate.WithRecordCreationTimestamp(signOpts.RecordCreationTimestamp)) - if err != nil { - return err - } - se = newSE - } - - // Publish the signatures associated with this entity - walkOpts, err := signOpts.Registry.ClientOpts(ctx) - if err != nil { - return fmt.Errorf("constructing client options: %w", err) - } - - // Check if we are overriding the signatures repository location - repo, _ := ociremote.GetEnvTargetRepository() - if repo.RepositoryStr() == "" { - ui.Infof(ctx, "Pushing signature to: %s", digest.Repository) - } else { - ui.Infof(ctx, "Pushing signature to: %s", repo.RepositoryStr()) - } - - // Publish the signatures associated with this entity (using OCI 1.1+ behavior) - if signOpts.RegistryExperimental.RegistryReferrersMode == options.RegistryReferrersModeOCI11 { - return ociremote.WriteSignaturesExperimentalOCI(digest, newSE, walkOpts...) - } - - // Publish the signatures associated with this entity - return ociremote.WriteSignatures(digest.Repository, newSE, walkOpts...) -} - -func fetchLocalSignedPayload(sig oci.Signature) (*cosign.LocalSignedPayload, error) { - signedPayload := &cosign.LocalSignedPayload{} - var err error - - signedPayload.Base64Signature, err = sig.Base64Signature() - if err != nil { - return nil, err - } - - sigCert, err := sig.Cert() - if err != nil { - return nil, err - } - if sigCert != nil { - signedPayload.Cert = base64.StdEncoding.EncodeToString(sigCert.Raw) - } - - signedPayload.Bundle, err = sig.Bundle() - if err != nil { - return nil, err - } - return signedPayload, nil -} diff --git a/cmd/cosign/cli/sign/sign_blob.go b/cmd/cosign/cli/sign/sign_blob.go index dc394f10676..5f4609a1f85 100644 --- a/cmd/cosign/cli/sign/sign_blob.go +++ b/cmd/cosign/cli/sign/sign_blob.go @@ -18,8 +18,6 @@ package sign import ( "context" "crypto" - "encoding/base64" - "encoding/json" "fmt" "io" "os" @@ -34,10 +32,8 @@ import ( "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa/client" "github.com/sigstore/cosign/v3/internal/ui" cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" - protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" "github.com/sigstore/sigstore-go/pkg/sign" "github.com/sigstore/sigstore/pkg/signature" - "google.golang.org/protobuf/encoding/protojson" ) func getPayload(ctx context.Context, payloadPath string, hashFunction crypto.Hash) (internal.HashReader, func() error, error) { @@ -53,26 +49,19 @@ func getPayload(ctx context.Context, payloadPath string, hashFunction crypto.Has } // nolint -func SignBlobCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpts, payloadPath, certPath, certChainPath string, b64 bool, outputSignature string, outputCertificate string, tlogUpload bool) ([]byte, error) { +func SignBlobCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpts, payloadPath, certPath, certChainPath string) error { var payload internal.HashReader ctx, cancel := context.WithTimeout(ctx, ro.Timeout) defer cancel() - var shouldUpload bool - var err error - if ko.SigningConfig == nil { - shouldUpload, err = signcommon.ShouldUploadToTlog(ctx, ko, nil, tlogUpload) - if err != nil { - return nil, fmt.Errorf("upload to tlog: %w", err) - } - ko.SigningConfig, err = signcommon.NewSigningConfigFromKeyOpts(ko, shouldUpload) - if err != nil { - return nil, fmt.Errorf("creating signing config: %w", err) - } - } else { - shouldUpload = len(ko.SigningConfig.RekorLogURLs()) > 0 + ko.SigningConfig = signcommon.NewEmptySigningConfig() + } + + shouldUpload, err := signcommon.ShouldUploadToTlog(ctx, ko, nil, len(ko.SigningConfig.RekorLogURLs()) > 0) + if err != nil { + return fmt.Errorf("should upload to tlog: %w", err) } if !shouldUpload { @@ -84,7 +73,7 @@ func SignBlobCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpt keypair, certBytes, chainBytes, idToken, err := signcommon.GetKeypairAndToken(ctx, ko, certPath, certChainPath) if err != nil { - return nil, fmt.Errorf("getting keypair and token: %w", err) + return fmt.Errorf("getting keypair and token: %w", err) } if closer, ok := keypair.(interface{ Close() }); ok { defer closer.Close() @@ -93,23 +82,13 @@ func SignBlobCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpt hashFunction := signcommon.ProtoHashAlgoToHash(keypair.GetHashAlgorithm()) payload, closePayload, err := getPayload(ctx, payloadPath, hashFunction) if err != nil { - return nil, fmt.Errorf("getting payload: %w", err) + return fmt.Errorf("getting payload: %w", err) } defer closePayload() - if hashFunction != crypto.SHA256 && !ko.NewBundleFormat && (shouldUpload || (!ko.Sk && ko.KeyRef == "")) { - ui.Infof(ctx, "Non SHA256 hash function is not supported for old bundle format. Use --new-bundle-format to use the new bundle format or use different signing key/algorithm.") - if !ko.SkipConfirmation { - if err := ui.ConfirmContinue(ctx); err != nil { - return nil, err - } - } - ui.Infof(ctx, "Continuing with non SHA256 hash function and old bundle format") - } - data, err := io.ReadAll(&payload) if err != nil { - return nil, fmt.Errorf("reading payload: %w", err) + return fmt.Errorf("reading payload: %w", err) } content := &sign.PlainData{ Data: data, @@ -119,94 +98,18 @@ func SignBlobCmd(ctx context.Context, ro *options.RootOptions, ko options.KeyOpt if ko.TSAClientCACert != "" || (ko.TSAClientCert != "" && ko.TSAClientKey != "") { tsaClientTransport, err = client.GetHTTPTransport(ko.TSAClientCACert, ko.TSAClientCert, ko.TSAClientKey, ko.TSAServerName, 30*time.Second) if err != nil { - return nil, fmt.Errorf("getting TSA client transport: %w", err) + return fmt.Errorf("getting TSA client transport: %w", err) } } signOpts := cbundle.SignOptions{TSAClientTransport: tsaClientTransport} bundleBytes, err := cbundle.SignData(ctx, content, keypair, idToken, certBytes, chainBytes, ko.SigningConfig, ko.TrustedMaterial, signOpts) if err != nil { - return nil, fmt.Errorf("signing bundle: %w", err) - } - - if ko.NewBundleFormat { - if err := os.WriteFile(ko.BundlePath, bundleBytes, 0600); err != nil { - return nil, fmt.Errorf("create bundle file: %w", err) - } - ui.Infof(ctx, "Wrote bundle to file %s", ko.BundlePath) - return nil, nil - } - - var pb protobundle.Bundle - if err := protojson.Unmarshal(bundleBytes, &pb); err != nil { - return nil, fmt.Errorf("unmarshalling bundle: %w", err) - } - - bundleComponents, err := signcommon.ExtractComponentsFromProtoBundle(&pb) - if err != nil { - return nil, fmt.Errorf("extracting components from bundle: %w", err) - } - - if ko.BundlePath != "" { - contents, err := signcommon.NewLegacyBundleFromProtoBundleComponents(bundleComponents) - if err != nil { - return nil, fmt.Errorf("creating legacy bundle: %w", err) - } - - if err := os.WriteFile(ko.BundlePath, contents, 0600); err != nil { - return nil, fmt.Errorf("create bundle file: %w", err) - } - ui.Infof(ctx, "Wrote bundle to file %s", ko.BundlePath) - } - - if outputSignature != "" { - bts := bundleComponents.Signature - if b64 { - bts = []byte(base64.StdEncoding.EncodeToString(bundleComponents.Signature)) - } - if err := os.WriteFile(outputSignature, bts, 0600); err != nil { - return nil, fmt.Errorf("create signature file: %w", err) - } - ui.Infof(ctx, "Wrote signature to file %s", outputSignature) - } else { - bts := bundleComponents.Signature - if b64 { - bts = []byte(base64.StdEncoding.EncodeToString(bundleComponents.Signature)) - fmt.Println(string(bts)) - } else { - if _, err := os.Stdout.Write(bts); err != nil { - return nil, err - } - } - } - - if outputCertificate != "" && len(bundleComponents.Certificates) > 0 { - certPem, _ := signcommon.EncodeCertificatesToPEM(bundleComponents.Certificates) - var bts []byte - if b64 { - bts = []byte(base64.StdEncoding.EncodeToString(certPem)) - } else { - bts = certPem - } - if err := os.WriteFile(outputCertificate, bts, 0600); err != nil { - return nil, fmt.Errorf("create certificate file: %w", err) - } - ui.Infof(ctx, "Wrote certificate to file %s", outputCertificate) - } - - if len(bundleComponents.RFC3161Timestamps) > 0 && ko.RFC3161TimestampPath != "" { - legacyTimestamp := cbundle.TimestampToRFC3161Timestamp(bundleComponents.RFC3161Timestamps[0].GetSignedTimestamp()) - ts, err := json.Marshal(legacyTimestamp) - if err != nil { - return nil, fmt.Errorf("marshalling timestamp: %w", err) - } - if err := os.WriteFile(ko.RFC3161TimestampPath, ts, 0600); err != nil { - return nil, fmt.Errorf("create timestamp file: %w", err) - } - ui.Infof(ctx, "Wrote timestamp to file %s", ko.RFC3161TimestampPath) + return fmt.Errorf("signing bundle: %w", err) } - if b64 { - return []byte(base64.StdEncoding.EncodeToString(bundleComponents.Signature)), nil + if err := os.WriteFile(ko.BundlePath, bundleBytes, 0600); err != nil { + return fmt.Errorf("create bundle file: %w", err) } - return bundleComponents.Signature, nil + ui.Infof(ctx, "Wrote bundle to file %s", ko.BundlePath) + return nil } diff --git a/cmd/cosign/cli/sign/sign_blob_test.go b/cmd/cosign/cli/sign/sign_blob_test.go index 9a4fd12343f..43c9153b848 100644 --- a/cmd/cosign/cli/sign/sign_blob_test.go +++ b/cmd/cosign/cli/sign/sign_blob_test.go @@ -47,17 +47,7 @@ func TestSignBlobCmd(t *testing.T) { keyOpts := options.KeyOpts{KeyRef: keyRef, BundlePath: bundlePath} // Test happy path - _, err := SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, "", "", true, "", "", false) - if err != nil { - t.Fatalf("unexpected error %v", err) - } - - // Test file outputs - keyOpts.NewBundleFormat = true - sigPath := filepath.Join(td, "output.sig") - certPath := filepath.Join(td, "output.pem") - _, err = SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, "", "", false, sigPath, certPath, false) - if err != nil { + if err := SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, "", ""); err != nil { t.Fatalf("unexpected error %v", err) } @@ -81,8 +71,7 @@ func TestSignBlobCmd(t *testing.T) { certPrivKeyRef := writeFile(t, td, string(pemBytes), "certkey.pem") keyOpts.KeyRef = certPrivKeyRef - _, err = SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, signCertPath, "", false, "", "", false) - if err != nil { + if err := SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, signCertPath, ""); err != nil { t.Fatalf("unexpected error %v", err) } @@ -119,14 +108,17 @@ func TestSignBlobCmd(t *testing.T) { t.Fatal(err) } keyOpts.SigningConfig = sc - sigBytes, err := SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, "", "", true, "", "", true) - if err != nil { + if err := SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, "", ""); err != nil { t.Fatalf("unexpected error %v", err) } - decodedSig, err := base64.StdEncoding.DecodeString(string(sigBytes)) - if err != nil { - t.Fatalf("failed to decode base64 signature: %v", err) + var b1 struct { + MessageSignature struct { + Signature string `json:"signature"` + } `json:"messageSignature"` } + bytes1, _ := os.ReadFile(bundlePath) + json.Unmarshal(bytes1, &b1) + decodedSig, _ := base64.StdEncoding.DecodeString(b1.MessageSignature.Signature) if !ed25519.Verify(pub, blob, decodedSig) { errString := "expected ed25519 signature" if ed25519.VerifyWithOptions(pub, blob, decodedSig, &ed25519.Options{Hash: crypto.SHA512}) == nil { @@ -137,14 +129,17 @@ func TestSignBlobCmd(t *testing.T) { // Test signing using Ed25519 key with default signing config and no transparency log upload keyOpts = options.KeyOpts{KeyRef: edKeyRef, BundlePath: bundlePath} - sigBytes, err = SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, "", "", true, "", "", false) - if err != nil { + if err := SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, "", ""); err != nil { t.Fatalf("unexpected error %v", err) } - decodedSig, err = base64.StdEncoding.DecodeString(string(sigBytes)) - if err != nil { - t.Fatalf("failed to decode base64 signature: %v", err) + var b2 struct { + MessageSignature struct { + Signature string `json:"signature"` + } `json:"messageSignature"` } + bytes2, _ := os.ReadFile(bundlePath) + json.Unmarshal(bytes2, &b2) + decodedSig, _ = base64.StdEncoding.DecodeString(b2.MessageSignature.Signature) if !ed25519.Verify(pub, blob, decodedSig) { errString := "expected ed25519 signature" if ed25519.VerifyWithOptions(pub, blob, decodedSig, &ed25519.Options{Hash: crypto.SHA512}) == nil { @@ -162,43 +157,3 @@ func writeFile(t *testing.T, td string, blob string, name string) string { } return blobPath } - -func TestSignBlobCmd_LegacyBundleNoCert(t *testing.T) { - td := t.TempDir() - bundlePath := filepath.Join(td, "legacy-bundle.json") - - keys, _ := cosign.GenerateKeyPair(nil) - keyRef := writeFile(t, td, string(keys.PrivateBytes), "key.pem") - - blob := []byte("foo") - blobPath := writeFile(t, td, string(blob), "foo.txt") - - rootOpts := &options.RootOptions{} - keyOpts := options.KeyOpts{ - KeyRef: keyRef, - BundlePath: bundlePath, - NewBundleFormat: false, - } - - _, err := SignBlobCmd(t.Context(), rootOpts, keyOpts, blobPath, "", "", true, "", "", false) - if err != nil { - t.Fatalf("unexpected error: %v", err) - } - - bundleBytes, err := os.ReadFile(bundlePath) - if err != nil { - t.Fatalf("failed to read written bundle file: %v", err) - } - - var payload cosign.LocalSignedPayload - if err := json.Unmarshal(bundleBytes, &payload); err != nil { - t.Fatalf("failed to unmarshal legacy bundle: %v", err) - } - - if payload.Cert != "" { - t.Fatalf("expected empty cert field in legacy bundle when signing with a key without certificate, got: %q", payload.Cert) - } - if payload.Base64Signature == "" { - t.Fatal("expected non-empty Base64Signature in legacy bundle") - } -} diff --git a/cmd/cosign/cli/signblob.go b/cmd/cosign/cli/signblob.go index 634394159b7..e8023be2aed 100644 --- a/cmd/cosign/cli/signblob.go +++ b/cmd/cosign/cli/signblob.go @@ -17,7 +17,6 @@ package cli import ( "fmt" - "os" "strings" "github.com/sigstore/cosign/v3/cmd/cosign/cli/generate" @@ -26,35 +25,33 @@ import ( "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/spf13/cobra" - "github.com/spf13/viper" ) func SignBlob() *cobra.Command { o := &options.SignBlobOptions{} - viper.RegisterAlias("output", "output-signature") cmd := &cobra.Command{ Use: "sign-blob", - Short: "Sign the supplied blob, outputting the base64-encoded signature to stdout", - Example: ` cosign sign-blob --key | + Short: "Sign the supplied blob, outputting the bundle to a file", + Example: ` cosign sign-blob --key | --bundle # sign a blob with a local key pair file - cosign sign-blob --key cosign.key + cosign sign-blob --key cosign.key --bundle # sign a blob with a key stored in an environment variable - cosign sign-blob --key env://[ENV_VAR] + cosign sign-blob --key env://[ENV_VAR] --bundle # sign a blob with a key pair stored in Azure Key Vault - cosign sign-blob --key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] + cosign sign-blob --key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] --bundle # sign a blob with a key pair stored in AWS KMS - cosign sign-blob --key awskms://[ENDPOINT]/[ID/ALIAS/ARN] + cosign sign-blob --key awskms://[ENDPOINT]/[ID/ALIAS/ARN] --bundle # sign a blob with a key pair stored in Google Cloud KMS - cosign sign-blob --key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY] + cosign sign-blob --key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY] --bundle # sign a blob with a key pair stored in Hashicorp Vault - cosign sign-blob --key hashivault://[KEY] `, + cosign sign-blob --key hashivault://[KEY] --bundle `, Args: cobra.MinimumNArgs(1), PersistentPreRun: options.BindViper, PreRunE: func(_ *cobra.Command, _ []string) error { @@ -62,8 +59,20 @@ func SignBlob() *cobra.Command { return &options.KeyParseError{} } - if o.NewBundleFormat && o.BundlePath == "" { - return fmt.Errorf("must specify --bundle with --new-bundle-format") + if o.BundlePath == "" { + return fmt.Errorf("please specify --bundle") + } + + var signType string + if o.Key == "" && !o.SecurityKey.Use { + signType = "keyless" + } else if o.IssueCertificate { + signType = "certificate-based" + } + if signType != "" { + if !o.UseSigningConfig && o.SigningConfigPath == "" { + return fmt.Errorf("%s signing requires a signing config (either from TUF via --use-signing-config or explicitly via a file with --signing-config)", signType) + } } // Check if the algorithm is in the list of supported algorithms @@ -93,43 +102,29 @@ func SignBlob() *cobra.Command { PassFunc: generate.GetPass, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, - FulcioURL: o.Fulcio.URL, IDToken: o.Fulcio.IdentityToken, FulcioAuthFlow: o.Fulcio.AuthFlow, - InsecureSkipFulcioVerify: o.Fulcio.InsecureSkipFulcioVerify, - RekorURL: o.Rekor.URL, - OIDCIssuer: o.OIDC.Issuer, OIDCClientID: o.OIDC.ClientID, OIDCClientSecret: oidcClientSecret, OIDCRedirectURL: o.OIDC.RedirectURL, OIDCDisableProviders: o.OIDC.DisableAmbientProviders, + OIDCProvider: o.OIDC.Provider, BundlePath: o.BundlePath, - NewBundleFormat: o.NewBundleFormat, SkipConfirmation: o.SkipConfirmation, TSAClientCACert: o.TSAClientCACert, TSAClientCert: o.TSAClientCert, TSAClientKey: o.TSAClientKey, TSAServerName: o.TSAServerName, - TSAServerURL: o.TSAServerURL, - RFC3161TimestampPath: o.RFC3161TimestampPath, IssueCertificateForExistingKey: o.IssueCertificate, SigningAlgorithm: o.SigningAlgorithm, } if err := signcommon.LoadTrustedMaterialAndSigningConfig(cmd.Context(), &ko, o.UseSigningConfig, o.SigningConfigPath, - o.Rekor.URL, o.Fulcio.URL, o.OIDC.Issuer, o.TSAServerURL, o.TrustedRootPath, o.TlogUpload, - o.NewBundleFormat, o.BundlePath, o.Key, o.IssueCertificate, - o.Output, "", o.OutputCertificate, "", o.OutputSignature, o.RFC3161TimestampPath); err != nil { + o.TrustedRootPath, o.Key); err != nil { return err } for _, blob := range args { - // TODO: remove when the output flag has been deprecated - if o.Output != "" { - fmt.Fprintln(os.Stderr, "WARNING: the '--output' flag is deprecated and will be removed in the future. Use '--output-signature'") - o.OutputSignature = o.Output - } - - if _, err := sign.SignBlobCmd(cmd.Context(), ro, ko, blob, o.Cert, o.CertChain, o.Base64Output, o.OutputSignature, o.OutputCertificate, o.TlogUpload); err != nil { + if err := sign.SignBlobCmd(cmd.Context(), ro, ko, blob, o.Cert, o.CertChain); err != nil { return fmt.Errorf("signing %s: %w", blob, err) } } diff --git a/cmd/cosign/cli/signcommon/common.go b/cmd/cosign/cli/signcommon/common.go index 8f69f1ecbc1..a826df97186 100644 --- a/cmd/cosign/cli/signcommon/common.go +++ b/cmd/cosign/cli/signcommon/common.go @@ -40,7 +40,6 @@ import ( "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign" cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" - "github.com/sigstore/cosign/v3/pkg/cosign/env" "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" @@ -150,7 +149,7 @@ func shouldUploadToTlog(ctx context.Context, ko options.KeyOpts, ref name.Refere // Check if the image is public (no auth in Get) if _, err := remote.Get(ref, remote.WithContext(ctx)); err != nil { - ui.Warnf(ctx, "%q appears to be a private repository, please confirm uploading to the transparency log at %q", ref.Context().String(), ko.RekorURL) + ui.Warnf(ctx, "%q appears to be a private repository, please confirm uploading to the transparency log", ref.Context().String()) if ui.ConfirmContinue(ctx) != nil { ui.Infof(ctx, "not uploading to transparency log") return false @@ -362,10 +361,10 @@ type CommonBundleOpts struct { } // NewAttestationBundle uses signing config and trusted root to sign an attestation and create a bundle. -func NewAttestationBundle(ctx context.Context, ko options.KeyOpts, cert, certChain string, bundleOpts CommonBundleOpts, signingConfig *root.SigningConfig, trustedMaterial root.TrustedMaterial) ([]byte, crypto.PublicKey, pb_go_v1.HashAlgorithm, error) { +func NewAttestationBundle(ctx context.Context, ko options.KeyOpts, cert, certChain string, bundleOpts CommonBundleOpts, signingConfig *root.SigningConfig, trustedMaterial root.TrustedMaterial) ([]byte, error) { keypair, certBytes, chainBytes, idToken, err := GetKeypairAndToken(ctx, ko, cert, certChain) if err != nil { - return nil, nil, pb_go_v1.HashAlgorithm_HASH_ALGORITHM_UNSPECIFIED, fmt.Errorf("getting keypair and token: %w", err) + return nil, fmt.Errorf("getting keypair and token: %w", err) } if closer, ok := keypair.(interface{ Close() }); ok { defer closer.Close() @@ -380,17 +379,17 @@ func NewAttestationBundle(ctx context.Context, ko options.KeyOpts, cert, certCha if ko.TSAClientCACert != "" || (ko.TSAClientCert != "" && ko.TSAClientKey != "") { tsaClientTransport, err = client.GetHTTPTransport(ko.TSAClientCACert, ko.TSAClientCert, ko.TSAClientKey, ko.TSAServerName, 30*time.Second) if err != nil { - return nil, nil, pb_go_v1.HashAlgorithm_HASH_ALGORITHM_UNSPECIFIED, fmt.Errorf("getting TSA client transport: %w", err) + return nil, fmt.Errorf("getting TSA client transport: %w", err) } } signOpts := cbundle.SignOptions{TSAClientTransport: tsaClientTransport} bundle, err := cbundle.SignData(ctx, content, keypair, idToken, certBytes, chainBytes, signingConfig, trustedMaterial, signOpts) if err != nil { - return nil, nil, pb_go_v1.HashAlgorithm_HASH_ALGORITHM_UNSPECIFIED, fmt.Errorf("signing bundle: %w", err) + return nil, fmt.Errorf("signing bundle: %w", err) } - return bundle, keypair.GetPublicKey(), keypair.GetHashAlgorithm(), nil + return bundle, nil } type BundleComponents struct { @@ -425,30 +424,13 @@ func ParseSignatureAlgorithmFlag(signingAlgorithm string) (pb_go_v1.PublicKeyDet // LoadTrustedMaterialAndSigningConfig loads the trusted material and signing config from the given options. func LoadTrustedMaterialAndSigningConfig(ctx context.Context, ko *options.KeyOpts, useSigningConfig bool, signingConfigPath string, - rekorURL, fulcioURL, oidcIssuer, tsaServerURL, trustedRootPath string, - tlogUpload bool, newBundleFormat bool, bundlePath string, keyRef string, issueCertificate bool, - output, outputAttestation, outputCertificate, outputPayload, outputSignature, outputTimestamp string) error { + trustedRootPath string, keyRef string) error { var err error - // If a signing config is used, then service URLs cannot be specified - if (useSigningConfig || signingConfigPath != "") && - ((rekorURL != "" && rekorURL != options.DefaultRekorURL) || - (fulcioURL != "" && fulcioURL != options.DefaultFulcioURL) || - (oidcIssuer != "" && oidcIssuer != options.DefaultOIDCIssuerURL) || - tsaServerURL != "") { - return fmt.Errorf("cannot specify service URLs and use signing config") - } - if (useSigningConfig || signingConfigPath != "") && !tlogUpload { - return fmt.Errorf("--tlog-upload=false is not supported with --signing-config or --use-signing-config. Provide a signing config with --signing-config without a transparency log service, which can be created with `cosign signing-config create` or `curl https://raw.githubusercontent.com/sigstore/root-signing/refs/heads/main/targets/signing_config.v0.2.json | jq 'del(.rekorTlogUrls)'` for the public instance") - } - // Signing config requires a bundle as output for verification materials since sigstore-go is used - if (useSigningConfig || signingConfigPath != "") && !newBundleFormat && bundlePath == "" { - return fmt.Errorf("must provide --new-bundle-format or --bundle where applicable with --signing-config or --use-signing-config") - } // Fetch a trusted root when: // * requesting a certificate and no CT log key is provided to verify an SCT // * using a signing config - if ((keyRef == "" || issueCertificate) && env.Getenv(env.VariableSigstoreCTLogPublicKeyFile) == "") || - (useSigningConfig || signingConfigPath != "") { + // TODO(#5013): Remove useSigningConfig in favor of offline signing flag + if keyRef == "" || useSigningConfig || signingConfigPath != "" { if trustedRootPath != "" { ko.TrustedMaterial, err = root.NewTrustedRootFromPath(trustedRootPath) if err != nil { @@ -473,26 +455,6 @@ func LoadTrustedMaterialAndSigningConfig(ctx context.Context, ko *options.KeyOpt } } - // TODO: Remove deprecated output flags warning in a future release (when flags are removed) - if newBundleFormat && outputSignature != "" { - ui.Warnf(context.Background(), "--output-signature is deprecated when using --new-bundle-format and will be ignored") - } - if newBundleFormat && outputAttestation != "" { - ui.Warnf(context.Background(), "--output-attestation is deprecated when using --new-bundle-format and will be ignored") - } - if newBundleFormat && outputCertificate != "" { - ui.Warnf(context.Background(), "--output-certificate is deprecated when using --new-bundle-format and will be ignored") - } - if newBundleFormat && outputPayload != "" { - ui.Warnf(context.Background(), "--output-payload is deprecated when using --new-bundle-format and will be ignored") - } - if newBundleFormat && outputTimestamp != "" { - ui.Warnf(context.Background(), "--rfc3161-timestamp is deprecated when using --new-bundle-format and will be ignored") - } - if newBundleFormat && output != "" { - ui.Warnf(context.Background(), "--output is deprecated when using --new-bundle-format and will be ignored") - } - return nil } @@ -598,64 +560,18 @@ func NewLegacyBundleFromProtoBundleComponents(bc *BundleComponents) ([]byte, err return json.Marshal(signedPayload) } -// NewSigningConfigFromKeyOpts creates a signing config from key options. -// This only supports Rekor v1. Rekor v2 requires a user-provided signing config. -func NewSigningConfigFromKeyOpts(ko options.KeyOpts, tlogUpload bool) (*root.SigningConfig, error) { - var fulcioServices []root.Service - if ko.FulcioURL != "" { - fulcioServices = append(fulcioServices, root.Service{ - URL: ko.FulcioURL, - MajorAPIVersion: 1, - ValidityPeriodStart: time.Now(), - }) - } - - var oidcServices []root.Service - if ko.OIDCIssuer != "" { - oidcServices = append(oidcServices, root.Service{ - URL: ko.OIDCIssuer, - MajorAPIVersion: 1, - ValidityPeriodStart: time.Now(), - }) - } - - var rekorServices []root.Service - var rekorConfig root.ServiceConfiguration - if ko.RekorURL != "" && tlogUpload { - rekorServices = append(rekorServices, root.Service{ - URL: ko.RekorURL, - MajorAPIVersion: 1, - ValidityPeriodStart: time.Now(), - }) - rekorConfig = root.ServiceConfiguration{ - Selector: prototrustroot.ServiceSelector_ANY, - Count: 1, - } - } - - var tsaServices []root.Service - var tsaConfig root.ServiceConfiguration - if ko.TSAServerURL != "" { - tsaServices = append(tsaServices, root.Service{ - URL: ko.TSAServerURL, - MajorAPIVersion: 1, - ValidityPeriodStart: time.Now(), - }) - tsaConfig = root.ServiceConfiguration{ - Selector: prototrustroot.ServiceSelector_ANY, - Count: 1, - } - } - - return root.NewSigningConfig( +// NewEmptySigningConfig returns a signing config with no services configured. +func NewEmptySigningConfig() *root.SigningConfig { + sc, _ := root.NewSigningConfig( root.SigningConfigMediaType02, - fulcioServices, - oidcServices, - rekorServices, - rekorConfig, - tsaServices, - tsaConfig, + nil, + nil, + nil, + root.ServiceConfiguration{Selector: prototrustroot.ServiceSelector_ANY}, + nil, + root.ServiceConfiguration{Selector: prototrustroot.ServiceSelector_ANY}, ) + return sc } // ProtoHashAlgoToHash converts a protobuf HashAlgorithm to a crypto.Hash. diff --git a/cmd/cosign/cli/verify.go b/cmd/cosign/cli/verify.go index 408c88e8d47..8282b751e45 100644 --- a/cmd/cosign/cli/verify.go +++ b/cmd/cosign/cli/verify.go @@ -80,54 +80,31 @@ against the transparency log.`, Args: cobra.MinimumNArgs(1), PersistentPreRun: options.BindViper, RunE: func(cmd *cobra.Command, args []string) error { - if o.CommonVerifyOptions.PrivateInfrastructure { - o.CommonVerifyOptions.IgnoreTlog = true - } - annotations, err := o.AnnotationsMap() if err != nil { return err } - hashAlgorithm, err := o.SignatureDigest.HashAlgorithm() - if err != nil { - return err - } - v := &verify.VerifyCommand{ RegistryOptions: o.Registry, CertVerifyOptions: o.CertVerify, CommonVerifyOptions: o.CommonVerifyOptions, CheckClaims: o.CheckClaims, KeyRef: o.Key, - CertRef: o.CertVerify.Cert, - CertChain: o.CertVerify.CertChain, - CAIntermediates: o.CertVerify.CAIntermediates, - CARoots: o.CertVerify.CARoots, CertGithubWorkflowTrigger: o.CertVerify.CertGithubWorkflowTrigger, CertGithubWorkflowSha: o.CertVerify.CertGithubWorkflowSha, CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName, CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository, CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef, IgnoreSCT: o.CertVerify.IgnoreSCT, - SCTRef: o.CertVerify.SCT, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, Output: o.Output, - RekorURL: o.Rekor.URL, - Attachment: o.Attachment, Annotations: annotations, - HashAlgorithm: hashAlgorithm, - SignatureRef: o.SignatureRef, - PayloadRef: o.PayloadRef, LocalImage: o.LocalImage, - Offline: o.CommonVerifyOptions.Offline, - TSACertChainPath: o.CommonVerifyOptions.TSACertChainPath, IgnoreTlog: o.CommonVerifyOptions.IgnoreTlog, MaxWorkers: o.CommonVerifyOptions.MaxWorkers, - ExperimentalOCI11: o.CommonVerifyOptions.ExperimentalOCI11, UseSignedTimestamps: o.CommonVerifyOptions.UseSignedTimestamps, - NewBundleFormat: o.CommonVerifyOptions.NewBundleFormat, AllowCertificateChain: o.CommonVerifyOptions.AllowCertificateChain, } @@ -142,7 +119,7 @@ against the transparency log.`, ctx, cancel := context.WithTimeout(cmd.Context(), ro.Timeout) defer cancel() - if o.CommonVerifyOptions.IgnoreTlog && !o.CommonVerifyOptions.PrivateInfrastructure { + if o.CommonVerifyOptions.IgnoreTlog { ui.Warnf(ctx, ignoreTLogMessage, "signature") } @@ -203,45 +180,27 @@ against the transparency log.`, Args: cobra.MinimumNArgs(1), PersistentPreRun: options.BindViper, RunE: func(cmd *cobra.Command, args []string) error { - if o.CommonVerifyOptions.PrivateInfrastructure { - o.CommonVerifyOptions.IgnoreTlog = true - } - - hashAlgorithm, err := o.SignatureDigest.HashAlgorithm() - if err != nil { - return err - } - v := &verify.VerifyAttestationCommand{ RegistryOptions: o.Registry, CommonVerifyOptions: o.CommonVerifyOptions, CheckClaims: o.CheckClaims, CertVerifyOptions: o.CertVerify, - CertRef: o.CertVerify.Cert, - CertChain: o.CertVerify.CertChain, - CAIntermediates: o.CertVerify.CAIntermediates, - CARoots: o.CertVerify.CARoots, CertGithubWorkflowTrigger: o.CertVerify.CertGithubWorkflowTrigger, CertGithubWorkflowSha: o.CertVerify.CertGithubWorkflowSha, CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName, CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository, CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef, IgnoreSCT: o.CertVerify.IgnoreSCT, - SCTRef: o.CertVerify.SCT, KeyRef: o.Key, Sk: o.SecurityKey.Use, Slot: o.SecurityKey.Slot, Output: o.Output, - RekorURL: o.Rekor.URL, PredicateType: o.Predicate.Type, Policies: o.Policies, LocalImage: o.LocalImage, NameOptions: o.Registry.NameOptions(), - Offline: o.CommonVerifyOptions.Offline, - TSACertChainPath: o.CommonVerifyOptions.TSACertChainPath, IgnoreTlog: o.CommonVerifyOptions.IgnoreTlog, MaxWorkers: o.CommonVerifyOptions.MaxWorkers, - HashAlgorithm: hashAlgorithm, UseSignedTimestamps: o.CommonVerifyOptions.UseSignedTimestamps, } @@ -252,7 +211,7 @@ against the transparency log.`, ctx, cancel := context.WithTimeout(cmd.Context(), ro.Timeout) defer cancel() - if o.CommonVerifyOptions.IgnoreTlog && !o.CommonVerifyOptions.PrivateInfrastructure { + if o.CommonVerifyOptions.IgnoreTlog { ui.Warnf(ctx, ignoreTLogMessage, "attestation") } @@ -310,52 +269,31 @@ The blob may be specified as a path to a file or - for stdin.`, Args: cobra.ExactArgs(1), PersistentPreRun: options.BindViper, RunE: func(cmd *cobra.Command, args []string) error { - if o.CommonVerifyOptions.PrivateInfrastructure { - o.CommonVerifyOptions.IgnoreTlog = true - } - - hashAlgorithm, err := o.SignatureDigest.HashAlgorithm() - if err != nil { - return err - } - ko := options.KeyOpts{ - KeyRef: o.Key, - Sk: o.SecurityKey.Use, - Slot: o.SecurityKey.Slot, - RekorURL: o.Rekor.URL, - BundlePath: o.BundlePath, - RFC3161TimestampPath: o.RFC3161TimestampPath, - TSACertChainPath: o.CommonVerifyOptions.TSACertChainPath, - NewBundleFormat: o.CommonVerifyOptions.NewBundleFormat, + KeyRef: o.Key, + Sk: o.SecurityKey.Use, + Slot: o.SecurityKey.Slot, + BundlePath: o.BundlePath, } verifyBlobCmd := &verify.VerifyBlobCmd{ KeyOpts: ko, CertVerifyOptions: o.CertVerify, - CertRef: o.CertVerify.Cert, - CertChain: o.CertVerify.CertChain, - CARoots: o.CertVerify.CARoots, - CAIntermediates: o.CertVerify.CAIntermediates, - SigRef: o.Signature, CertGithubWorkflowTrigger: o.CertVerify.CertGithubWorkflowTrigger, CertGithubWorkflowSHA: o.CertVerify.CertGithubWorkflowSha, CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName, CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository, CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef, IgnoreSCT: o.CertVerify.IgnoreSCT, - SCTRef: o.CertVerify.SCT, - Offline: o.CommonVerifyOptions.Offline, IgnoreTlog: o.CommonVerifyOptions.IgnoreTlog, UseSignedTimestamps: o.CommonVerifyOptions.UseSignedTimestamps, TrustedRootPath: o.CommonVerifyOptions.TrustedRootPath, - HashAlgorithm: hashAlgorithm, AllowCertificateChain: o.CommonVerifyOptions.AllowCertificateChain, } ctx, cancel := context.WithTimeout(cmd.Context(), ro.Timeout) defer cancel() - if o.CommonVerifyOptions.IgnoreTlog && !o.CommonVerifyOptions.PrivateInfrastructure { + if o.CommonVerifyOptions.IgnoreTlog { ui.Warnf(ctx, ignoreTLogMessage, "blob") } @@ -403,49 +341,28 @@ The blob may be specified as a path to a file.`, Args: cobra.MaximumNArgs(1), PersistentPreRun: options.BindViper, RunE: func(cmd *cobra.Command, args []string) error { - if o.CommonVerifyOptions.PrivateInfrastructure { - o.CommonVerifyOptions.IgnoreTlog = true - } - - hashAlgorithm, err := o.SignatureDigest.HashAlgorithm() - if err != nil { - return err - } - ko := options.KeyOpts{ - KeyRef: o.Key, - Sk: o.SecurityKey.Use, - Slot: o.SecurityKey.Slot, - RekorURL: o.Rekor.URL, - BundlePath: o.BundlePath, - RFC3161TimestampPath: o.RFC3161TimestampPath, - TSACertChainPath: o.CommonVerifyOptions.TSACertChainPath, - NewBundleFormat: o.CommonVerifyOptions.NewBundleFormat, + KeyRef: o.Key, + Sk: o.SecurityKey.Use, + Slot: o.SecurityKey.Slot, + BundlePath: o.BundlePath, } v := verify.VerifyBlobAttestationCommand{ KeyOpts: ko, PredicateType: o.Type, CheckClaims: o.CheckClaims, - SignaturePath: o.SignaturePath, CertVerifyOptions: o.CertVerify, - CertRef: o.CertVerify.Cert, - CertChain: o.CertVerify.CertChain, - CARoots: o.CertVerify.CARoots, - CAIntermediates: o.CertVerify.CAIntermediates, CertGithubWorkflowTrigger: o.CertVerify.CertGithubWorkflowTrigger, CertGithubWorkflowSHA: o.CertVerify.CertGithubWorkflowSha, CertGithubWorkflowName: o.CertVerify.CertGithubWorkflowName, CertGithubWorkflowRepository: o.CertVerify.CertGithubWorkflowRepository, CertGithubWorkflowRef: o.CertVerify.CertGithubWorkflowRef, IgnoreSCT: o.CertVerify.IgnoreSCT, - SCTRef: o.CertVerify.SCT, - Offline: o.CommonVerifyOptions.Offline, IgnoreTlog: o.CommonVerifyOptions.IgnoreTlog, UseSignedTimestamps: o.CommonVerifyOptions.UseSignedTimestamps, TrustedRootPath: o.CommonVerifyOptions.TrustedRootPath, Digest: o.Digest, DigestAlg: o.DigestAlg, - HashAlgorithm: hashAlgorithm, AllowCertificateChain: o.CommonVerifyOptions.AllowCertificateChain, } // We only use the blob if we are checking claims. @@ -460,7 +377,7 @@ The blob may be specified as a path to a file.`, ctx, cancel := context.WithTimeout(cmd.Context(), ro.Timeout) defer cancel() - if o.CommonVerifyOptions.IgnoreTlog && !o.CommonVerifyOptions.PrivateInfrastructure { + if o.CommonVerifyOptions.IgnoreTlog { ui.Warnf(ctx, ignoreTLogMessage, "blob attestation") } diff --git a/cmd/cosign/cli/verify/common.go b/cmd/cosign/cli/verify/common.go index 507fc0611a4..069b5e0944b 100644 --- a/cmd/cosign/cli/verify/common.go +++ b/cmd/cosign/cli/verify/common.go @@ -14,24 +14,13 @@ package verify import ( - "bytes" "context" - "crypto" - "crypto/x509" - "encoding/base64" "encoding/json" - "errors" "fmt" "os" - "reflect" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/fulcio" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/rekor" "github.com/sigstore/cosign/v3/internal/ui" - "github.com/sigstore/cosign/v3/pkg/blob" "github.com/sigstore/cosign/v3/pkg/cosign" - "github.com/sigstore/cosign/v3/pkg/cosign/env" "github.com/sigstore/cosign/v3/pkg/cosign/pivkey" "github.com/sigstore/cosign/v3/pkg/cosign/pkcs11key" "github.com/sigstore/cosign/v3/pkg/oci" @@ -42,136 +31,40 @@ import ( "github.com/sigstore/sigstore/pkg/signature/payload" ) -// CheckSigstoreBundleUnsupportedOptions checks for incompatible settings on any Verify* command struct when NewBundleFormat is used. -func CheckSigstoreBundleUnsupportedOptions(cmd any, verifyOfflineWithKey bool, co *cosign.CheckOpts) error { - if !co.NewBundleFormat { - return nil - } - fieldToErr := map[string]string{ - "CertRef": "certificate must be in bundle and may not be provided using --certificate", - "CertChain": "certificate chain must be in bundle and may not be provided using --certificate-chain", - "CARoots": "CA roots/intermediates must be provided using --trusted-root", - "CAIntermediates": "CA roots/intermediates must be provided using --trusted-root", - "TSACertChainPath": "TSA certificate chain path may only be provided using --trusted-root", - "RFC3161TimestampPath": "RFC3161 timestamp may not be provided using --rfc3161-timestamp", - "SigRef": "signature may not be provided using --signature", - "SCTRef": "SCT may not be provided using --sct", - } - v := reflect.ValueOf(cmd) - for f, e := range fieldToErr { - if field := v.FieldByName(f); field.IsValid() && field.String() != "" { - return fmt.Errorf("unsupported: %s when using --new-bundle-format", e) - } - } - if co.TrustedMaterial == nil && !verifyOfflineWithKey { - return fmt.Errorf("trusted root is required when using new bundle format") - } - return nil -} - -// LoadVerifierFromKeyOrCert returns either a signature.Verifier or a certificate from the provided flags to use for verifying an artifact. +// LoadVerifierFromKey returns a signature.Verifier from the provided key flags to use for verifying an artifact. // In the case of certain types of keys, it returns a close function that must be called by the calling method. -func LoadVerifierFromKeyOrCert(ctx context.Context, keyRef, slot, certRef, certChain string, hashAlgorithm crypto.Hash, sk, withGetCert bool, co *cosign.CheckOpts) (signature.Verifier, *x509.Certificate, func(), error) { +func LoadVerifierFromKey(ctx context.Context, keyRef, slot string, sk bool) (signature.Verifier, func(), error) { var sigVerifier signature.Verifier var err error switch { case keyRef != "": - sigVerifier, err = csignature.PublicKeyFromKeyRefWithHashAlgo(ctx, keyRef, hashAlgorithm) + sigVerifier, err = csignature.PublicKeyFromKeyRef(ctx, keyRef) if err != nil { - return nil, nil, nil, fmt.Errorf("loading public key: %w", err) + return nil, nil, fmt.Errorf("loading public key: %w", err) } pkcs11Key, ok := sigVerifier.(*pkcs11key.Key) closeSV := func() {} if ok { closeSV = pkcs11Key.Close } - return sigVerifier, nil, closeSV, nil + return sigVerifier, closeSV, nil case sk: sk, err := pivkey.GetKeyWithSlot(slot) if err != nil { - return nil, nil, nil, fmt.Errorf("opening piv token: %w", err) + return nil, nil, fmt.Errorf("opening piv token: %w", err) } sigVerifier, err = sk.Verifier() if err != nil { sk.Close() - return nil, nil, nil, fmt.Errorf("initializing piv token verifier: %w", err) - } - return sigVerifier, nil, sk.Close, nil - case certRef != "": - cert, err := loadCertFromFileOrURL(certRef) - if err != nil { - return nil, nil, nil, fmt.Errorf("loading cert: %w", err) - } - if withGetCert { - return nil, cert, func() {}, nil - } - if certChain == "" { - sigVerifier, err = cosign.ValidateAndUnpackCert(cert, co) - if err != nil { - return nil, nil, nil, fmt.Errorf("validating cert: %w", err) - } - return sigVerifier, nil, func() {}, nil - } - chain, err := loadCertChainFromFileOrURL(certChain) - if err != nil { - return nil, nil, nil, fmt.Errorf("loading cert chain: %w", err) - } - sigVerifier, err = cosign.ValidateAndUnpackCertWithChain(cert, chain, co) - if err != nil { - return nil, nil, nil, fmt.Errorf("validating cert with chain: %w", err) - } - return sigVerifier, nil, func() {}, nil - } - return nil, nil, func() {}, nil -} - -// SetLegacyClientsAndKeys sets up TSA and rekor clients and keys for TSA, rekor, and CT log. -// It may perform an online fetch of keys, so using trusted root instead of these TUF v1 methods is recommended. -// It takes a CheckOpts as input and modifies it. -func SetLegacyClientsAndKeys(ctx context.Context, ignoreTlog, shouldVerifySCT, keylessVerification bool, rekorURL, tsaCertChain, certChain, caRoots, caIntermediates string, co *cosign.CheckOpts) error { - var err error - if !ignoreTlog && !co.NewBundleFormat && rekorURL != "" { - co.RekorClient, err = rekor.NewClient(rekorURL) - if err != nil { - return fmt.Errorf("creating rekor client: %w", err) - } - } - // If trusted material is set, we don't need to fetch disparate keys. - if co.TrustedMaterial != nil { - return nil - } - if co.UseSignedTimestamps { - tsaCertificates, err := cosign.GetTSACerts(ctx, tsaCertChain, cosign.GetTufTargets) - if err != nil { - return fmt.Errorf("loading TSA certificates: %w", err) - } - co.TSACertificate = tsaCertificates.LeafCert - co.TSARootCertificates = tsaCertificates.RootCert - co.TSAIntermediateCertificates = tsaCertificates.IntermediateCerts - } - if !ignoreTlog { - co.RekorPubKeys, err = cosign.GetRekorPubs(ctx) - if err != nil { - return fmt.Errorf("getting rekor public keys: %w", err) - } - } - if shouldVerifySCT { - co.CTLogPubKeys, err = cosign.GetCTLogPubs(ctx) - if err != nil { - return fmt.Errorf("getting ctlog public keys: %w", err) + return nil, nil, fmt.Errorf("initializing piv token verifier: %w", err) } + return sigVerifier, sk.Close, nil } - if keylessVerification { - if err := loadCertsKeylessVerification(certChain, caRoots, caIntermediates, co); err != nil { - return fmt.Errorf("loading certs for keyless verification: %w", err) - } - } - return nil + return nil, func() {}, nil } // SetTrustedMaterial sets TrustedMaterial on CheckOpts, either from the provided trusted root path or from TUF. -// It does not set TrustedMaterial if the user provided trusted material via other flags or environment variables. -func SetTrustedMaterial(ctx context.Context, trustedRootPath, certChain, caRoots, caIntermediates, tsaCertChainPath string, verifyOnlyWithKey bool, co *cosign.CheckOpts) error { +func SetTrustedMaterial(trustedRootPath string, verifyOnlyWithKey bool, co *cosign.CheckOpts) error { var err error if trustedRootPath != "" { co.TrustedMaterial, err = root.NewTrustedRootFromPath(trustedRootPath) @@ -183,18 +76,9 @@ func SetTrustedMaterial(ctx context.Context, trustedRootPath, certChain, caRoots if verifyOnlyWithKey { return nil } - if options.NOf(certChain, caRoots, caIntermediates, tsaCertChainPath) == 0 && - env.Getenv(env.VariableSigstoreCTLogPublicKeyFile) == "" && - env.Getenv(env.VariableSigstoreRootFile) == "" && - env.Getenv(env.VariableSigstoreRekorPublicKey) == "" && - env.Getenv(env.VariableSigstoreTSACertificateFile) == "" { - co.TrustedMaterial, err = cosign.TrustedRoot() - if err != nil { - if co.NewBundleFormat { - return fmt.Errorf("getting trusted root from TUF for new bundle verification: %w", err) - } - ui.Warnf(ctx, "Could not fetch trusted_root.json from the TUF repository. Continuing with individual targets. Error from TUF: %v", err) - } + co.TrustedMaterial, err = cosign.TrustedRoot() + if err != nil { + return fmt.Errorf("getting trusted root from TUF for bundle verification: %w", err) } return nil } @@ -346,133 +230,9 @@ func PrintVerification(ctx context.Context, verified []oci.Signature, output str } } -func loadCertFromFileOrURL(path string) (*x509.Certificate, error) { - pems, err := blob.LoadFileOrURL(path) - if err != nil { - return nil, err - } - return loadCertFromPEM(pems) -} - -func loadCertFromPEM(pems []byte) (*x509.Certificate, error) { - var out []byte - out, err := base64.StdEncoding.DecodeString(string(pems)) - if err != nil { - // not a base64 - out = pems - } - - certs, err := cryptoutils.UnmarshalCertificatesFromPEM(out) - if err != nil { - return nil, err - } - if len(certs) == 0 { - return nil, errors.New("no certs found in pem file") - } - return certs[0], nil -} - -func loadCertChainFromFileOrURL(path string) ([]*x509.Certificate, error) { - pems, err := blob.LoadFileOrURL(path) - if err != nil { - return nil, err - } - certs, err := cryptoutils.LoadCertificatesFromPEM(bytes.NewReader(pems)) - if err != nil { - return nil, err - } - return certs, nil -} - -func keylessVerification(keyRef string, sk bool) bool { - if keyRef != "" { - return false - } - if sk { - return false - } - return true -} - -func shouldVerifySCT(ignoreSCT bool, keyRef string, sk bool) bool { - if keyRef != "" { - return false - } - if sk { - return false - } - if ignoreSCT { - return false - } - return true -} - // No trusted root is needed if verification doesn't require Rekor or // signed timestamps, and a key is explicitly provided instead of using -// a Fulcio certificate either via a key or certificate reference or security key. -func verifyOfflineWithKey(keyRef, certRef string, sk bool, co *cosign.CheckOpts) bool { - return (keyRef != "" || certRef != "" || sk) && co.IgnoreTlog && !co.UseSignedTimestamps -} - -// loadCertsKeylessVerification loads certificates provided as a certificate chain or CA roots + CA intermediate -// certificate files. If both certChain and caRootsFile are empty strings, the Fulcio roots are loaded. -// -// The co *cosign.CheckOpts is both input and output parameter - it gets updated -// with the root and intermediate certificates needed for verification. -func loadCertsKeylessVerification(certChainFile string, - caRootsFile string, - caIntermediatesFile string, - co *cosign.CheckOpts) error { - var err error - switch { - case certChainFile != "": - chain, err := loadCertChainFromFileOrURL(certChainFile) - if err != nil { - return err - } - co.RootCerts = x509.NewCertPool() - co.RootCerts.AddCert(chain[len(chain)-1]) - if len(chain) > 1 { - co.IntermediateCerts = x509.NewCertPool() - for _, cert := range chain[:len(chain)-1] { - co.IntermediateCerts.AddCert(cert) - } - } - case caRootsFile != "": - caRoots, err := loadCertChainFromFileOrURL(caRootsFile) - if err != nil { - return err - } - co.RootCerts = x509.NewCertPool() - if len(caRoots) > 0 { - for _, cert := range caRoots { - co.RootCerts.AddCert(cert) - } - } - if caIntermediatesFile != "" { - caIntermediates, err := loadCertChainFromFileOrURL(caIntermediatesFile) - if err != nil { - return err - } - if len(caIntermediates) > 0 { - co.IntermediateCerts = x509.NewCertPool() - for _, cert := range caIntermediates { - co.IntermediateCerts.AddCert(cert) - } - } - } - default: - // This performs an online fetch of the Fulcio roots from a TUF repository. - // This is needed for verifying keyless certificates (both online and offline). - co.RootCerts, err = fulcio.GetRoots() - if err != nil { - return fmt.Errorf("getting Fulcio roots: %w", err) - } - co.IntermediateCerts, err = fulcio.GetIntermediates() - if err != nil { - return fmt.Errorf("getting Fulcio intermediates: %w", err) - } - } - - return nil +// a Fulcio certificate either via a key or security key. +func verifyOfflineWithKey(keyRef string, sk bool, co *cosign.CheckOpts) bool { + return (keyRef != "" || sk) && co.IgnoreTlog && !co.UseSignedTimestamps } diff --git a/cmd/cosign/cli/verify/common_test.go b/cmd/cosign/cli/verify/common_test.go index 490d5b04126..0e0b06d60e1 100644 --- a/cmd/cosign/cli/verify/common_test.go +++ b/cmd/cosign/cli/verify/common_test.go @@ -14,12 +14,10 @@ package verify import ( - "context" "path/filepath" "strings" "testing" - "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/env" ) @@ -27,17 +25,14 @@ import ( func TestSetTrustedMaterialNewBundleTUFError(t *testing.T) { setBrokenTrustedRootTUFEnv(t) - co := &cosign.CheckOpts{NewBundleFormat: true} - var err error - stderr := ui.RunWithTestCtx(func(ctx context.Context, _ ui.WriteFunc) { - err = SetTrustedMaterial(ctx, "", "", "", "", "", false, co) - }) + co := &cosign.CheckOpts{} + err := SetTrustedMaterial("", false, co) if err == nil { t.Fatal("expected trusted root TUF error") } - if !strings.Contains(err.Error(), "getting trusted root from TUF for new bundle verification") { - t.Fatalf("expected new bundle trusted root error, got %v", err) + if !strings.Contains(err.Error(), "getting trusted root from TUF for bundle verification") { + t.Fatalf("expected bundle trusted root error, got %v", err) } if !strings.Contains(err.Error(), "error reading root.json given by TUF_ROOT_JSON") { t.Fatalf("expected underlying TUF error, got %v", err) @@ -45,29 +40,6 @@ func TestSetTrustedMaterialNewBundleTUFError(t *testing.T) { if co.TrustedMaterial != nil { t.Fatal("expected TrustedMaterial to remain unset") } - if stderr != "" { - t.Fatalf("expected no warning when returning new bundle error, got %q", stderr) - } -} - -func TestSetTrustedMaterialLegacyTUFFallback(t *testing.T) { - setBrokenTrustedRootTUFEnv(t) - - co := &cosign.CheckOpts{} - var err error - stderr := ui.RunWithTestCtx(func(ctx context.Context, _ ui.WriteFunc) { - err = SetTrustedMaterial(ctx, "", "", "", "", "", false, co) - }) - - if err != nil { - t.Fatalf("expected legacy trusted material fallback, got %v", err) - } - if co.TrustedMaterial != nil { - t.Fatal("expected TrustedMaterial to remain unset") - } - if !strings.Contains(stderr, "Could not fetch trusted_root.json from the TUF repository") { - t.Fatalf("expected legacy fallback warning, got %q", stderr) - } } func setBrokenTrustedRootTUFEnv(t *testing.T) { diff --git a/cmd/cosign/cli/verify/verify.go b/cmd/cosign/cli/verify/verify.go index 51629dbc62f..08e86bb9158 100644 --- a/cmd/cosign/cli/verify/verify.go +++ b/cmd/cosign/cli/verify/verify.go @@ -17,18 +17,13 @@ package verify import ( "context" - "crypto" "encoding/json" "flag" "fmt" - "os" - "path/filepath" "github.com/google/go-containerregistry/pkg/name" "github.com/in-toto/in-toto-golang/in_toto" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign" - cosignError "github.com/sigstore/cosign/v3/cmd/cosign/errors" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/attestation" "github.com/sigstore/cosign/v3/pkg/oci" @@ -48,36 +43,22 @@ type VerifyCommand struct { options.CommonVerifyOptions CheckClaims bool KeyRef string - CertRef string CertGithubWorkflowTrigger string CertGithubWorkflowSha string CertGithubWorkflowName string CertGithubWorkflowRepository string CertGithubWorkflowRef string - CAIntermediates string - CARoots string - CertChain string CertOidcProvider string IgnoreSCT bool - SCTRef string Sk bool Slot string Output string - RekorURL string - Attachment string Annotations sigs.AnnotationsMap - SignatureRef string - PayloadRef string - HashAlgorithm crypto.Hash LocalImage bool NameOptions []name.Option - Offline bool - TSACertChainPath string UseSignedTimestamps bool IgnoreTlog bool MaxWorkers int - ExperimentalOCI11 bool - NewBundleFormat bool AllowCertificateChain bool } @@ -87,20 +68,6 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { return flag.ErrHelp } - switch c.Attachment { - case "sbom": - fmt.Fprintln(os.Stderr, options.SBOMAttachmentDeprecation) - case "": - break - default: - return flag.ErrHelp - } - - // always default to sha256 if the algorithm hasn't been explicitly set - if c.HashAlgorithm == 0 { - c.HashAlgorithm = crypto.SHA256 - } - // key and cert identity are mutually exclusive if options.NOf(c.KeyRef, c.CertIdentity, c.CertIdentityRegexp) > 1 { return &options.KeyAndIdentityParseError{} @@ -134,76 +101,32 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { CertGithubWorkflowRepository: c.CertGithubWorkflowRepository, CertGithubWorkflowRef: c.CertGithubWorkflowRef, IgnoreSCT: c.IgnoreSCT, - SignatureRef: c.SignatureRef, - PayloadRef: c.PayloadRef, Identities: identities, - Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, MaxWorkers: c.MaxWorkers, - ExperimentalOCI11: c.ExperimentalOCI11, - UseSignedTimestamps: c.TSACertChainPath != "" || c.UseSignedTimestamps, - NewBundleFormat: c.NewBundleFormat, + UseSignedTimestamps: c.UseSignedTimestamps, AllowCertificateChain: c.AllowCertificateChain, } - vOfflineKey := verifyOfflineWithKey(c.KeyRef, c.CertRef, c.Sk, co) + vOfflineKey := verifyOfflineWithKey(c.KeyRef, c.Sk, co) - // Auto-detect bundle format for local images - if c.LocalImage { - hasBundles, err := cosign.HasLocalBundles(images[0]) - if err != nil { - return fmt.Errorf("checking local image format: %w", err) - } - co.NewBundleFormat = hasBundles - } else { - ref, err := name.ParseReference(images[0], c.NameOptions...) - if err == nil && c.NewBundleFormat { - newBundles, _, err := cosign.GetBundles(ctx, ref, co.RegistryClientOpts, c.NameOptions...) - if len(newBundles) == 0 || err != nil { - co.NewBundleFormat = false - } - } - } - - err = SetTrustedMaterial(ctx, c.TrustedRootPath, c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath, vOfflineKey, co) + err = SetTrustedMaterial(c.TrustedRootPath, vOfflineKey, co) if err != nil { return fmt.Errorf("setting trusted material: %w", err) } - if err = CheckSigstoreBundleUnsupportedOptions(*c, vOfflineKey, co); err != nil { - return err - } - if c.CheckClaims { - if co.NewBundleFormat { - co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier - } else { - co.ClaimVerifier = cosign.SimpleClaimVerifier - } + co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier } - err = SetLegacyClientsAndKeys(ctx, c.IgnoreTlog, shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk), keylessVerification(c.KeyRef, c.Sk), c.RekorURL, c.TSACertChainPath, c.CertChain, c.CARoots, c.CAIntermediates, co) - if err != nil { - return fmt.Errorf("setting up clients and keys: %w", err) - } - - // User provides a key or certificate. Otherwise, verification requires a Fulcio certificate - // provided in an attached bundle or OCI annotation. LoadVerifierFromKeyOrCert must be called - // after initializing trust material in order to verify certificate chain. + // User provides a key. Otherwise, verification requires a Fulcio certificate provided in an + // attached bundle or OCI annotation. var closeSV func() - co.SigVerifier, _, closeSV, err = LoadVerifierFromKeyOrCert(ctx, c.KeyRef, c.Slot, c.CertRef, c.CertChain, c.HashAlgorithm, c.Sk, false, co) + co.SigVerifier, closeSV, err = LoadVerifierFromKey(ctx, c.KeyRef, c.Slot, c.Sk) if err != nil { return fmt.Errorf("loading verifier from key opts: %w", err) } defer closeSV() - if c.CertRef != "" && c.SCTRef != "" { - sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) - if err != nil { - return fmt.Errorf("reading sct from file: %w", err) - } - co.SCT = sct - } - // NB: There are only 2 kinds of verification right now: // 1. You gave us the public key explicitly to verify against so co.SigVerifier is non-nil or, // 2. We’re going to find an x509 certificate on the signature and verify against @@ -217,16 +140,9 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { var bundleVerified bool if c.LocalImage { - if co.NewBundleFormat { - verified, bundleVerified, err = cosign.VerifyLocalImageAttestations(ctx, img, co) - if err != nil { - return err - } - } else { - verified, bundleVerified, err = cosign.VerifyLocalImageSignatures(ctx, img, co) - if err != nil { - return err - } + verified, bundleVerified, err = cosign.VerifyLocalImageAttestations(ctx, img, co) + if err != nil { + return err } PrintVerificationHeader(ctx, img, co, bundleVerified, fulcioVerified) PrintVerification(ctx, verified, c.Output) @@ -236,27 +152,15 @@ func (c *VerifyCommand) Exec(ctx context.Context, images []string) (err error) { return fmt.Errorf("parsing reference: %w", err) } - if co.NewBundleFormat { - // OCI bundle always contains attestation - verified, bundleVerified, err = cosign.VerifyImageAttestations(ctx, ref, co, c.NameOptions...) - if err != nil { - return err - } - - verifiedOutput, err := transformOutput(verified, ref.Name()) - if err == nil { - verified = verifiedOutput - } - } else { - ref, err = sign.GetAttachedImageRef(ref, c.Attachment, ociremoteOpts...) - if err != nil { - return fmt.Errorf("resolving attachment type %s for image %s: %w", c.Attachment, img, err) - } + // OCI bundle always contains attestation + verified, bundleVerified, err = cosign.VerifyImageAttestations(ctx, ref, co, c.NameOptions...) + if err != nil { + return err + } - verified, bundleVerified, err = cosign.VerifyImageSignatures(ctx, ref, co) - if err != nil { - return cosignError.WrapError(err) - } + verifiedOutput, err := transformOutput(verified, ref.Name()) + if err == nil { + verified = verifiedOutput } PrintVerificationHeader(ctx, ref.Name(), co, bundleVerified, fulcioVerified) diff --git a/cmd/cosign/cli/verify/verify_attestation.go b/cmd/cosign/cli/verify/verify_attestation.go index c7a64e1098e..a6b3039d304 100644 --- a/cmd/cosign/cli/verify/verify_attestation.go +++ b/cmd/cosign/cli/verify/verify_attestation.go @@ -17,11 +17,9 @@ package verify import ( "context" - "crypto" "errors" "flag" "fmt" - "os" "path/filepath" "strings" @@ -45,31 +43,23 @@ type VerifyAttestationCommand struct { options.CommonVerifyOptions CheckClaims bool KeyRef string - CertRef string CertGithubWorkflowTrigger string CertGithubWorkflowSha string CertGithubWorkflowName string CertGithubWorkflowRepository string CertGithubWorkflowRef string - CAIntermediates string - CARoots string - CertChain string IgnoreSCT bool - SCTRef string Sk bool Slot string Output string - RekorURL string PredicateType string Policies []string LocalImage bool NameOptions []name.Option Offline bool - TSACertChainPath string IgnoreTlog bool MaxWorkers int UseSignedTimestamps bool - HashAlgorithm crypto.Hash } // Exec runs the verification command @@ -83,11 +73,6 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e return &options.KeyAndIdentityParseError{} } - // always default to sha256 if the algorithm hasn't been explicitly set - if c.HashAlgorithm == 0 { - c.HashAlgorithm = crypto.SHA256 - } - // We can't have both a key and a security key if options.NOf(c.KeyRef, c.Sk) > 1 { return &options.KeyParseError{} @@ -124,65 +109,29 @@ func (c *VerifyAttestationCommand) Exec(ctx context.Context, images []string) (e Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, MaxWorkers: c.MaxWorkers, - UseSignedTimestamps: c.TSACertChainPath != "" || c.UseSignedTimestamps, - NewBundleFormat: c.NewBundleFormat, + UseSignedTimestamps: c.UseSignedTimestamps, AllowCertificateChain: c.AllowCertificateChain, } - vOfflineKey := verifyOfflineWithKey(c.KeyRef, c.CertRef, c.Sk, co) - - // Auto-detect bundle format for local images - if c.LocalImage { - hasBundles, err := cosign.HasLocalAttestationBundles(images[0]) - if err != nil { - return fmt.Errorf("checking local image format: %w", err) - } - co.NewBundleFormat = hasBundles - } else { - ref, err := name.ParseReference(images[0], c.NameOptions...) - if err == nil && c.NewBundleFormat { - newBundles, _, err := cosign.GetBundles(ctx, ref, co.RegistryClientOpts, c.NameOptions...) - if len(newBundles) == 0 || err != nil { - co.NewBundleFormat = false - } - } - } + vOfflineKey := verifyOfflineWithKey(c.KeyRef, c.Sk, co) if c.CheckClaims { co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier } - err = SetTrustedMaterial(ctx, c.TrustedRootPath, c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath, vOfflineKey, co) + err = SetTrustedMaterial(c.TrustedRootPath, vOfflineKey, co) if err != nil { return fmt.Errorf("setting trusted material: %w", err) } - if err = CheckSigstoreBundleUnsupportedOptions(*c, vOfflineKey, co); err != nil { - return err - } - - err = SetLegacyClientsAndKeys(ctx, c.IgnoreTlog, shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk), keylessVerification(c.KeyRef, c.Sk), c.RekorURL, c.TSACertChainPath, c.CertChain, c.CARoots, c.CAIntermediates, co) - if err != nil { - return fmt.Errorf("setting up clients and keys: %w", err) - } - - // User provides a key or certificate. Otherwise, verification requires a Fulcio certificate - // provided in an attached bundle or OCI annotation. LoadVerifierFromKeyOrCert must be called - // after initializing trust material in order to verify certificate chain. + // User provides a key. Otherwise, verification requires a Fulcio certificate + // provided in an attached bundle or OCI annotation. var closeSV func() - co.SigVerifier, _, closeSV, err = LoadVerifierFromKeyOrCert(ctx, c.KeyRef, c.Slot, c.CertRef, c.CertChain, c.HashAlgorithm, c.Sk, false, co) + co.SigVerifier, closeSV, err = LoadVerifierFromKey(ctx, c.KeyRef, c.Slot, c.Sk) if err != nil { return fmt.Errorf("loading verifierfrom key opts: %w", err) } defer closeSV() - if c.CertRef != "" && c.SCTRef != "" { - sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) - if err != nil { - return fmt.Errorf("reading sct from file: %w", err) - } - co.SCT = sct - } - // NB: There are only 2 kinds of verification right now: // 1. You gave us the public key explicitly to verify against so co.SigVerifier is non-nil or, // 2. We're going to find an x509 certificate on the signature and verify against Fulcio root trust diff --git a/cmd/cosign/cli/verify/verify_attestation_test.go b/cmd/cosign/cli/verify/verify_attestation_test.go index f7c9c9ca262..6f7e17b9578 100644 --- a/cmd/cosign/cli/verify/verify_attestation_test.go +++ b/cmd/cosign/cli/verify/verify_attestation_test.go @@ -27,7 +27,6 @@ func TestVerifyAttestationMissingSubject(t *testing.T) { ctx := context.Background() verifyAttestation := VerifyAttestationCommand{ - CertRef: "cert.pem", CertVerifyOptions: options.CertVerifyOptions{ CertOidcIssuer: "issuer", }, @@ -43,7 +42,6 @@ func TestVerifyAttestationMissingIssuer(t *testing.T) { ctx := context.Background() verifyAttestation := VerifyAttestationCommand{ - CertRef: "cert.pem", CertVerifyOptions: options.CertVerifyOptions{ CertIdentity: "subject", }, diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index f784a63786c..682331c0a3b 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -18,45 +18,24 @@ package verify import ( "bytes" "context" - "crypto" - "crypto/x509" - "encoding/base64" "encoding/hex" - "encoding/json" - "errors" "fmt" "io" - "io/fs" "os" - "path/filepath" "strings" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/internal/ui" "github.com/sigstore/cosign/v3/pkg/blob" "github.com/sigstore/cosign/v3/pkg/cosign" - "github.com/sigstore/cosign/v3/pkg/cosign/bundle" - "github.com/sigstore/cosign/v3/pkg/oci/static" sgbundle "github.com/sigstore/sigstore-go/pkg/bundle" sgverify "github.com/sigstore/sigstore-go/pkg/verify" - - "github.com/sigstore/sigstore/pkg/cryptoutils" ) -func isb64(data []byte) bool { - _, err := base64.StdEncoding.DecodeString(string(data)) - return err == nil -} - // nolint type VerifyBlobCmd struct { options.KeyOpts options.CertVerifyOptions - CertRef string - CAIntermediates string - CARoots string - CertChain string - SigRef string TrustedRootPath string CertGithubWorkflowTrigger string CertGithubWorkflowSHA string @@ -64,36 +43,28 @@ type VerifyBlobCmd struct { CertGithubWorkflowRepository string CertGithubWorkflowRef string IgnoreSCT bool - SCTRef string Offline bool UseSignedTimestamps bool IgnoreTlog bool - HashAlgorithm crypto.Hash AllowCertificateChain bool } // nolint func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { - // always default to sha256 if the algorithm hasn't been explicitly set - if c.HashAlgorithm == 0 { - c.HashAlgorithm = crypto.SHA256 - } - - // Require a certificate/key OR a local bundle file that has the cert. - if options.NOf(c.KeyRef, c.CertRef, c.Sk, c.BundlePath) == 0 { - return fmt.Errorf("provide a key with --key or --sk, a certificate to verify against with --certificate, or a bundle with --bundle") - } - // key and cert identity are mutually exclusive if options.NOf(c.KeyRef, c.CertIdentity, c.CertIdentityRegexp) > 1 { return &options.KeyAndIdentityParseError{} } - // Key, sk, and cert are mutually exclusive. - if options.NOf(c.KeyRef, c.Sk, c.CertRef) > 1 { + // Key and sk are mutually exclusive. + if options.NOf(c.KeyRef, c.Sk) > 1 { return &options.PubKeyParseError{} } + if c.BundlePath == "" { + return fmt.Errorf("please specify --bundle") + } + var identities []cosign.Identity var err error if c.KeyRef == "" && !c.Sk { @@ -113,210 +84,51 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { Identities: identities, Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, - UseSignedTimestamps: c.TSACertChainPath != "" || c.UseSignedTimestamps, + UseSignedTimestamps: c.UseSignedTimestamps, AllowCertificateChain: c.AllowCertificateChain, } - co.NewBundleFormat = c.KeyOpts.NewBundleFormat && checkNewBundle(c.BundlePath, co.BundleOptions()...) - vOfflineKey := verifyOfflineWithKey(c.KeyRef, c.CertRef, c.Sk, co) + vOfflineKey := verifyOfflineWithKey(c.KeyRef, c.Sk, co) - // User provides a key or certificate. Otherwise, verification requires a Fulcio certificate - // provided in an attached bundle or OCI annotation. + // User provides a key. Otherwise, verification requires a Fulcio certificate + // provided in an attached bundle. var closeSV func() - var cert *x509.Certificate - co.SigVerifier, cert, closeSV, err = LoadVerifierFromKeyOrCert(ctx, c.KeyRef, c.Slot, c.CertRef, "", c.HashAlgorithm, c.Sk, true, co) + co.SigVerifier, closeSV, err = LoadVerifierFromKey(ctx, c.KeyRef, c.Slot, c.Sk) if err != nil { return fmt.Errorf("loading verifier from key opts: %w", err) } defer closeSV() - err = SetTrustedMaterial(ctx, c.TrustedRootPath, c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath, vOfflineKey, co) + err = SetTrustedMaterial(c.TrustedRootPath, vOfflineKey, co) if err != nil { return fmt.Errorf("setting trusted material: %w", err) } - if err = CheckSigstoreBundleUnsupportedOptions(*c, vOfflineKey, co); err != nil { - return err - } - - if co.NewBundleFormat { - bundle, err := sgbundle.LoadJSONFromPath(c.BundlePath, co.BundleOptions()...) - if err != nil { - return err - } - - var artifactPolicyOption sgverify.ArtifactPolicyOption - blobBytes, err := payloadBytes(blobRef) - if err != nil { - alg, digest, payloadDigestError := payloadDigest(blobRef) - if payloadDigestError != nil { - return err - } - artifactPolicyOption = sgverify.WithArtifactDigest(alg, digest) - } else { - artifactPolicyOption = sgverify.WithArtifact(bytes.NewReader(blobBytes)) - } - - _, err = cosign.VerifyNewBundle(ctx, co, artifactPolicyOption, bundle) - if err != nil { - return err - } - - ui.Infof(ctx, "Verified OK") - return nil - } - - blobBytes, err := payloadBytes(blobRef) + bundle, err := sgbundle.LoadJSONFromPath(c.BundlePath, co.BundleOptions()...) if err != nil { return err } - if c.TrustedRootPath != "" { - return fmt.Errorf("--trusted-root only supported with --new-bundle-format") - } - if c.RFC3161TimestampPath != "" && !co.UseSignedTimestamps { - return fmt.Errorf("when specifying --rfc3161-timestamp-path, you must also specify --use-signed-timestamps or --timestamp-certificate-chain") - } else if c.RFC3161TimestampPath == "" && co.UseSignedTimestamps { - return fmt.Errorf("when specifying --use-signed-timestamps or --timestamp-certificate-chain, you must also specify --rfc3161-timestamp-path") - } - - err = SetLegacyClientsAndKeys(ctx, c.IgnoreTlog, shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk), keylessVerification(c.KeyRef, c.Sk), c.RekorURL, c.TSACertChainPath, c.CertChain, c.CARoots, c.CAIntermediates, co) + var artifactPolicyOption sgverify.ArtifactPolicyOption + blobBytes, err := payloadBytes(blobRef) if err != nil { - return fmt.Errorf("setting up clients and keys: %w", err) - } - - opts := make([]static.Option, 0) - if c.BundlePath != "" { - b, err := cosign.FetchLocalSignedPayloadFromPath(c.BundlePath) - if err != nil { + alg, digest, payloadDigestError := payloadDigest(blobRef) + if payloadDigestError != nil { return err } - if b.Cert != "" { - certBytes := []byte(b.Cert) - if isb64(certBytes) { - certBytes, _ = base64.StdEncoding.DecodeString(b.Cert) - } - bundleCert, err := loadCertFromPEM(certBytes) - if err != nil { - return fmt.Errorf("loading verifier certificate from bundle: %w", err) - } - // if a cert was passed in, make sure it matches the cert in the bundle - if cert != nil && !cert.Equal(bundleCert) { - return fmt.Errorf("the cert passed in does not match the cert in the provided bundle") - } - cert = bundleCert - } - // A verifier must come either from a certificate from the bundle, - // or provided via --key, --sk, or --certificate. - if co.SigVerifier == nil && cert == nil { - return fmt.Errorf("bundle does not contain cert for verification, please provide public key") - } - - opts = append(opts, static.WithBundle(b.Bundle)) - } - if c.RFC3161TimestampPath != "" { - var rfc3161Timestamp bundle.RFC3161Timestamp - ts, err := blob.LoadFileOrURL(c.RFC3161TimestampPath) - if err != nil { - return err - } - if err := json.Unmarshal(ts, &rfc3161Timestamp); err != nil { - return err - } - opts = append(opts, static.WithRFC3161Timestamp(&rfc3161Timestamp)) - } - // Set an SCT if provided via the CLI. - if c.SCTRef != "" { - sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) - if err != nil { - return fmt.Errorf("reading sct from file: %w", err) - } - co.SCT = sct - } - // Set a cert chain if provided. - var chainPEM []byte - switch { - case c.CertChain != "": - chain, err := loadCertChainFromFileOrURL(c.CertChain) - if err != nil { - return err - } - if chain == nil { - return errors.New("expected certificate chain in --certificate-chain") - } - // Set the last one in the co.RootCerts. This is trusted, as its passed in - // via the CLI. - if co.RootCerts == nil { - co.RootCerts = x509.NewCertPool() - } - co.RootCerts.AddCert(chain[len(chain)-1]) - // Use the whole as the cert chain in the signature object. - // The last one is omitted because it is considered the "root". - chainPEM, err = cryptoutils.MarshalCertificatesToPEM(chain) - if err != nil { - return err - } - case c.CARoots != "": - // CA roots + possible intermediates are already loaded into co.RootCerts with the call to - // loadCertsKeylessVerification above. - } - - // Gather the cert for the signature and add the cert along with the - // cert chain into the signature object. - var certPEM []byte - if cert != nil { - certPEM, err = cryptoutils.MarshalCertificateToPEM(cert) - if err != nil { - return err - } - opts = append(opts, static.WithCertChain(certPEM, chainPEM)) + artifactPolicyOption = sgverify.WithArtifactDigest(alg, digest) + } else { + artifactPolicyOption = sgverify.WithArtifact(bytes.NewReader(blobBytes)) } - sig, err := base64signature(c.SigRef, c.BundlePath) + _, err = cosign.VerifyNewBundle(ctx, co, artifactPolicyOption, bundle) if err != nil { return err } - signature, err := static.NewSignature(blobBytes, sig, opts...) - if err != nil { - return err - } - if _, err = cosign.VerifyBlobSignature(ctx, signature, co); err != nil { - return err - } ui.Infof(ctx, "Verified OK") return nil } -// base64signature returns the base64 encoded signature -func base64signature(sigRef, bundlePath string) (string, error) { - var targetSig []byte - var err error - switch { - case sigRef != "": - targetSig, err = blob.LoadFileOrURL(sigRef) - if err != nil { - if !errors.Is(err, fs.ErrNotExist) { - // ignore if file does not exist, it can be a base64 encoded string as well - return "", err - } - targetSig = []byte(sigRef) - } - case bundlePath != "": - b, err := cosign.FetchLocalSignedPayloadFromPath(bundlePath) - if err != nil { - return "", err - } - targetSig = []byte(b.Base64Signature) - default: - return "", fmt.Errorf("missing flag '--signature'") - } - - if isb64(targetSig) { - return string(targetSig), nil - } - return base64.StdEncoding.EncodeToString(targetSig), nil -} - func payloadBytes(blobRef string) ([]byte, error) { var blobBytes []byte var err error diff --git a/cmd/cosign/cli/verify/verify_blob_attestation.go b/cmd/cosign/cli/verify/verify_blob_attestation.go index 7528d8f5158..a952cd27d16 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -18,11 +18,8 @@ package verify import ( "context" "crypto" - "crypto/x509" - "encoding/base64" "encoding/hex" "encoding/json" - "errors" "fmt" "io" "os" @@ -33,14 +30,11 @@ import ( internal "github.com/sigstore/cosign/v3/internal/pkg/cosign" payloadsize "github.com/sigstore/cosign/v3/internal/pkg/cosign/payload/size" "github.com/sigstore/cosign/v3/internal/ui" - "github.com/sigstore/cosign/v3/pkg/blob" "github.com/sigstore/cosign/v3/pkg/cosign" - "github.com/sigstore/cosign/v3/pkg/cosign/bundle" "github.com/sigstore/cosign/v3/pkg/oci/static" "github.com/sigstore/cosign/v3/pkg/policy" sgbundle "github.com/sigstore/sigstore-go/pkg/bundle" sgverify "github.com/sigstore/sigstore-go/pkg/verify" - "github.com/sigstore/sigstore/pkg/cryptoutils" ) // VerifyBlobAttestationCommand verifies an attestation on a supplied blob @@ -49,10 +43,6 @@ type VerifyBlobAttestationCommand struct { options.KeyOpts options.CertVerifyOptions - CertRef string - CertChain string - CAIntermediates string - CARoots string TrustedRootPath string CertGithubWorkflowTrigger string @@ -62,7 +52,6 @@ type VerifyBlobAttestationCommand struct { CertGithubWorkflowRef string IgnoreSCT bool - SCTRef string Offline bool IgnoreTlog bool @@ -70,30 +59,22 @@ type VerifyBlobAttestationCommand struct { PredicateType string // TODO: Add policies - SignaturePath string // Path to the signature UseSignedTimestamps bool Digest string DigestAlg string - HashAlgorithm crypto.Hash - AllowCertificateChain bool } // Exec runs the verification command func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath string) (err error) { - if options.NOf(c.SignaturePath, c.BundlePath) == 0 { - return fmt.Errorf("please specify path to the DSSE envelope signature via --signature or --bundle") - } - - // always default to sha256 if the algorithm hasn't been explicitly set - if c.HashAlgorithm == 0 { - c.HashAlgorithm = crypto.SHA256 + if c.BundlePath == "" { + return fmt.Errorf("please specify --bundle") } - // Require a certificate/key OR a local bundle file that has the cert. - if options.NOf(c.KeyRef, c.CertRef, c.Sk, c.BundlePath) == 0 { - return fmt.Errorf("provide a key with --key or --sk, a certificate to verify against with --certificate, or a bundle with --bundle") + // Require a key OR a local bundle file that has the cert. + if options.NOf(c.KeyRef, c.Sk, c.BundlePath) == 0 { + return fmt.Errorf("provide a key with --key or --sk, or a bundle with --bundle") } // key and cert identity are mutually exclusive @@ -124,17 +105,15 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st IgnoreSCT: c.IgnoreSCT, Offline: c.Offline, IgnoreTlog: c.IgnoreTlog, - UseSignedTimestamps: c.TSACertChainPath != "" || c.UseSignedTimestamps, + UseSignedTimestamps: c.UseSignedTimestamps, AllowCertificateChain: c.AllowCertificateChain, } - co.NewBundleFormat = c.NewBundleFormat && checkNewBundle(c.BundlePath, co.BundleOptions()...) - vOfflineKey := verifyOfflineWithKey(c.KeyRef, c.CertRef, c.Sk, co) + vOfflineKey := verifyOfflineWithKey(c.KeyRef, c.Sk, co) - // User provides a key or certificate. Otherwise, verification requires a Fulcio certificate - // provided in an attached bundle or OCI annotation. + // User provides a key. Otherwise, verification requires a Fulcio certificate + // provided in an attached bundle. var closeSV func() - var cert *x509.Certificate - co.SigVerifier, cert, closeSV, err = LoadVerifierFromKeyOrCert(ctx, c.KeyRef, c.Slot, c.CertRef, "", c.HashAlgorithm, c.Sk, true, co) + co.SigVerifier, closeSV, err = LoadVerifierFromKey(ctx, c.KeyRef, c.Slot, c.Sk) if err != nil { return fmt.Errorf("loading verifier from key opts: %w", err) } @@ -198,207 +177,67 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st co.ClaimVerifier = cosign.IntotoSubjectClaimVerifier } - err = SetTrustedMaterial(ctx, c.TrustedRootPath, c.CertChain, c.CARoots, c.CAIntermediates, c.TSACertChainPath, vOfflineKey, co) + err = SetTrustedMaterial(c.TrustedRootPath, vOfflineKey, co) if err != nil { return fmt.Errorf("setting trusted material: %w", err) } - if err = CheckSigstoreBundleUnsupportedOptions(*c, vOfflineKey, co); err != nil { + bundle, err := sgbundle.LoadJSONFromPath(c.BundlePath, co.BundleOptions()...) + if err != nil { return err } - if co.NewBundleFormat { - bundle, err := sgbundle.LoadJSONFromPath(c.BundlePath, co.BundleOptions()...) + var policyOpt sgverify.ArtifactPolicyOption + switch { + case !c.CheckClaims: + policyOpt = sgverify.WithoutArtifactUnsafe() + case artifactPath != "": + // Pass the artifact directly so sigstore-go can peek at the bundle + // and choose the correct hash algorithm automatically, rather than + // requiring the caller to supply --digestAlg up-front. + artifactFile, err := os.Open(filepath.Clean(artifactPath)) if err != nil { return err } - - var policyOpt sgverify.ArtifactPolicyOption - switch { - case !c.CheckClaims: - policyOpt = sgverify.WithoutArtifactUnsafe() - case artifactPath != "": - // Pass the artifact directly so sigstore-go can peek at the bundle - // and choose the correct hash algorithm automatically, rather than - // requiring the caller to supply --digestAlg up-front. - artifactFile, err := os.Open(filepath.Clean(artifactPath)) - if err != nil { - return err - } - defer artifactFile.Close() - policyOpt = sgverify.WithArtifact(artifactFile) - default: - policyOpt = sgverify.WithArtifactDigest(h.Algorithm, digest) - } - - _, err = cosign.VerifyNewBundle(ctx, co, policyOpt, bundle) - if err != nil { - return err - } - - sigContent, err := bundle.SignatureContent() - if err != nil { - return fmt.Errorf("fetching signature content: %w", err) - } - - envContent := sigContent.EnvelopeContent() - if envContent == nil { - return fmt.Errorf("bundle does not contain a DSSE envelope") - } - - rawEnv := envContent.RawEnvelope() - if rawEnv == nil { - return fmt.Errorf("bundle does not contain a raw DSSE envelope") - } - - payloadBytes, err := json.Marshal(rawEnv) - if err != nil { - return fmt.Errorf("marshaling envelope: %w", err) - } - - att, err := static.NewAttestation(payloadBytes) - if err != nil { - return fmt.Errorf("creating attestation from envelope: %w", err) - } - - // This checks the predicate type -- if no error is returned and no payload is, then - // the attestation is not of the given predicate type. - b, gotPredicateType, err := policy.AttestationToPayloadJSON(ctx, c.PredicateType, att) - if err != nil { - return fmt.Errorf("converting to consumable policy validation: %w", err) - } - if b == nil { - return fmt.Errorf("invalid predicate type, expected %s got %s", c.PredicateType, gotPredicateType) - } - - ui.Infof(ctx, "Verified OK") - return nil + defer artifactFile.Close() + policyOpt = sgverify.WithArtifact(artifactFile) + default: + policyOpt = sgverify.WithArtifactDigest(h.Algorithm, digest) } - if c.TrustedRootPath != "" { - return fmt.Errorf("--trusted-root only supported with --new-bundle-format") - } - if c.RFC3161TimestampPath != "" && !co.UseSignedTimestamps { - return fmt.Errorf("when specifying --rfc3161-timestamp-path, you must also specify --use-signed-timestamps or --timestamp-certificate-chain") - } else if c.RFC3161TimestampPath == "" && co.UseSignedTimestamps { - return fmt.Errorf("when specifying --use-signed-timestamps or --timestamp-certificate-chain, you must also specify --rfc3161-timestamp-path") - } - - err = SetLegacyClientsAndKeys(ctx, c.IgnoreTlog, shouldVerifySCT(c.IgnoreSCT, c.KeyRef, c.Sk), keylessVerification(c.KeyRef, c.Sk), c.RekorURL, c.TSACertChainPath, c.CertChain, c.CARoots, c.CAIntermediates, co) + _, err = cosign.VerifyNewBundle(ctx, co, policyOpt, bundle) if err != nil { - return fmt.Errorf("setting up clients and keys: %w", err) + return err } - var encodedSig []byte - if c.SignaturePath != "" { - encodedSig, err = os.ReadFile(filepath.Clean(c.SignaturePath)) - if err != nil { - return fmt.Errorf("reading %s: %w", c.SignaturePath, err) - } + sigContent, err := bundle.SignatureContent() + if err != nil { + return fmt.Errorf("fetching signature content: %w", err) } - opts := make([]static.Option, 0) - if c.BundlePath != "" { - b, err := cosign.FetchLocalSignedPayloadFromPath(c.BundlePath) - if err != nil { - return err - } - if b.Cert != "" { - certBytes := []byte(b.Cert) - if isb64(certBytes) { - certBytes, _ = base64.StdEncoding.DecodeString(b.Cert) - } - bundleCert, err := loadCertFromPEM(certBytes) - if err != nil { - return fmt.Errorf("loading verifier certificate from bundle: %w", err) - } - // if a cert was passed in, make sure it matches the cert in the bundle - if cert != nil && !cert.Equal(bundleCert) { - return fmt.Errorf("the cert passed in does not match the cert in the provided bundle") - } - cert = bundleCert - } - // A verifier must come either from a certificate from the bundle, - // or provided via --key, --sk, or --certificate. - if co.SigVerifier == nil && cert == nil { - return fmt.Errorf("bundle does not contain cert for verification, please provide public key") - } - - encodedSig, err = base64.StdEncoding.DecodeString(b.Base64Signature) - if err != nil { - return fmt.Errorf("decoding signature: %w", err) - } - opts = append(opts, static.WithBundle(b.Bundle)) - } - if c.RFC3161TimestampPath != "" { - var rfc3161Timestamp bundle.RFC3161Timestamp - ts, err := blob.LoadFileOrURL(c.RFC3161TimestampPath) - if err != nil { - return err - } - if err := json.Unmarshal(ts, &rfc3161Timestamp); err != nil { - return err - } - opts = append(opts, static.WithRFC3161Timestamp(&rfc3161Timestamp)) - } - // Set an SCT if provided via the CLI. - if c.SCTRef != "" { - sct, err := os.ReadFile(filepath.Clean(c.SCTRef)) - if err != nil { - return fmt.Errorf("reading sct from file: %w", err) - } - co.SCT = sct - } - // Set a cert chain if provided. - var chainPEM []byte - if c.CertChain != "" { - chain, err := loadCertChainFromFileOrURL(c.CertChain) - if err != nil { - return err - } - if chain == nil { - return errors.New("expected certificate chain in --certificate-chain") - } - // Set the last one in the co.RootCerts. This is trusted, as its passed in - // via the CLI. - if co.RootCerts == nil { - co.RootCerts = x509.NewCertPool() - } - co.RootCerts.AddCert(chain[len(chain)-1]) - // Use the whole as the cert chain in the signature object. - // The last one is omitted because it is considered the "root". - chainPEM, err = cryptoutils.MarshalCertificatesToPEM(chain) - if err != nil { - return err - } + envContent := sigContent.EnvelopeContent() + if envContent == nil { + return fmt.Errorf("bundle does not contain a DSSE envelope") } - // Gather the cert for the signature and add the cert along with the - // cert chain into the signature object. - var certPEM []byte - if cert != nil { - certPEM, err = cryptoutils.MarshalCertificateToPEM(cert) - if err != nil { - return err - } - opts = append(opts, static.WithCertChain(certPEM, chainPEM)) + rawEnv := envContent.RawEnvelope() + if rawEnv == nil { + return fmt.Errorf("bundle does not contain a raw DSSE envelope") } - signature, err := static.NewAttestation(encodedSig, opts...) + payloadBytes, err := json.Marshal(rawEnv) if err != nil { - return err + return fmt.Errorf("marshaling envelope: %w", err) } - // TODO: This verifier only supports verification of a single signer/signature on - // the envelope. Either have the verifier validate that only one signature exists, - // or use a multi-signature verifier. - if _, err = cosign.VerifyBlobAttestation(ctx, signature, h, co); err != nil { - return err + att, err := static.NewAttestation(payloadBytes) + if err != nil { + return fmt.Errorf("creating attestation from envelope: %w", err) } // This checks the predicate type -- if no error is returned and no payload is, then // the attestation is not of the given predicate type. - b, gotPredicateType, err := policy.AttestationToPayloadJSON(ctx, c.PredicateType, signature) + b, gotPredicateType, err := policy.AttestationToPayloadJSON(ctx, c.PredicateType, att) if err != nil { return fmt.Errorf("converting to consumable policy validation: %w", err) } @@ -406,7 +245,7 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st return fmt.Errorf("invalid predicate type, expected %s got %s", c.PredicateType, gotPredicateType) } - fmt.Fprintln(os.Stderr, "Verified OK") + ui.Infof(ctx, "Verified OK") return nil } diff --git a/cmd/cosign/cli/verify/verify_blob_attestation_test.go b/cmd/cosign/cli/verify/verify_blob_attestation_test.go index 3896083916e..f42cdf3e82d 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation_test.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation_test.go @@ -69,62 +69,67 @@ func TestVerifyBlobAttestation(t *testing.T) { hugeBlobPath := writeBlobFile(t, td, hugeBlobContents, "huge-blob") keyRef := writeBlobFile(t, td, pubkey, "cosign.pub") + bundleSLSAProvenance := makeBundleFromDSSE(t, blobSLSAProvenanceSignature) + bundleEmptySubject := makeBundleFromDSSE(t, dssePredicateEmptySubject) + bundleMissingSha256 := makeBundleFromDSSE(t, dssePredicateMissingSha256) + bundleMultipleSubjects := makeBundleFromDSSE(t, dssePredicateMultipleSubjects) + bundleMultipleSubjectsInvalid := makeBundleFromDSSE(t, dssePredicateMultipleSubjectsInvalid) + tests := []struct { description string blobPath string digest string bundlePath string - signature string predicateType string env map[string]string shouldErr bool }{ { description: "verify a slsaprovenance predicate", + bundlePath: bundleSLSAProvenance, predicateType: "slsaprovenance", blobPath: blobPath, - signature: blobSLSAProvenanceSignature, }, { description: "fail with incorrect predicate", - signature: blobSLSAProvenanceSignature, + bundlePath: bundleSLSAProvenance, blobPath: blobPath, predicateType: "custom", shouldErr: true, }, { description: "fail with incorrect blob", - signature: blobSLSAProvenanceSignature, + bundlePath: bundleSLSAProvenance, blobPath: anotherBlobPath, shouldErr: true, }, { description: "dsse envelope predicate has no subject", - signature: dssePredicateEmptySubject, + bundlePath: bundleEmptySubject, blobPath: blobPath, shouldErr: true, }, { description: "dsse envelope predicate missing sha256 digest", - signature: dssePredicateMissingSha256, + bundlePath: bundleMissingSha256, blobPath: blobPath, shouldErr: true, }, { description: "dsse envelope has multiple subjects, one is valid", + bundlePath: bundleMultipleSubjects, predicateType: "slsaprovenance", - signature: dssePredicateMultipleSubjects, blobPath: blobPath, }, { description: "dsse envelope has multiple subjects, one is valid, but we are looking for different predicatetype", + bundlePath: bundleMultipleSubjects, predicateType: "notreallyslsaprovenance", - signature: dssePredicateMultipleSubjects, blobPath: blobPath, shouldErr: true, }, { description: "dsse envelope has multiple subjects, none has correct sha256 digest", + bundlePath: bundleMultipleSubjectsInvalid, predicateType: "slsaprovenance", - signature: dssePredicateMultipleSubjectsInvalid, blobPath: blobPath, shouldErr: true, }, { description: "override file size limit", - signature: blobSLSAProvenanceSignature, + bundlePath: bundleSLSAProvenance, blobPath: hugeBlobPath, env: map[string]string{"COSIGN_MAX_ATTACHMENT_SIZE": "128"}, shouldErr: true, @@ -142,10 +147,10 @@ func TestVerifyBlobAttestation(t *testing.T) { shouldErr: true, }, { description: "verify with digest instead of blob", + bundlePath: bundleSLSAProvenance, predicateType: "slsaprovenance", blobPath: "", digest: blobSha256, - signature: blobSLSAProvenanceSignature, }, } @@ -154,15 +159,9 @@ func TestVerifyBlobAttestation(t *testing.T) { for k, v := range test.env { t.Setenv(k, v) } - decodedSig, err := base64.StdEncoding.DecodeString(test.signature) - if err != nil { - t.Fatal(err) - } - sigRef := writeBlobFile(t, td, string(decodedSig), "signature") cmd := VerifyBlobAttestationCommand{ KeyOpts: options.KeyOpts{KeyRef: keyRef}, - SignaturePath: sigRef, IgnoreTlog: true, CheckClaims: true, PredicateType: test.predicateType, @@ -173,10 +172,9 @@ func TestVerifyBlobAttestation(t *testing.T) { } if test.bundlePath != "" { cmd.BundlePath = test.bundlePath - cmd.NewBundleFormat = true cmd.TrustedRootPath = writeTrustedRootFile(t, td, "{\"mediaType\":\"application/vnd.dev.sigstore.trustedroot+json;version=0.1\"}") } - err = cmd.Exec(ctx, test.blobPath) + err := cmd.Exec(ctx, test.blobPath) if (err != nil) != test.shouldErr { t.Fatalf("verifyBlobAttestation()= %s, expected shouldErr=%t ", err, test.shouldErr) @@ -194,22 +192,23 @@ func TestVerifyBlobAttestationNoCheckClaims(t *testing.T) { anotherBlobPath := writeBlobFile(t, td, anotherBlobContents, "other-blob") keyRef := writeBlobFile(t, td, pubkey, "cosign.pub") + bundleSLSAProvenance := makeBundleFromDSSE(t, blobSLSAProvenanceSignature) + tests := []struct { description string blobPath string - signature string bundlePath string }{ { description: "verify a predicate", + bundlePath: bundleSLSAProvenance, blobPath: blobPath, - signature: blobSLSAProvenanceSignature, }, { description: "verify a predicate no path", - signature: blobSLSAProvenanceSignature, + bundlePath: bundleSLSAProvenance, }, { description: "verify a predicate with another blob path", - signature: blobSLSAProvenanceSignature, + bundlePath: bundleSLSAProvenance, // This works because we're not checking the claims. It doesn't matter what we put in here - it should pass so long as the DSSE signagure can be verified. blobPath: anotherBlobPath, }, { @@ -219,29 +218,21 @@ func TestVerifyBlobAttestationNoCheckClaims(t *testing.T) { blobPath: anotherBlobPath, }, { description: "verify a predicate with /dev/null", - signature: blobSLSAProvenanceSignature, + bundlePath: bundleSLSAProvenance, blobPath: "/dev/null", }, } for _, test := range tests { t.Run(test.description, func(t *testing.T) { - decodedSig, err := base64.StdEncoding.DecodeString(test.signature) - if err != nil { - t.Fatal(err) - } - sigRef := writeBlobFile(t, td, string(decodedSig), "signature") - cmd := VerifyBlobAttestationCommand{ KeyOpts: options.KeyOpts{KeyRef: keyRef}, - SignaturePath: sigRef, IgnoreTlog: true, CheckClaims: false, PredicateType: "slsaprovenance", } if test.bundlePath != "" { cmd.BundlePath = test.bundlePath - cmd.NewBundleFormat = true cmd.TrustedRootPath = writeTrustedRootFile(t, td, "{\"mediaType\":\"application/vnd.dev.sigstore.trustedroot+json;version=0.1\"}") } if err := cmd.Exec(ctx, test.blobPath); err != nil { @@ -415,83 +406,35 @@ func TestVerifyBlobAttestation_MalformedPayloads(t *testing.T) { if err != nil { t.Fatalf("failed to setup envelope: %v", err) } + var envJSON struct { + PayloadType string `json:"payloadType"` + Payload string `json:"payload"` + Signatures []struct { + Sig string `json:"sig"` + } `json:"signatures"` + } + if err := json.Unmarshal([]byte(sigStr), &envJSON); err != nil { + t.Fatalf("failed to unmarshal dsse envelope: %v", err) + } + if len(envJSON.Signatures) == 0 { + t.Fatalf("no signatures in dsse envelope") + } + bundlePath := makeLocalAttestNewBundle(t, envJSON.Payload, envJSON.PayloadType, envJSON.Signatures[0].Sig) - t.Run("Standalone Signature", func(t *testing.T) { - sigRef := writeBlobFile(t, td, sigStr, "signature") - - cmd := VerifyBlobAttestationCommand{ - KeyOpts: options.KeyOpts{KeyRef: keyRef}, - SignaturePath: sigRef, - IgnoreTlog: true, - CheckClaims: false, - PredicateType: tc.predicateType, - } - - err = cmd.Exec(ctx, blobPath) - if err == nil { - t.Fatalf("[%s Standalone] FAIL: swallowed error, returned Verified OK", tc.description) - } else { - t.Logf("[%s Standalone] PASS: returned error: %v", tc.description, err) - } - }) - - t.Run("Old Bundle Format", func(t *testing.T) { - bundleData := map[string]interface{}{ - "base64Signature": base64.StdEncoding.EncodeToString([]byte(sigStr)), - "cert": string(pubKeyBytes), - } - bundleBytes, err := json.Marshal(bundleData) - if err != nil { - t.Fatalf("failed to marshal old bundle: %v", err) - } - bundleRef := writeBlobFile(t, td, string(bundleBytes), "bundle.json") - - cmd := VerifyBlobAttestationCommand{ - KeyOpts: options.KeyOpts{KeyRef: keyRef, BundlePath: bundleRef}, - IgnoreTlog: true, - CheckClaims: false, - PredicateType: tc.predicateType, - } - - err = cmd.Exec(ctx, blobPath) - if err == nil { - t.Fatalf("[%s Old Bundle] FAIL: swallowed error, returned Verified OK", tc.description) - } else { - t.Logf("[%s Old Bundle] PASS: returned error: %v", tc.description, err) - } - }) - - t.Run("New Bundle Format", func(t *testing.T) { - var envJSON struct { - PayloadType string `json:"payloadType"` - Payload string `json:"payload"` - Signatures []struct { - Sig string `json:"sig"` - } `json:"signatures"` - } - if err := json.Unmarshal([]byte(sigStr), &envJSON); err != nil { - t.Fatalf("failed to unmarshal dsse envelope: %v", err) - } - if len(envJSON.Signatures) == 0 { - t.Fatalf("no signatures in dsse envelope") - } - bundlePath := makeLocalAttestNewBundle(t, envJSON.Payload, envJSON.PayloadType, envJSON.Signatures[0].Sig) - - cmd := VerifyBlobAttestationCommand{ - KeyOpts: options.KeyOpts{KeyRef: keyRef, BundlePath: bundlePath, NewBundleFormat: true}, - IgnoreTlog: true, - CheckClaims: false, - PredicateType: tc.predicateType, - TrustedRootPath: writeTrustedRootFile(t, td, "{\"mediaType\":\"application/vnd.dev.sigstore.trustedroot+json;version=0.1\"}"), - } + cmd := VerifyBlobAttestationCommand{ + KeyOpts: options.KeyOpts{KeyRef: keyRef, BundlePath: bundlePath}, + IgnoreTlog: true, + CheckClaims: false, + PredicateType: tc.predicateType, + TrustedRootPath: writeTrustedRootFile(t, td, "{\"mediaType\":\"application/vnd.dev.sigstore.trustedroot+json;version=0.1\"}"), + } - err = cmd.Exec(ctx, blobPath) - if err == nil { - t.Fatalf("[%s New Bundle] FAIL: swallowed error, returned Verified OK", tc.description) - } else { - t.Logf("[%s New Bundle] PASS: returned error: %v", tc.description, err) - } - }) + err = cmd.Exec(ctx, blobPath) + if err == nil { + t.Fatalf("[%s] FAIL: swallowed error, returned Verified OK", tc.description) + } else { + t.Logf("[%s] PASS: returned error: %v", tc.description, err) + } }) } } @@ -595,61 +538,23 @@ func TestVerifyBlobAttestationSkWithoutIdentities(t *testing.T) { } } -func TestVerifyBlobAttestationLegacyBundlePublicKey(t *testing.T) { - ctx := context.Background() - td := t.TempDir() - - blobPath := writeBlobFile(t, td, blobContents, "blob") - keyRef := writeBlobFile(t, td, pubkey, "cosign.pub") - - t.Run("legacy bundle with public key in cert field fails", func(t *testing.T) { - bundleData := map[string]any{ - "base64Signature": blobSLSAProvenanceSignature, - "cert": pubkey, - } - bundleBytes, err := json.Marshal(bundleData) - if err != nil { - t.Fatalf("failed to marshal old bundle: %v", err) - } - bundleRef := writeBlobFile(t, td, string(bundleBytes), "bundle.json") - - cmd := VerifyBlobAttestationCommand{ - KeyOpts: options.KeyOpts{KeyRef: keyRef, BundlePath: bundleRef}, - IgnoreTlog: true, - CheckClaims: true, - PredicateType: "slsaprovenance", - } - - err = cmd.Exec(ctx, blobPath) - if err == nil { - t.Fatal("expected error when bundle cert field contains a public key, got nil") - } - if !strings.Contains(err.Error(), "loading verifier certificate from bundle") { - t.Fatalf("expected error containing 'loading verifier certificate from bundle', got: %v", err) - } - }) - - t.Run("legacy bundle with empty cert field succeeds when key is provided via KeyRef", func(t *testing.T) { - bundleData := map[string]any{ - "base64Signature": blobSLSAProvenanceSignature, - "cert": "", - } - bundleBytes, err := json.Marshal(bundleData) - if err != nil { - t.Fatalf("failed to marshal old bundle: %v", err) - } - bundleRef := writeBlobFile(t, td, string(bundleBytes), "bundle.json") - - cmd := VerifyBlobAttestationCommand{ - KeyOpts: options.KeyOpts{KeyRef: keyRef, BundlePath: bundleRef}, - IgnoreTlog: true, - CheckClaims: true, - PredicateType: "slsaprovenance", - } - - err = cmd.Exec(ctx, blobPath) - if err != nil { - t.Fatalf("expected success when bundle cert field is empty and key provided via KeyRef, got: %v", err) - } - }) +func makeBundleFromDSSE(t *testing.T, dsseBase64 string) string { + decodedDSSE, err := base64.StdEncoding.DecodeString(dsseBase64) + if err != nil { + t.Fatal(err) + } + var envJSON struct { + PayloadType string `json:"payloadType"` + Payload string `json:"payload"` + Signatures []struct { + Sig string `json:"sig"` + } `json:"signatures"` + } + if err := json.Unmarshal(decodedDSSE, &envJSON); err != nil { + t.Fatal(err) + } + if len(envJSON.Signatures) == 0 { + t.Fatal("no signatures in dsse envelope") + } + return makeLocalAttestNewBundle(t, envJSON.Payload, envJSON.PayloadType, envJSON.Signatures[0].Sig) } diff --git a/cmd/cosign/cli/verify/verify_blob_test.go b/cmd/cosign/cli/verify/verify_blob_test.go index 9986e201462..c09ff0d7d3d 100644 --- a/cmd/cosign/cli/verify/verify_blob_test.go +++ b/cmd/cosign/cli/verify/verify_blob_test.go @@ -22,111 +22,26 @@ import ( "crypto/elliptic" "crypto/rand" "crypto/sha256" - "crypto/x509" - "encoding/base64" - "encoding/hex" "encoding/json" "errors" - "fmt" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" - "time" - "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" - "github.com/go-openapi/runtime" - "github.com/go-openapi/swag/conv" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa/mock" - "github.com/sigstore/cosign/v3/internal/test" - "github.com/sigstore/cosign/v3/pkg/cosign" - "github.com/sigstore/cosign/v3/pkg/cosign/bundle" sigs "github.com/sigstore/cosign/v3/pkg/signature" - ctypes "github.com/sigstore/cosign/v3/pkg/types" protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" protocommon "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1" "github.com/sigstore/rekor/pkg/generated/models" - "github.com/sigstore/rekor/pkg/pki" - "github.com/sigstore/rekor/pkg/types" - rekor_dsse "github.com/sigstore/rekor/pkg/types/dsse" - "github.com/sigstore/rekor/pkg/types/hashedrekord" - hashedrekord_v001 "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1" - "github.com/sigstore/rekor/pkg/types/intoto" - "github.com/sigstore/rekor/pkg/types/rekord" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" - "github.com/sigstore/sigstore/pkg/signature/dsse" signatureoptions "github.com/sigstore/sigstore/pkg/signature/options" "google.golang.org/protobuf/encoding/protojson" ) -func TestSignaturesRef(t *testing.T) { - sig := "a==" - b64sig := "YT09" - tests := []struct { - description string - sigRef string - shouldErr bool - }{ - { - description: "raw sig", - sigRef: sig, - }, - { - description: "encoded sig", - sigRef: b64sig, - }, { - description: "empty ref", - shouldErr: true, - }, - } - - for _, test := range tests { - t.Run(test.description, func(t *testing.T) { - gotSig, err := base64signature(test.sigRef, "") - if test.shouldErr && err != nil { - return - } - if test.shouldErr { - t.Fatal("should have received an error") - } - if gotSig != b64sig { - t.Fatalf("unexpected signature, expected: %s got: %s", sig, gotSig) - } - }) - } -} - -func TestSignaturesBundle(t *testing.T) { - td := t.TempDir() - fp := filepath.Join(td, "file") - - b64sig := "YT09" - - // save as a LocalSignedPayload to the file - lsp := cosign.LocalSignedPayload{ - Base64Signature: b64sig, - } - contents, err := json.Marshal(lsp) - if err != nil { - t.Fatal(err) - } - if err := os.WriteFile(fp, contents, 0644); err != nil { - t.Fatal(err) - } - - gotSig, err := base64signature("", fp) - if err != nil { - t.Fatal(err) - } - if gotSig != b64sig { - t.Fatalf("unexpected signature, expected: %s got: %s", b64sig, gotSig) - } -} - // Does not test identity options, only blob verification with different // options. func TestVerifyBlob(t *testing.T) { @@ -149,31 +64,6 @@ func TestVerifyBlob(t *testing.T) { // Generate expired and unexpired certificates identity := "hello@foo.com" issuer := "issuer" - rootCert, rootPriv, _ := test.GenerateRootCa() - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - chain, _ := cryptoutils.MarshalCertificatesToPEM([]*x509.Certificate{rootCert}) - chainPath := writeBlobFile(t, td, string(chain), "chain.pem") - - unexpiredLeafCert, _ := test.GenerateLeafCertWithExpiration(identity, issuer, - time.Now(), leafPriv, rootCert, rootPriv) - unexpiredCertPem, _ := cryptoutils.MarshalCertificateToPEM(unexpiredLeafCert) - - expiredLeafCert, _ := test.GenerateLeafCertWithExpiration(identity, issuer, - time.Now().Add(-10*time.Minute), leafPriv, rootCert, rootPriv) - expiredLeafPem, _ := cryptoutils.MarshalCertificateToPEM(expiredLeafCert) - - unrelatedPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal(err) - } - unrelatedSigner, err := signature.LoadECDSASignerVerifier(unrelatedPriv, crypto.SHA256) - if err != nil { - t.Fatal(err) - } - unrelatedLeafCert, _ := test.GenerateLeafCertWithExpiration(identity, issuer, - time.Now(), unrelatedPriv, rootCert, rootPriv) - unrelatedCertPem, _ := cryptoutils.MarshalCertificateToPEM(unrelatedLeafCert) // Make rekor signer rekorPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) @@ -204,54 +94,10 @@ func TestVerifyBlob(t *testing.T) { otherBytes := []byte("bar") otherSignature := makeSignature(otherBytes, signer) - unrelatedSignature := makeSignature(blobBytes, unrelatedSigner) - - // initialize timestamp for expired and unexpired certificates - expiredTSAOpts := mock.TSAClientOptions{Time: time.Now().Add(-1 * time.Minute), Message: []byte(blobSignature)} - unexpiredTSAOpts := mock.TSAClientOptions{Time: time.Now(), Message: []byte(blobSignature)} - tsaClient, err := mock.NewTSAClient(expiredTSAOpts) - if err != nil { - t.Fatal(err) - } - certChainPEM, err := cryptoutils.MarshalCertificatesToPEM(tsaClient.CertChain) - if err != nil { - t.Fatalf("unexpected error marshalling cert chain: %v", err) - } - expiredTSACertChainPath := filepath.Join(td, "exptsacertchain.pem") - if err := os.WriteFile(expiredTSACertChainPath, certChainPEM, 0644); err != nil { - t.Fatal(err) - } - tsr, err := tsaClient.GetTimestampResponse(nil) - if err != nil { - t.Fatalf("unable to generate a timestamp response: %v", err) - } - rfc3161Timestamp := &bundle.RFC3161Timestamp{SignedRFC3161Timestamp: tsr} - expiredTSPath := writeTimestampFile(t, td, rfc3161Timestamp, "expiredrfc3161TS.json") - tsaClient, err = mock.NewTSAClient(unexpiredTSAOpts) - if err != nil { - t.Fatal(err) - } - tsr, err = tsaClient.GetTimestampResponse(nil) - if err != nil { - t.Fatalf("unable to generate a timestamp response: %v", err) - } - rfc3161Timestamp = &bundle.RFC3161Timestamp{SignedRFC3161Timestamp: tsr} - unexpiredTSPath := writeTimestampFile(t, td, rfc3161Timestamp, "unexpiredrfc3161TS.json") - certChainPEM, err = cryptoutils.MarshalCertificatesToPEM(tsaClient.CertChain) - if err != nil { - t.Fatalf("unexpected error marshalling cert chain: %v", err) - } - unexpiredTSACertChainPath := filepath.Join(td, "unexptsacertchain.pem") - if err := os.WriteFile(unexpiredTSACertChainPath, certChainPEM, 0644); err != nil { - t.Fatal(err) - } - tts := []struct { name string blob []byte - signature string key []byte - cert *x509.Certificate bundlePath string newBundle bool // The rekor entry response when Rekor is enabled @@ -261,124 +107,18 @@ func TestVerifyBlob(t *testing.T) { expectedErr string tsPath string tsChainPath string - }{ - { - name: "valid signature with public key", - blob: blobBytes, - signature: blobSignature, - key: pubKeyBytes, - shouldErr: false, - skipTlogVerify: true, - }, - { - name: "valid signature with public key - no rekor fail", - blob: blobBytes, - signature: blobSignature, - key: pubKeyBytes, - rekorEntry: nil, - shouldErr: true, - expectedErr: "signature not found in transparency log", - }, - { - name: "valid signature with public key - rekor entry success", - blob: blobBytes, - signature: blobSignature, - key: pubKeyBytes, - rekorEntry: []*models.LogEntry{makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), - pubKeyBytes, true)}, - shouldErr: false, - }, - { - name: "valid signature with public key - good bundle provided fails when public key is in cert", - blob: blobBytes, - signature: blobSignature, - key: pubKeyBytes, - bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), - pubKeyBytes, true), - shouldErr: true, - expectedErr: "loading verifier certificate from bundle", - }, - { - name: "valid signature with public key - good bundle provided succeeds when key is provided via KeyRef", - blob: blobBytes, - signature: blobSignature, - key: pubKeyBytes, - bundlePath: makeLocalBundleWithoutCert(t, *rekorSigner, blobBytes, []byte(blobSignature), - pubKeyBytes, true), - shouldErr: false, - }, - { - name: "valid signature with public key - bundle without rekor bundle fails", - blob: blobBytes, - signature: blobSignature, - key: pubKeyBytes, - bundlePath: makeLocalBundleWithoutRekorBundle(t, []byte(blobSignature), nil), - shouldErr: true, - expectedErr: "signature not found in transparency log", - }, - { - name: "valid signature with public key - bad bundle SET", - blob: blobBytes, - signature: blobSignature, - key: pubKeyBytes, - bundlePath: makeLocalBundle(t, *signer, blobBytes, []byte(blobSignature), - unexpiredCertPem, true), - shouldErr: true, - expectedErr: "rekor log public key not found for payload", - }, - { - name: "valid signature with public key - bad bundle cert mismatch", - blob: blobBytes, - signature: unrelatedSignature, - key: pubKeyBytes, - bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(unrelatedSignature), - unrelatedCertPem, true), - shouldErr: true, - expectedErr: "both public key and certificate were provided but did not match", - }, - { - name: "valid signature with public key and bundle cert derived from public key", - blob: blobBytes, - signature: blobSignature, - key: pubKeyBytes, - bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), - unexpiredCertPem, true), - shouldErr: false, - }, - { - name: "valid signature with public key - bad bundle signature mismatch", - blob: blobBytes, - signature: blobSignature, - key: pubKeyBytes, - bundlePath: makeLocalBundleWithoutCert(t, *rekorSigner, blobBytes, []byte(makeSignature(blobBytes, signer)), - pubKeyBytes, true), - shouldErr: true, - expectedErr: "signature in bundle does not match signature being verified", - }, - { - name: "valid signature with public key - bad bundle msg & signature mismatch", - blob: blobBytes, - signature: blobSignature, - key: pubKeyBytes, - bundlePath: makeLocalBundleWithoutCert(t, *rekorSigner, otherBytes, []byte(otherSignature), - pubKeyBytes, true), - shouldErr: true, - expectedErr: "signature in bundle does not match signature being verified", - }, - { - name: "valid signature with public key - new bundle", - blob: blobBytes, - signature: "", - key: pubKeyBytes, - bundlePath: makeLocalNewBundle(t, []byte(blobSignature), sha256.Sum256(blobBytes)), - newBundle: true, - skipTlogVerify: true, - shouldErr: false, - }, + }{{ + name: "valid signature with public key - new bundle", + blob: blobBytes, + key: pubKeyBytes, + bundlePath: makeLocalNewBundle(t, []byte(blobSignature), sha256.Sum256(blobBytes)), + newBundle: true, + skipTlogVerify: true, + shouldErr: false, + }, { name: "invalid signature with public key - new bundle", blob: blobBytes, - signature: "", key: pubKeyBytes, bundlePath: makeLocalNewBundle(t, []byte(otherSignature), sha256.Sum256(blobBytes)), newBundle: true, @@ -386,234 +126,6 @@ func TestVerifyBlob(t *testing.T) { shouldErr: true, expectedErr: "invalid signature", }, - { - name: "invalid signature with public key", - blob: blobBytes, - signature: otherSignature, - key: pubKeyBytes, - shouldErr: true, - expectedErr: "signature not found in transparency log", - }, - { - name: "invalid signature with public key and no Rekor entry", - blob: blobBytes, - signature: otherSignature, - key: pubKeyBytes, - shouldErr: true, - expectedErr: "signature not found in transparency log", - }, - { - name: "valid signature with unexpired certificate - no rekor entry", - blob: blobBytes, - signature: blobSignature, - cert: unexpiredLeafCert, - shouldErr: true, - expectedErr: "signature not found in transparency log", - }, - { - name: "valid signature with unexpired certificate - bad bundle signature mismatch", - blob: blobBytes, - signature: blobSignature, - cert: unexpiredLeafCert, - bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(makeSignature(blobBytes, signer)), - unexpiredCertPem, true), - shouldErr: true, - expectedErr: "signature in bundle does not match signature being verified", - }, - { - name: "valid signature with unexpired certificate - bad bundle msg & signature mismatch", - blob: blobBytes, - signature: blobSignature, - cert: unexpiredLeafCert, - bundlePath: makeLocalBundle(t, *rekorSigner, otherBytes, []byte(otherSignature), - unexpiredCertPem, true), - shouldErr: true, - expectedErr: "signature in bundle does not match signature being verified", - }, - { - name: "invalid signature with unexpired certificate and no Rekor entry, expect Rekor", - blob: blobBytes, - signature: otherSignature, - cert: unexpiredLeafCert, - shouldErr: true, - expectedErr: "signature not found in transparency log", - }, - { - name: "valid signature with unexpired certificate and Rekor proof", - blob: blobBytes, - signature: blobSignature, - cert: unexpiredLeafCert, - rekorEntry: []*models.LogEntry{makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), - unexpiredCertPem, true)}, - shouldErr: false, - }, - { - name: "valid signature with unexpired certificate - rekor entry found", - blob: blobBytes, - signature: blobSignature, - cert: unexpiredLeafCert, - rekorEntry: []*models.LogEntry{makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), - unexpiredCertPem, true)}, - shouldErr: false, - }, - { - name: "valid signature with expired certificate and no proof, expect Rekor", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - shouldErr: true, - expectedErr: "signature not found in transparency log", - }, - { - name: "valid signature with expired certificate, no Rekor", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - skipTlogVerify: true, - shouldErr: true, - expectedErr: "expected a signed timestamp to verify an expired certificate", - }, - { - name: "valid signature with expired certificate - good rekor lookup", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - rekorEntry: []*models.LogEntry{makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), - expiredLeafPem, true)}, - shouldErr: false, - }, - { - name: "valid signature with expired certificate - multiple rekor entries", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - rekorEntry: []*models.LogEntry{makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), - expiredLeafPem, true), makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), - expiredLeafPem, true)}, - shouldErr: false, - }, - { - name: "valid signature with expired certificate - bad rekor integrated time", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - rekorEntry: []*models.LogEntry{makeRekorEntry(t, *rekorSigner, blobBytes, []byte(blobSignature), - expiredLeafPem, false)}, - shouldErr: true, - expectedErr: "certificate expired before observed time", - }, - - { - name: "valid signature with unexpired certificate - good bundle", - blob: blobBytes, - signature: blobSignature, - cert: unexpiredLeafCert, - bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), - unexpiredCertPem, true), - shouldErr: false, - }, - { - name: "valid signature with expired certificate - good bundle", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), - expiredLeafPem, true), - shouldErr: false, - }, - { - name: "valid signature with expired certificate - bundle with bad expiration", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), - expiredLeafPem, false), - shouldErr: true, - expectedErr: "certificate expired before observed time", - }, - { - name: "valid signature with expired certificate - bundle with bad SET", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - bundlePath: makeLocalBundle(t, *signer, blobBytes, []byte(blobSignature), - expiredLeafPem, true), - shouldErr: true, - expectedErr: "rekor log public key not found for payload", - }, - { - name: "valid signature with expired certificate - good bundle", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), - expiredLeafPem, true), - shouldErr: false, - }, - { - name: "valid signature with expired certificate - bad rekor entry", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - // This is the wrong signer for the SET! - rekorEntry: []*models.LogEntry{makeRekorEntry(t, *signer, blobBytes, []byte(blobSignature), - expiredLeafPem, true)}, - shouldErr: true, - expectedErr: "rekor log public key not found for payload", - }, - { - name: "valid signature with expired certificate - good bundle, good timestamp", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), - expiredLeafPem, true), - tsPath: expiredTSPath, - tsChainPath: expiredTSACertChainPath, - shouldErr: false, - }, - { - name: "valid signature with expired certificate - no bundle, good timestamp", - blob: blobBytes, - signature: blobSignature, - cert: expiredLeafCert, - tsPath: expiredTSPath, - tsChainPath: expiredTSACertChainPath, - skipTlogVerify: true, - shouldErr: false, - }, - { - name: "mismatched signature with expired certificate", - blob: otherBytes, - signature: otherSignature, - cert: expiredLeafCert, - tsPath: expiredTSPath, - tsChainPath: expiredTSACertChainPath, - skipTlogVerify: true, - shouldErr: true, - expectedErr: "hashed messages don't match", - }, - { - name: "valid signature with unexpired certificate - good bundle, good timestamp", - blob: blobBytes, - signature: blobSignature, - cert: unexpiredLeafCert, - bundlePath: makeLocalBundle(t, *rekorSigner, blobBytes, []byte(blobSignature), - unexpiredCertPem, true), - tsPath: unexpiredTSPath, - tsChainPath: unexpiredTSACertChainPath, - shouldErr: false, - }, - { - name: "valid signature with unexpired certificate - no bundle, good timestamp", - blob: blobBytes, - signature: blobSignature, - cert: unexpiredLeafCert, - tsPath: unexpiredTSPath, - tsChainPath: unexpiredTSACertChainPath, - skipTlogVerify: true, - shouldErr: false, - }, } for _, tt := range tts { t.Run(tt.name, func(t *testing.T) { @@ -633,8 +145,6 @@ func TestVerifyBlob(t *testing.T) { cmd := VerifyBlobCmd{ KeyOpts: options.KeyOpts{ BundlePath: tt.bundlePath, - NewBundleFormat: tt.newBundle, - RekorURL: testServer.URL, RFC3161TimestampPath: tt.tsPath, TSACertChainPath: tt.tsChainPath, }, @@ -643,22 +153,9 @@ func TestVerifyBlob(t *testing.T) { CertOidcIssuer: issuer, }, IgnoreSCT: true, - CertChain: chainPath, IgnoreTlog: tt.skipTlogVerify, } blobPath := writeBlobFile(t, td, string(blobBytes), "blob.txt") - if tt.signature != "" { - sigPath := writeBlobFile(t, td, tt.signature, "signature.txt") - cmd.SigRef = sigPath - } - if tt.cert != nil { - certPEM, err := cryptoutils.MarshalCertificateToPEM(tt.cert) - if err != nil { - t.Fatal("MarshalCertificateToPEM: %w", err) - } - certPath := writeBlobFile(t, td, string(certPEM), "cert.pem") - cmd.CertRef = certPath - } if tt.key != nil { keyPath := writeBlobFile(t, td, string(tt.key), "key.pem") cmd.KeyRef = keyPath @@ -666,10 +163,8 @@ func TestVerifyBlob(t *testing.T) { } if tt.newBundle { cmd.TrustedRootPath = writeTrustedRootFile(t, td, "{\"mediaType\":\"application/vnd.dev.sigstore.trustedroot+json;version=0.1\"}") - cmd.RekorURL = "" cmd.RFC3161TimestampPath = "" cmd.TSACertChainPath = "" - cmd.CertChain = "" } err := cmd.Exec(context.Background(), blobPath) if (err != nil) != tt.shouldErr { @@ -688,7 +183,6 @@ func TestVerifyBlobCertMissingSubject(t *testing.T) { ctx := context.Background() verifyBlob := VerifyBlobCmd{ - CertRef: "cert.pem", CertVerifyOptions: options.CertVerifyOptions{ CertOidcIssuer: "issuer", }, @@ -788,7 +282,8 @@ func TestVerifyBlobSkWithoutIdentities(t *testing.T) { ctx := context.Background() verifyBlob := VerifyBlobCmd{ KeyOpts: options.KeyOpts{ - Sk: true, + Sk: true, + BundlePath: "bundle.sigstore.json", }, } @@ -801,169 +296,6 @@ func TestVerifyBlobSkWithoutIdentities(t *testing.T) { } } -func makeRekorEntry(t *testing.T, rekorSigner signature.ECDSASignerVerifier, - pyld, sig, svBytes []byte, expiryValid bool) *models.LogEntry { - ctx := context.Background() - // Calculate log ID, the digest of the Rekor public key - logID, err := getLogID(rekorSigner.Public()) - if err != nil { - t.Fatal(err) - } - - hashedrekord := &hashedrekord_v001.V001Entry{} - h := sha256.Sum256(pyld) - pe, err := hashedrekord.CreateFromArtifactProperties(ctx, types.ArtifactProperties{ - ArtifactHash: hex.EncodeToString(h[:]), - SignatureBytes: sig, - PublicKeyBytes: [][]byte{svBytes}, - PKIFormat: "x509", - }) - if err != nil { - t.Fatal(err) - } - entry, err := types.UnmarshalEntry(pe) - if err != nil { - t.Fatal(err) - } - leaf, err := entry.Canonicalize(ctx) - if err != nil { - t.Fatal(err) - } - - integratedTime := time.Now() - certs, _ := cryptoutils.UnmarshalCertificatesFromPEM(svBytes) - if len(certs) > 0 { - if expiryValid { - integratedTime = certs[0].NotAfter.Add(-time.Second) - } else { - integratedTime = certs[0].NotAfter.Add(time.Second) - } - } - e := models.LogEntryAnon{ - Body: base64.StdEncoding.EncodeToString(leaf), - IntegratedTime: conv.Pointer(integratedTime.Unix()), - LogIndex: conv.Pointer(int64(0)), - LogID: conv.Pointer(logID), - } - // Marshal payload, sign, and set SET in Bundle - jsonPayload, err := json.Marshal(e) - if err != nil { - t.Fatal(err) - } - canonicalized, err := jsoncanonicalizer.Transform(jsonPayload) - if err != nil { - t.Fatal(err) - } - bundleSig, err := rekorSigner.SignMessage(bytes.NewReader(canonicalized)) - if err != nil { - t.Fatal(err) - } - uuid, _ := cosign.ComputeLeafHash(&e) - - e.Verification = &models.LogEntryAnonVerification{ - SignedEntryTimestamp: bundleSig, - InclusionProof: &models.InclusionProof{ - LogIndex: conv.Pointer(int64(0)), - TreeSize: conv.Pointer(int64(1)), - RootHash: conv.Pointer(hex.EncodeToString(uuid)), - Hashes: []string{}, - }, - } - return &models.LogEntry{hex.EncodeToString(uuid): e} -} - -func makeLocalBundle(t *testing.T, rekorSigner signature.ECDSASignerVerifier, - pyld []byte, sig []byte, svBytes []byte, expiryValid bool) string { - td := t.TempDir() - - // Create bundle. - entry := makeRekorEntry(t, rekorSigner, pyld, sig, svBytes, expiryValid) - var e models.LogEntryAnon - for _, v := range *entry { - e = v - } - b := cosign.LocalSignedPayload{ - Base64Signature: base64.StdEncoding.EncodeToString(sig), - Cert: string(svBytes), - Bundle: &bundle.RekorBundle{ - Payload: bundle.RekorPayload{ - Body: e.Body, - IntegratedTime: *e.IntegratedTime, - LogIndex: *e.LogIndex, - LogID: *e.LogID, - }, - SignedEntryTimestamp: e.Verification.SignedEntryTimestamp, - }, - } - - // Write bundle to disk - jsonBundle, err := json.Marshal(b) - if err != nil { - t.Fatal(err) - } - bundlePath := filepath.Join(td, "bundle.sig") - if err := os.WriteFile(bundlePath, jsonBundle, 0644); err != nil { - t.Fatal(err) - } - return bundlePath -} - -func makeLocalBundleWithoutCert(t *testing.T, rekorSigner signature.ECDSASignerVerifier, - pyld []byte, sig []byte, svBytes []byte, expiryValid bool) string { - td := t.TempDir() - - // Create bundle. - entry := makeRekorEntry(t, rekorSigner, pyld, sig, svBytes, expiryValid) - var e models.LogEntryAnon - for _, v := range *entry { - e = v - } - b := cosign.LocalSignedPayload{ - Base64Signature: base64.StdEncoding.EncodeToString(sig), - Cert: "", - Bundle: &bundle.RekorBundle{ - Payload: bundle.RekorPayload{ - Body: e.Body, - IntegratedTime: *e.IntegratedTime, - LogIndex: *e.LogIndex, - LogID: *e.LogID, - }, - SignedEntryTimestamp: e.Verification.SignedEntryTimestamp, - }, - } - - // Write bundle to disk - jsonBundle, err := json.Marshal(b) - if err != nil { - t.Fatal(err) - } - bundlePath := filepath.Join(td, "bundle.sig") - if err := os.WriteFile(bundlePath, jsonBundle, 0644); err != nil { - t.Fatal(err) - } - return bundlePath -} - -func makeLocalBundleWithoutRekorBundle(t *testing.T, sig []byte, svBytes []byte) string { - td := t.TempDir() - - b := cosign.LocalSignedPayload{ - Base64Signature: base64.StdEncoding.EncodeToString(sig), - Cert: string(svBytes), - } - - // Write bundle to disk - jsonBundle, err := json.Marshal(b) - if err != nil { - t.Fatal(err) - } - bundlePath := filepath.Join(td, "bundle.sig") - if err := os.WriteFile(bundlePath, jsonBundle, 0644); err != nil { - t.Fatal(err) - } - return bundlePath -} - func makeLocalNewBundle(t *testing.T, sig []byte, digest [32]byte) string { b := &protobundle.Bundle{ MediaType: "application/vnd.dev.sigstore.bundle.v0.3+json", @@ -1000,807 +332,7 @@ func makeLocalNewBundle(t *testing.T, sig []byte, digest [32]byte) string { return bundlePath } -func TestVerifyBlobCmdWithBundle(t *testing.T) { - t.Setenv("TUF_ROOT", t.TempDir()) - keyless := newKeylessStack(t) - defer os.RemoveAll(keyless.td) - - t.Run("Normal verification", func(t *testing.T) { - identity := "hello@foo.com" - issuer := "issuer" - leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) - - // Create blob - blob := "someblob" - - // Sign blob with private key - sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) - if err != nil { - t.Fatal(err) - } - - // Create bundle - entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig) - b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) - b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload) - bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") - blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt") - - // Verify command - cmd := VerifyBlobCmd{ - KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - CertVerifyOptions: options.CertVerifyOptions{ - CertIdentity: identity, - CertOidcIssuer: issuer, - }, - IgnoreSCT: true, - } - if err := cmd.Exec(context.Background(), blobPath); err != nil { - t.Fatal(err) - } - }) - t.Run("Mismatched cert/sig", func(t *testing.T) { - // This test ensures that the signature and cert at the top level in the LocalSignedPayload must be identical to the ones in the RekorBundle. - identity := "hello@foo.com" - issuer := "issuer" - leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) - _, _, leafPemCert2, signer2 := keyless.genLeafCert(t, identity, issuer) - - // Create blob - blob := "someblob" - - sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) - if err != nil { - t.Fatal(err) - } - - sig2, err := signer2.SignMessage(bytes.NewReader([]byte(blob))) - if err != nil { - t.Fatal(err) - } - - // Create bundle - entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert2, sig2) - b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) - b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload) - bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") - blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt") - - // Verify command - cmd := VerifyBlobCmd{ - KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - IgnoreSCT: true, - } - if err := cmd.Exec(context.Background(), blobPath); err == nil { - t.Fatal("expecting err due to mismatched signatures, got nil") - } - }) - t.Run("Expired cert", func(t *testing.T) { - identity := "hello@foo.com" - issuer := "issuer" - leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) - - // Create blob - blob := "someblob" - - // Sign blob with private key - sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) - if err != nil { - t.Fatal(err) - } - - // Create bundle - entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig) - b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()-1, entry) - b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload) - bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") - blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt") - - // Verify command - cmd := VerifyBlobCmd{ - KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - IgnoreSCT: true, - } - - if err := cmd.Exec(context.Background(), blobPath); err == nil { - t.Fatal("expected error due to expired cert, received nil") - } - }) - t.Run("dsse Attestation", func(t *testing.T) { - identity := "hello@foo.com" - issuer := "issuer" - leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) - - stmt := `{"_type":"https://in-toto.io/Statement/v0.1","predicateType":"customFoo","subject":[{"name":"subject","digest":{"sha256":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}}],"predicate":{}}` - wrapped := dsse.WrapSigner(signer, ctypes.IntotoPayloadType) - signedPayload, err := wrapped.SignMessage(bytes.NewReader([]byte(stmt)), signatureoptions.WithContext(context.Background())) - if err != nil { - t.Fatal(err) - } - // intoto sig = json-serialized dsse envelope - sig := signedPayload - - // Create bundle - entry := genRekorEntry(t, rekor_dsse.KIND, "0.0.1", signedPayload, leafPemCert, sig) - b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) - b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload) - bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") - blobPath := writeBlobFile(t, keyless.td, string(signedPayload), "attestation.txt") - - // Verify command - cmd := VerifyBlobAttestationCommand{ - CertVerifyOptions: options.CertVerifyOptions{ - CertIdentity: identity, - CertOidcIssuer: issuer, - }, - CertRef: "", // Cert is fetched from bundle - CertChain: "", // Chain is fetched from TUF/SIGSTORE_ROOT_FILE - SignaturePath: "", // Sig is fetched from bundle - KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - IgnoreSCT: true, - PredicateType: "customFoo", - } - if err := cmd.Exec(context.Background(), blobPath); err != nil { - t.Fatal(err) - } - }) - t.Run("intoto Attestation", func(t *testing.T) { - identity := "hello@foo.com" - issuer := "issuer" - leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) - - stmt := `{"_type":"https://in-toto.io/Statement/v0.1","predicateType":"customFoo","subject":[{"name":"subject","digest":{"sha256":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}}],"predicate":{}}` - wrapped := dsse.WrapSigner(signer, ctypes.IntotoPayloadType) - signedPayload, err := wrapped.SignMessage(bytes.NewReader([]byte(stmt)), signatureoptions.WithContext(context.Background())) - if err != nil { - t.Fatal(err) - } - // intoto sig = json-serialized dsse envelope - sig := signedPayload - - // Create bundle - entry := genRekorEntry(t, intoto.KIND, "0.0.1", signedPayload, leafPemCert, sig) - b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) - b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload) - bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") - blobPath := writeBlobFile(t, keyless.td, string(signedPayload), "attestation.txt") - - // Verify command - cmd := VerifyBlobAttestationCommand{ - CertVerifyOptions: options.CertVerifyOptions{ - CertIdentity: identity, - CertOidcIssuer: issuer, - }, - CertRef: "", // Cert is fetched from bundle - CertChain: "", // Chain is fetched from TUF/SIGSTORE_ROOT_FILE - SignaturePath: "", // Sig is fetched from bundle - KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - IgnoreSCT: true, - PredicateType: "customFoo", - } - if err := cmd.Exec(context.Background(), blobPath); err != nil { - t.Fatal(err) - } - }) - t.Run("Invalid blob signature", func(t *testing.T) { - identity := "hello@foo.com" - issuer := "issuer" - leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) - - // Create blob - blob := "someblob" - - // Sign blob with private key - sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) - if err != nil { - t.Fatal(err) - } - - // Create bundle - entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig) - b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) - b.Bundle.SignedEntryTimestamp = []byte{'i', 'n', 'v', 'a', 'l', 'i', 'd'} - bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") - blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt") - - // Verify command - cmd := VerifyBlobCmd{ - CertVerifyOptions: options.CertVerifyOptions{ - CertIdentity: identity, - CertOidcIssuer: issuer, - }, - CertRef: "", // Cert is fetched from bundle - CertChain: "", // Chain is fetched from TUF/SIGSTORE_ROOT_FILE - SigRef: "", // Sig is fetched from bundle - KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - IgnoreSCT: true, - } - err = cmd.Exec(context.Background(), blobPath) - if err == nil || !strings.Contains(err.Error(), "unable to verify SET") { - t.Fatalf("expected error verifying SET, got %v", err) - } - }) - t.Run("Mismatched certificate email", func(t *testing.T) { - identity := "hello@foo.com" - issuer := "issuer" - leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) - - // Create blob - blob := "someblob" - - // Sign blob with private key - sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) - if err != nil { - t.Fatal(err) - } - - // Create bundle - entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig) - b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) - b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload) - bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") - blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt") - - // Verify command - cmd := VerifyBlobCmd{ - KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - CertRef: "", // Cert is fetched from bundle - CertVerifyOptions: options.CertVerifyOptions{ - CertOidcIssuer: issuer, - CertIdentity: "invalid@example.com", - }, - CertChain: "", // Chain is fetched from TUF/SIGSTORE_ROOT_FILE - SigRef: "", // Sig is fetched from bundle - IgnoreSCT: true, - } - err = cmd.Exec(context.Background(), blobPath) - if err == nil || !strings.Contains(err.Error(), "none of the expected identities matched what was in the certificate") { - t.Fatalf("expected error with mismatched identity, got %v", err) - } - }) - t.Run("Mismatched certificate issuer", func(t *testing.T) { - identity := "hello@foo.com" - issuer := "issuer" - leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) - - // Create blob - blob := "someblob" - - // Sign blob with private key - sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) - if err != nil { - t.Fatal(err) - } - - // Create bundle - entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig) - b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) - b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload) - bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") - blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt") - - // Verify command - cmd := VerifyBlobCmd{ - CertRef: "", // Cert is fetched from bundle - CertVerifyOptions: options.CertVerifyOptions{ - CertOidcIssuer: "invalid", - CertIdentity: identity, - }, - CertChain: "", // Chain is fetched from TUF/SIGSTORE_ROOT_FILE - SigRef: "", // Sig is fetched from bundle - KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - IgnoreSCT: true, - } - err = cmd.Exec(context.Background(), blobPath) - if err == nil || !strings.Contains(err.Error(), "none of the expected identities matched what was in the certificate") { - t.Fatalf("expected error with mismatched issuer, got %v", err) - } - }) - t.Run("Implicit Fulcio chain with bundle in non-experimental mode", func(t *testing.T) { - identity := "hello@foo.com" - issuer := "issuer" - leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) - - // Create blob - blob := "someblob" - - // Sign blob with private key - sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) - if err != nil { - t.Fatal(err) - } - - // Create bundle - entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig) - b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) - b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload) - bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") - blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt") - certPath := writeBlobFile(t, keyless.td, string(leafPemCert), "cert.pem") - - // Verify command - cmd := VerifyBlobCmd{ - CertRef: certPath, - CertVerifyOptions: options.CertVerifyOptions{ - CertOidcIssuer: issuer, - CertIdentity: identity, - }, - CertChain: "", // Chain is fetched from TUF/SIGSTORE_ROOT_FILE - SigRef: "", // Sig is fetched from bundle - KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - IgnoreSCT: true, - } - err = cmd.Exec(context.Background(), blobPath) - if err != nil { - t.Fatalf("expected success without specifying the intermediates, got %v", err) - } - }) - t.Run("Explicit Fulcio chain with rekor and timestamp bundles in non-experimental mode", func(t *testing.T) { - identity := "hello@foo.com" - issuer := "issuer" - leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) - - // Create blob - blob := "someblob" - - // Sign blob with private key - sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) - if err != nil { - t.Fatal(err) - } - - // Initialize timestamp with mock client - tsaClient, err := mock.NewTSAClient((mock.TSAClientOptions{Time: time.Now(), Message: sig})) - if err != nil { - t.Fatal(err) - } - certChainPEM, err := cryptoutils.MarshalCertificatesToPEM(tsaClient.CertChain) - if err != nil { - t.Fatalf("unexpected error marshalling cert chain: %v", err) - } - tsaCertChainPath := filepath.Join(keyless.td, "tsacertchain.pem") - if err := os.WriteFile(tsaCertChainPath, certChainPEM, 0644); err != nil { - t.Fatal(err) - } - tsr, err := tsaClient.GetTimestampResponse(nil) - if err != nil { - t.Fatalf("unable to generate a timestamp response: %v", err) - } - rfc3161Timestamp := &bundle.RFC3161Timestamp{SignedRFC3161Timestamp: tsr} - tsPath := writeTimestampFile(t, keyless.td, rfc3161Timestamp, "rfc3161TS.json") - - entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig) - b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) - b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload) - bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") - blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt") - - // Verify command - cmd := VerifyBlobCmd{ - CertVerifyOptions: options.CertVerifyOptions{ - CertOidcIssuer: issuer, - CertIdentity: identity, - }, - CertChain: os.Getenv("SIGSTORE_ROOT_FILE"), - SigRef: "", // Sig is fetched from bundle - KeyOpts: options.KeyOpts{BundlePath: bundlePath, TSACertChainPath: tsaCertChainPath, RFC3161TimestampPath: tsPath}, - IgnoreSCT: true, - } - err = cmd.Exec(context.Background(), blobPath) - if err != nil { - t.Fatalf("expected success verifying with timestamp, got %v", err) - } - }) - t.Run("Explicit Fulcio chain with bundle in non-experimental mode", func(t *testing.T) { - identity := "hello@foo.com" - issuer := "issuer" - leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) - - // Create blob - blob := "someblob" - - // Sign blob with private key - sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) - if err != nil { - t.Fatal(err) - } - - // Create bundle - entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig) - b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) - b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload) - bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") - blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt") - - // Verify command - cmd := VerifyBlobCmd{ - CertVerifyOptions: options.CertVerifyOptions{ - CertOidcIssuer: issuer, - CertIdentity: identity, - }, - CertChain: os.Getenv("SIGSTORE_ROOT_FILE"), - SigRef: "", // Sig is fetched from bundle - KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - IgnoreSCT: true, - } - err = cmd.Exec(context.Background(), blobPath) - if err != nil { - t.Fatalf("expected success specifying the intermediates, got %v", err) - } - }) - t.Run("Explicit Fulcio mismatched chain failure", func(t *testing.T) { - identity := "hello@foo.com" - issuer := "issuer" - leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) - - // Create blob - blob := "someblob" - - // Sign blob with private key - sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) - if err != nil { - t.Fatal(err) - } - - // Create bundle - entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig) - b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) - b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload) - bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") - blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt") - - rootCert, _, _ := test.GenerateRootCa() - rootPemCert, _ := cryptoutils.MarshalCertificateToPEM(rootCert) - tmpChainFile, err := os.CreateTemp(t.TempDir(), "cosign_fulcio_root_*.cert") - if err != nil { - t.Fatalf("failed to create temp chain file: %v", err) - } - defer tmpChainFile.Close() - if _, err := tmpChainFile.Write(rootPemCert); err != nil { - t.Fatalf("failed to write chain file: %v", err) - } - - // Verify command - cmd := VerifyBlobCmd{ - CertVerifyOptions: options.CertVerifyOptions{ - CertOidcIssuer: issuer, - CertIdentity: identity, - }, - CertChain: tmpChainFile.Name(), - SigRef: "", // Sig is fetched from bundle - KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - IgnoreSCT: true, - } - err = cmd.Exec(context.Background(), blobPath) - if err == nil || !strings.Contains(err.Error(), "x509: certificate signed by unknown authority") { - t.Fatalf("expected error with mismatched root, got %v", err) - } - }) - t.Run("intoto Attestation with keyless", func(t *testing.T) { - identity := "hello@foo.com" - issuer := "issuer" - leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) - - stmt := `{"_type":"https://in-toto.io/Statement/v0.1","predicateType":"customFoo","subject":[{"name":"subject","digest":{"sha256":"deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"}}],"predicate":{}}` - wrapped := dsse.WrapSigner(signer, ctypes.IntotoPayloadType) - signedPayload, err := wrapped.SignMessage(bytes.NewReader([]byte(stmt)), signatureoptions.WithContext(context.Background())) - if err != nil { - t.Fatal(err) - } - // intoto sig = json-serialized dsse envelope - sig := signedPayload - - // Create bundle - entry := genRekorEntry(t, intoto.KIND, "0.0.1", signedPayload, leafPemCert, sig) - b := createBundle(t, sig, leafPemCert, keyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) - b.Bundle.SignedEntryTimestamp = keyless.rekorSignPayload(t, b.Bundle.Payload) - bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") - blobPath := writeBlobFile(t, keyless.td, string(signedPayload), "attestation.txt") - - // Verify command with bundle - cmd := VerifyBlobAttestationCommand{ - CertVerifyOptions: options.CertVerifyOptions{ - CertOidcIssuer: issuer, - CertIdentity: identity, - }, - CertRef: "", // Cert is fetched from bundle - CertChain: "", // Chain is fetched from TUF/SIGSTORE_ROOT_FILE - SignaturePath: "", // Sig is fetched from bundle - KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - IgnoreSCT: true, - CheckClaims: false, // Intentionally false. This checks the subject claim. This is tested in verify_blob_attestation_test.go - PredicateType: "customFoo", - } - if err := cmd.Exec(context.Background(), blobPath); err != nil { - t.Fatal(err) - } - }) -} - -func TestVerifyBlobCmdInvalidRootCA(t *testing.T) { - t.Setenv("TUF_ROOT", t.TempDir()) - keyless := newKeylessStack(t) - defer os.RemoveAll(keyless.td) - - // Change the keyless stack. - newKeyless := newKeylessStack(t) - defer os.RemoveAll(newKeyless.td) - t.Run("Invalid certificate root when specifying cert via certRef", func(t *testing.T) { - identity := "hello@foo.com" - issuer := "issuer" - leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) - - // Create blob - blob := "someblob" - - // Sign blob with private key - sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) - if err != nil { - t.Fatal(err) - } - - // Create bundle - entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig) - b := createBundle(t, sig, leafPemCert, newKeyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) - b.Bundle.SignedEntryTimestamp = newKeyless.rekorSignPayload(t, b.Bundle.Payload) - bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") - blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt") - certPath := writeBlobFile(t, keyless.td, string(leafPemCert), "cert.pem") - - // Verify command - cmd := VerifyBlobCmd{ - CertRef: certPath, - CertVerifyOptions: options.CertVerifyOptions{ - CertOidcIssuer: issuer, - CertIdentity: identity, - }, - CertChain: "", // Chain is fetched from TUF/SIGSTORE_ROOT_FILE - SigRef: "", // Sig is fetched from bundle - KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - IgnoreSCT: true, - } - err = cmd.Exec(context.Background(), blobPath) - if err == nil || !strings.Contains(err.Error(), "certificate signed by unknown authority") { - t.Fatalf("expected error with certificate signed by unknown authority, got %v", err) - } - }) - t.Run("Invalid certificate root when specifying cert in bundle", func(t *testing.T) { - identity := "hello@foo.com" - issuer := "issuer" - leafCert, _, leafPemCert, signer := keyless.genLeafCert(t, identity, issuer) - - // Create blob - blob := "someblob" - - // Sign blob with private key - sig, err := signer.SignMessage(bytes.NewReader([]byte(blob))) - if err != nil { - t.Fatal(err) - } - - // Create bundle - entry := genRekorEntry(t, hashedrekord.KIND, hashedrekord.New().DefaultVersion(), []byte(blob), leafPemCert, sig) - b := createBundle(t, sig, leafPemCert, newKeyless.rekorLogID, leafCert.NotBefore.Unix()+1, entry) - b.Bundle.SignedEntryTimestamp = newKeyless.rekorSignPayload(t, b.Bundle.Payload) - bundlePath := writeBundleFile(t, keyless.td, b, "bundle.json") - blobPath := writeBlobFile(t, keyless.td, blob, "blob.txt") - - // Verify command - cmd := VerifyBlobCmd{ - CertRef: "", - CertVerifyOptions: options.CertVerifyOptions{ - CertOidcIssuer: issuer, // Fetched from bundle - CertIdentity: identity, - }, - CertChain: "", // Chain is fetched from TUF/SIGSTORE_ROOT_FILE - SigRef: "", // Sig is fetched from bundle - KeyOpts: options.KeyOpts{BundlePath: bundlePath}, - IgnoreSCT: true, - } - err = cmd.Exec(context.Background(), blobPath) - if err == nil || !strings.Contains(err.Error(), "certificate signed by unknown authority") { - t.Fatalf("expected error with certificate signed by unknown authority, got %v", err) - } - }) -} - -type keylessStack struct { - rootCert *x509.Certificate - rootPriv *ecdsa.PrivateKey - rootPemCert []byte - subCert *x509.Certificate - subPriv *ecdsa.PrivateKey - subPemCert []byte - rekorSigner *signature.ECDSASignerVerifier - rekorLogID string - td string // temporary directory -} - -func newKeylessStack(t *testing.T) *keylessStack { - stack := &keylessStack{td: t.TempDir()} - stack.rootCert, stack.rootPriv, _ = test.GenerateRootCa() - stack.rootPemCert, _ = cryptoutils.MarshalCertificateToPEM(stack.rootCert) - stack.subCert, stack.subPriv, _ = test.GenerateSubordinateCa(stack.rootCert, stack.rootPriv) - stack.subPemCert, _ = cryptoutils.MarshalCertificateToPEM(stack.subCert) - - stack.genChainFile(t) - stack.genRekor(t) - return stack -} - -func (s *keylessStack) genLeafCert(t *testing.T, subject string, issuer string) (*x509.Certificate, *ecdsa.PrivateKey, []byte, *signature.ECDSASignerVerifier) { //nolint: unparam - cert, priv, _ := test.GenerateLeafCert(subject, issuer, s.subCert, s.subPriv) - pemCert, _ := cryptoutils.MarshalCertificateToPEM(cert) - signer, err := signature.LoadECDSASignerVerifier(priv, crypto.SHA256) - if err != nil { - t.Fatal(err) - } - return cert, priv, pemCert, signer -} - -func (s *keylessStack) genChainFile(t *testing.T) { - chain := make([]byte, 0, len(s.subPemCert)+len(s.rootPemCert)) - chain = append(chain, s.subPemCert...) - chain = append(chain, s.rootPemCert...) - tmpChainFile, err := os.CreateTemp(s.td, "cosign_fulcio_chain_*.cert") - if err != nil { - t.Fatalf("failed to create temp chain file: %v", err) - } - defer tmpChainFile.Close() - if _, err := tmpChainFile.Write(chain); err != nil { - t.Fatalf("failed to write chain file: %v", err) - } - // Override for Fulcio root so it doesn't use TUF - t.Setenv("SIGSTORE_ROOT_FILE", tmpChainFile.Name()) -} - -func (s *keylessStack) genRekor(t *testing.T) { - // Create Rekor private key and write to disk - rekorPriv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatal(err) - } - s.rekorSigner, err = signature.LoadECDSASignerVerifier(rekorPriv, crypto.SHA256) - if err != nil { - t.Fatal(err) - } - rekorPub := s.rekorSigner.Public() - pemRekor, err := cryptoutils.MarshalPublicKeyToPEM(rekorPub) - if err != nil { - t.Fatal(err) - } - tmpRekorPubFile, err := os.CreateTemp(s.td, "cosign_rekor_pub_*.key") - if err != nil { - t.Fatalf("failed to create temp rekor pub file: %v", err) - } - defer tmpRekorPubFile.Close() - if _, err := tmpRekorPubFile.Write(pemRekor); err != nil { - t.Fatalf("failed to write rekor pub file: %v", err) - } - - // Calculate log ID, the digest of the Rekor public key - s.rekorLogID, err = getLogID(rekorPub) - if err != nil { - t.Fatal(err) - } - // Override for Rekor public key so it doesn't use TUF - t.Setenv("SIGSTORE_REKOR_PUBLIC_KEY", tmpRekorPubFile.Name()) -} -func (s *keylessStack) rekorSignPayload(t *testing.T, payload bundle.RekorPayload) []byte { - // Marshal payload, sign, and return SET - jsonPayload, err := json.Marshal(payload) - if err != nil { - t.Fatal(err) - } - canonicalized, err := jsoncanonicalizer.Transform(jsonPayload) - if err != nil { - t.Fatal(err) - } - bundleSig, err := s.rekorSigner.SignMessage(bytes.NewReader(canonicalized)) - if err != nil { - t.Fatal(err) - } - return bundleSig -} - // getLogID calculates the digest of a PKIX-encoded public key -func getLogID(pub crypto.PublicKey) (string, error) { - pubBytes, err := x509.MarshalPKIXPublicKey(pub) - if err != nil { - return "", err - } - digest := sha256.Sum256(pubBytes) - return hex.EncodeToString(digest[:]), nil -} - -func genRekorEntry(t *testing.T, kind, version string, artifact []byte, cert []byte, sig []byte) string { - // Generate the Rekor Entry - entryImpl, err := createEntry(context.Background(), kind, version, artifact, cert, sig) - if err != nil { - t.Fatal(err) - } - entryBytes, err := entryImpl.Canonicalize(context.Background()) - if err != nil { - t.Fatal(err) - } - return base64.StdEncoding.EncodeToString(entryBytes) -} - -func createBundle(_ *testing.T, sig []byte, certPem []byte, logID string, integratedTime int64, rekorEntry string) *cosign.LocalSignedPayload { - // Create bundle with: - // * Blob signature - // * Signing certificate - // * Bundle with a payload and signature over the payload - b := &cosign.LocalSignedPayload{ - Base64Signature: base64.StdEncoding.EncodeToString(sig), - Cert: string(certPem), - Bundle: &bundle.RekorBundle{ - SignedEntryTimestamp: []byte{}, - Payload: bundle.RekorPayload{ - LogID: logID, - IntegratedTime: integratedTime, - LogIndex: 1, - Body: rekorEntry, - }, - }, - } - - return b -} - -func createEntry(ctx context.Context, kind, apiVersion string, blobBytes, certBytes, sigBytes []byte) (types.EntryImpl, error) { - props := types.ArtifactProperties{ - PublicKeyBytes: [][]byte{certBytes}, - PKIFormat: string(pki.X509), - } - switch kind { - case rekord.KIND: - props.ArtifactBytes = blobBytes - props.SignatureBytes = sigBytes - case hashedrekord.KIND: - blobHash := sha256.Sum256(blobBytes) - props.ArtifactHash = strings.ToLower(hex.EncodeToString(blobHash[:])) - props.SignatureBytes = sigBytes - case intoto.KIND: - props.ArtifactBytes = blobBytes - case rekor_dsse.KIND: - props.ArtifactBytes = blobBytes - default: - return nil, fmt.Errorf("unexpected entry kind: %s", kind) - } - proposedEntry, err := types.NewProposedEntry(ctx, kind, apiVersion, props) - if err != nil { - return nil, err - } - eimpl, err := types.CreateVersionedEntry(proposedEntry) - if err != nil { - return nil, err - } - - can, err := types.CanonicalizeEntry(ctx, eimpl) - if err != nil { - return nil, err - } - proposedEntryCan, err := models.UnmarshalProposedEntry(bytes.NewReader(can), runtime.JSONConsumer()) - if err != nil { - return nil, err - } - - return types.UnmarshalEntry(proposedEntryCan) -} - -func writeBundleFile(t *testing.T, td string, b *cosign.LocalSignedPayload, name string) string { //nolint: unparam - // Write bundle to disk - jsonBundle, err := json.Marshal(b) - if err != nil { - t.Fatal(err) - } - bundlePath := filepath.Join(td, name) - if err := os.WriteFile(bundlePath, jsonBundle, 0644); err != nil { - t.Fatal(err) - } - return bundlePath -} func writeBlobFile(t *testing.T, td string, blob string, name string) string { // Write blob to disk @@ -1811,18 +343,6 @@ func writeBlobFile(t *testing.T, td string, blob string, name string) string { return blobPath } -func writeTimestampFile(t *testing.T, td string, ts *bundle.RFC3161Timestamp, name string) string { - jsonBundle, err := json.Marshal(ts) - if err != nil { - t.Fatal(err) - } - path := filepath.Join(td, name) - if err := os.WriteFile(path, jsonBundle, 0644); err != nil { - t.Fatal(err) - } - return path -} - func writeTrustedRootFile(t *testing.T, td, contents string) string { //nolint: unparam path := filepath.Join(td, "trusted_root.json") if err := os.WriteFile(path, []byte(contents), 0644); err != nil { diff --git a/cmd/cosign/cli/verify/verify_bundle.go b/cmd/cosign/cli/verify/verify_bundle.go index fd505542b60..83830ee5032 100644 --- a/cmd/cosign/cli/verify/verify_bundle.go +++ b/cmd/cosign/cli/verify/verify_bundle.go @@ -38,11 +38,6 @@ import ( "github.com/sigstore/cosign/v3/pkg/cosign" ) -func checkNewBundle(bundlePath string, opts ...sgbundle.Option) bool { - _, err := sgbundle.LoadJSONFromPath(bundlePath, opts...) - return err == nil -} - func AssembleNewBundle(ctx context.Context, sigBytes, signedTimestamp []byte, envelope *dsse.Envelope, artifactRef string, cert *x509.Certificate, ignoreTlog bool, sigVerifier signature.Verifier, pkOpts []signature.PublicKeyOption, rekorClient *client.Rekor) (*sgbundle.Bundle, error) { payload, err := payloadBytes(artifactRef) if err != nil { diff --git a/cmd/cosign/cli/verify/verify_test.go b/cmd/cosign/cli/verify/verify_test.go index 90b797399ac..99ba6d6097d 100644 --- a/cmd/cosign/cli/verify/verify_test.go +++ b/cmd/cosign/cli/verify/verify_test.go @@ -38,10 +38,8 @@ import ( "github.com/google/go-containerregistry/pkg/name" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - "github.com/sigstore/cosign/v3/internal/pkg/cosign/fulcio/fulcioroots" "github.com/sigstore/cosign/v3/internal/test" "github.com/sigstore/cosign/v3/internal/ui" - "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/oci" "github.com/sigstore/cosign/v3/pkg/oci/static" "github.com/sigstore/sigstore/pkg/signature/payload" @@ -86,43 +84,6 @@ func getTestCerts(t *testing.T) *certData { return cd } -func makeCertChainFile(t *testing.T, rootCert, subCert, leafCert []byte) string { - t.Helper() - f, err := os.CreateTemp("", "certchain") - if err != nil { - t.Fatal(err) - } - defer f.Close() - _, err = f.Write(append(append(rootCert, subCert...), leafCert...)) - if err != nil { - t.Fatal(err) - } - return f.Name() -} - -func makeRootsIntermediatesFiles(t *testing.T, roots, intermediates []byte) (string, string) { - t.Helper() - rootF, err := os.CreateTemp("", "roots") - if err != nil { - t.Fatal(err) - } - defer rootF.Close() - _, err = rootF.Write(roots) - if err != nil { - t.Fatal(err) - } - intermediateF, err := os.CreateTemp("", "intermediates") - if err != nil { - t.Fatal(err) - } - defer intermediateF.Close() - _, err = intermediateF.Write(intermediates) - if err != nil { - t.Fatal(err) - } - return rootF.Name(), intermediateF.Name() -} - func TestPrintVerification(t *testing.T) { // while we are adding a more human-readable output for cert extensions, on the other hand // we want as backward compatible as possible, so we are keeping the old OIDs field names as well. @@ -243,7 +204,6 @@ func appendSlices(slices [][]byte) []byte { func TestVerifyCertMissingSubject(t *testing.T) { ctx := context.Background() verifyCommand := VerifyCommand{ - CertRef: "cert.pem", CertVerifyOptions: options.CertVerifyOptions{ CertOidcIssuer: "issuer", }, @@ -258,7 +218,6 @@ func TestVerifyCertMissingSubject(t *testing.T) { func TestVerifyCertMissingIssuer(t *testing.T) { ctx := context.Background() verifyCommand := VerifyCommand{ - CertRef: "cert.pem", CertVerifyOptions: options.CertVerifyOptions{ CertIdentity: "identity", }, @@ -319,88 +278,6 @@ func TestVerifyMutuallyExclusiveFlags(t *testing.T) { } } -func TestLoadCertsKeylessVerification(t *testing.T) { - certs := getTestCerts(t) - certChainFile := makeCertChainFile(t, certs.RootCertPEM, certs.SubCertPEM, certs.LeafCertPEM) - rootsFile, intermediatesFile := makeRootsIntermediatesFiles(t, certs.RootCertPEM, certs.SubCertPEM) - tests := []struct { - name string - certChain string - caRoots string - caIntermediates string - co *cosign.CheckOpts - sigstoreRootFile string - wantErr bool - }{ - { - name: "default fulcio", - wantErr: false, - }, - { - name: "non-existent SIGSTORE_ROOT_FILE", - sigstoreRootFile: "tesdata/nosuch-asdfjkl.pem", - wantErr: true, - }, - { - name: "good certchain", - certChain: certChainFile, - wantErr: false, - }, - { - name: "bad certchain", - certChain: "testdata/nosuch-certchain-file.pem", - wantErr: true, - }, - { - name: "roots", - caRoots: rootsFile, - wantErr: false, - }, - { - name: "bad roots", - caRoots: "testdata/nosuch-roots-file.pem", - wantErr: true, - }, - { - name: "roots and intermediate", - caRoots: rootsFile, - caIntermediates: intermediatesFile, - wantErr: false, - }, - { - name: "bad roots good intermediate", - caRoots: "testdata/nosuch-roots-file.pem", - caIntermediates: intermediatesFile, - wantErr: true, - }, - { - name: "good roots bad intermediate", - caRoots: rootsFile, - caIntermediates: "testdata/nosuch-intermediates-file.pem", - wantErr: true, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - if tt.sigstoreRootFile != "" { - os.Setenv("SIGSTORE_ROOT_FILE", tt.sigstoreRootFile) - } else { - t.Setenv("SIGSTORE_ROOT_FILE", "") - } - fulcioroots.ReInit() - if tt.co == nil { - tt.co = &cosign.CheckOpts{} - } - - err := loadCertsKeylessVerification(tt.certChain, tt.caRoots, tt.caIntermediates, tt.co) - if err == nil && tt.wantErr { - t.Fatalf("expected error but got none") - } else if err != nil && !tt.wantErr { - t.Fatalf("unexpected error: %v", err) - } - }) - } -} func TestTransformOutputSuccess(t *testing.T) { // Build minimal in-toto statement stmt := `{ diff --git a/doc/cosign.md b/doc/cosign.md index c403e9ae0d4..cc7214d12c2 100644 --- a/doc/cosign.md +++ b/doc/cosign.md @@ -30,7 +30,7 @@ A tool for Container Signing, Verification and Storage in an OCI registry * [cosign public-key](cosign_public-key.md) - Gets a public key from the key-pair * [cosign save](cosign_save.md) - Save the container image and associated signatures to disk at the specified directory * [cosign sign](cosign_sign.md) - Sign the supplied container image -* [cosign sign-blob](cosign_sign-blob.md) - Sign the supplied blob, outputting the base64-encoded signature to stdout +* [cosign sign-blob](cosign_sign-blob.md) - Sign the supplied blob, outputting the bundle to a file * [cosign signing-config](cosign_signing-config.md) - Interact with a Sigstore protobuf signing config * [cosign tree](cosign_tree.md) - Display supply chain security related artifacts for an image such as signatures, SBOMs and attestations * [cosign trusted-root](cosign_trusted-root.md) - Interact with a Sigstore protobuf trusted root diff --git a/doc/cosign_attest-blob.md b/doc/cosign_attest-blob.md index 3d3ed6429c1..ad2901e661f 100644 --- a/doc/cosign_attest-blob.md +++ b/doc/cosign_attest-blob.md @@ -9,25 +9,25 @@ cosign attest-blob [flags] ### Examples ``` - cosign attest-blob --key | [--predicate ] [--a key=value] [--f] [--r] + cosign attest-blob --key | [--predicate ] [--yes] --bundle # attach an attestation to a blob with a local key pair file and write the bundle to a file cosign attest-blob --predicate --type --key cosign.key --bundle # attach an attestation to a blob with a key pair stored in Azure Key Vault - cosign attest-blob --predicate --type --key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] + cosign attest-blob --predicate --type --key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] --bundle # attach an attestation to a blob with a key pair stored in AWS KMS - cosign attest-blob --predicate --type --key awskms://[ENDPOINT]/[ID/ALIAS/ARN] + cosign attest-blob --predicate --type --key awskms://[ENDPOINT]/[ID/ALIAS/ARN] --bundle # attach an attestation to a blob with a key pair stored in Google Cloud KMS - cosign attest-blob --predicate --type --key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY]/versions/[VERSION] + cosign attest-blob --predicate --type --key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY]/versions/[VERSION] --bundle # attach an attestation to a blob with a key pair stored in Hashicorp Vault - cosign attest-blob --predicate --type --key hashivault://[KEY] + cosign attest-blob --predicate --type --key hashivault://[KEY] --bundle # supply attestation via stdin - echo | cosign attest-blob --predicate - --yes + echo | cosign attest-blob --predicate - --bundle --yes ``` ### Options diff --git a/doc/cosign_attest.md b/doc/cosign_attest.md index f862bb53f17..9c6423c727b 100644 --- a/doc/cosign_attest.md +++ b/doc/cosign_attest.md @@ -9,7 +9,7 @@ cosign attest [flags] ### Examples ``` - cosign attest --key | [--predicate ] [--a key=value] [--no-upload=true|false] [--record-creation-timestamp=true|false] [--f] [--r] + cosign attest --key | [--predicate ] [--no-upload=true|false] [--yes] # attach an attestation to a container image Google sign-in cosign attest --timeout 90s --predicate --type @@ -40,9 +40,6 @@ cosign attest [flags] # write attestation to stdout cosign attest --predicate --type --key cosign.key --no-upload true - - # attach an attestation to a container image and honor the creation timestamp of the signature - cosign attest --predicate --type --key cosign.key --record-creation-timestamp ``` ### Options diff --git a/doc/cosign_sign-blob.md b/doc/cosign_sign-blob.md index 951b2e52894..085de7788d1 100644 --- a/doc/cosign_sign-blob.md +++ b/doc/cosign_sign-blob.md @@ -1,6 +1,6 @@ ## cosign sign-blob -Sign the supplied blob, outputting the base64-encoded signature to stdout +Sign the supplied blob, outputting the bundle to a file ``` cosign sign-blob [flags] @@ -9,25 +9,25 @@ cosign sign-blob [flags] ### Examples ``` - cosign sign-blob --key | + cosign sign-blob --key | --bundle # sign a blob with a local key pair file - cosign sign-blob --key cosign.key + cosign sign-blob --key cosign.key --bundle # sign a blob with a key stored in an environment variable - cosign sign-blob --key env://[ENV_VAR] + cosign sign-blob --key env://[ENV_VAR] --bundle # sign a blob with a key pair stored in Azure Key Vault - cosign sign-blob --key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] + cosign sign-blob --key azurekms://[VAULT_NAME][VAULT_URI]/[KEY] --bundle # sign a blob with a key pair stored in AWS KMS - cosign sign-blob --key awskms://[ENDPOINT]/[ID/ALIAS/ARN] + cosign sign-blob --key awskms://[ENDPOINT]/[ID/ALIAS/ARN] --bundle # sign a blob with a key pair stored in Google Cloud KMS - cosign sign-blob --key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY] + cosign sign-blob --key gcpkms://projects/[PROJECT]/locations/global/keyRings/[KEYRING]/cryptoKeys/[KEY] --bundle # sign a blob with a key pair stored in Hashicorp Vault - cosign sign-blob --key hashivault://[KEY] + cosign sign-blob --key hashivault://[KEY] --bundle ``` ### Options diff --git a/doc/cosign_sign.md b/doc/cosign_sign.md index 98eafbac0d7..42d05a8fc1e 100644 --- a/doc/cosign_sign.md +++ b/doc/cosign_sign.md @@ -18,7 +18,7 @@ cosign sign [flags] ### Examples ``` - cosign sign --key | [-a key=value] [--upload=true|false] [-f] [-r] + cosign sign --key | [-a key=value] [--upload=true|false] [-y] [-r] # sign a container image with the Sigstore OIDC flow cosign sign diff --git a/pkg/cosign/fuzz_test.go b/pkg/cosign/fuzz_test.go index 9cf9afc579f..20973c8d67b 100644 --- a/pkg/cosign/fuzz_test.go +++ b/pkg/cosign/fuzz_test.go @@ -16,19 +16,9 @@ package cosign import ( - "context" "os" "path/filepath" "testing" - - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - - "github.com/sigstore/cosign/v3/pkg/oci/mutate" -) - -var ( - defaultFuzzRef = "fuzz/test" ) func fuzzPass(s string) PassFunc { @@ -67,57 +57,3 @@ func FuzzImportKeyPairLoadPrivateKey(f *testing.F) { } }) } - -func FuzzSigVerify(f *testing.F) { - f.Fuzz(func(t *testing.T, sigData, payloadData []byte, verificationTest int) { - path := t.TempDir() - sigPath := filepath.Join(path, "sigFile") - err := os.WriteFile(sigPath, sigData, 0x755) - if err != nil { - return - } - payloadPath := filepath.Join(path, "payloadFile") - err = os.WriteFile(payloadPath, payloadData, 0x755) - if err != nil { - return - } - ref, err := name.ParseReference(defaultFuzzRef) - if err != nil { - panic(err) - } - sigs, err := loadSignatureFromFile(context.Background(), sigPath, ref, &CheckOpts{PayloadRef: payloadPath}) - if err != nil { - return - } - switch verificationTest % 5 { - case 0: - VerifyImageAttestation(context.Background(), sigs, v1.Hash{}, &CheckOpts{IgnoreTlog: true}) - case 1: - verifySignatures(context.Background(), sigs, v1.Hash{}, &CheckOpts{IgnoreTlog: true}) - case 2: - sl, err := sigs.Get() - if err != nil { - t.Fatal(err) - } - for _, sig := range sl { - VerifyBlobSignature(context.Background(), sig, &CheckOpts{IgnoreTlog: true}) - } - case 3: - sl, err := sigs.Get() - if err != nil { - t.Fatal(err) - } - for _, sig := range sl { - VerifyImageSignature(context.Background(), sig, v1.Hash{}, &CheckOpts{IgnoreTlog: true}) - } - case 4: - sl, err := sigs.Get() - if err != nil { - t.Fatal(err) - } - for _, sig := range sl { - mutate.Signature(sig) - } - } - }) -} diff --git a/pkg/cosign/verify.go b/pkg/cosign/verify.go index e51c1216eb1..f3ed2b0f5bf 100644 --- a/pkg/cosign/verify.go +++ b/pkg/cosign/verify.go @@ -21,14 +21,12 @@ import ( "crypto/ecdsa" "crypto/sha256" "crypto/x509" - "encoding/asn1" "encoding/base64" "encoding/hex" "encoding/json" "encoding/pem" "errors" "fmt" - "io/fs" "log" "net/http" "os" @@ -45,17 +43,12 @@ import ( ggcrlayout "github.com/google/go-containerregistry/pkg/v1/layout" "github.com/google/go-containerregistry/pkg/v1/remote/transport" "github.com/nozzle/throttler" - ssldsse "github.com/secure-systems-lab/go-securesystemslib/dsse" "github.com/sigstore/cosign/v3/internal/pkg/cosign" - ociexperimental "github.com/sigstore/cosign/v3/internal/pkg/oci/remote" "github.com/sigstore/cosign/v3/internal/ui" - "github.com/sigstore/cosign/v3/pkg/blob" cbundle "github.com/sigstore/cosign/v3/pkg/cosign/bundle" "github.com/sigstore/cosign/v3/pkg/oci" - "github.com/sigstore/cosign/v3/pkg/oci/layout" ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" "github.com/sigstore/cosign/v3/pkg/oci/static" - "github.com/sigstore/cosign/v3/pkg/types" protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" "github.com/sigstore/rekor/pkg/generated/client" "github.com/sigstore/rekor/pkg/generated/models" @@ -72,8 +65,6 @@ import ( "github.com/sigstore/sigstore-go/pkg/verify" "github.com/sigstore/sigstore/pkg/cryptoutils" "github.com/sigstore/sigstore/pkg/signature" - "github.com/sigstore/sigstore/pkg/signature/dsse" - "github.com/sigstore/sigstore/pkg/signature/options" "github.com/sigstore/sigstore/pkg/tuf" tsaverification "github.com/sigstore/timestamp-authority/v2/pkg/verification" "google.golang.org/protobuf/encoding/protojson" @@ -117,11 +108,6 @@ type CheckOpts struct { // PKOpts are the options provided to `SigVerifier.PublicKey()`. PKOpts []signature.PublicKeyOption - // RootCerts are the root CA certs used to verify a signature's chained certificate. - RootCerts *x509.CertPool - // IntermediateCerts are the optional intermediate CA certs used to verify a certificate chain. - IntermediateCerts *x509.CertPool - // CertGithubWorkflowTrigger is the GitHub Workflow Trigger name expected for a certificate to be valid. The empty string means any certificate can be valid. CertGithubWorkflowTrigger string // CertGithubWorkflowSha is the GitHub Workflow SHA expected for a certificate to be valid. The empty string means any certificate can be valid. @@ -138,9 +124,6 @@ type CheckOpts struct { IgnoreSCT bool // Detached SCT. Optional, as the SCT is usually embedded in the certificate. SCT []byte - // CTLogPubKeys, if set, is used to validate SCTs against those keys. - // It is a map from log id to LogIDMetadata. It is a map from LogID to crypto.PublicKey. LogID is derived from the PublicKey (see RFC 6962 S3.2). - CTLogPubKeys *TrustedTransparencyLogPubKeys // SignatureRef is the reference to the signature file. PayloadRef should always be specified as well (though it’s possible for a _some_ signatures to be verified without it, with a warning). SignatureRef string @@ -174,10 +157,6 @@ type CheckOpts struct { // Should the experimental OCI 1.1 behaviour be enabled or not. // Defaults to false. ExperimentalOCI11 bool - - // NewBundleFormat enables the new bundle format (Cosign Bundle Spec) and the new verifier. - NewBundleFormat bool - // AllowCertificateChain permits bundles with version >= v0.3 to contain // X.509 certificate chains in the verification material. AllowCertificateChain bool @@ -198,10 +177,13 @@ type verifyTrustedMaterial struct { } func (v *verifyTrustedMaterial) PublicKeyVerifier(hint string) (root.TimeConstrainedVerifier, error) { - if v.keyTrustedMaterial == nil { - return nil, fmt.Errorf("no public key material available") + if v.keyTrustedMaterial != nil { + return v.keyTrustedMaterial.PublicKeyVerifier(hint) + } + if v.TrustedMaterial != nil { + return v.TrustedMaterial.PublicKeyVerifier(hint) } - return v.keyTrustedMaterial.PublicKeyVerifier(hint) + return nil, fmt.Errorf("no public key material available") } // verificationOptions returns the verification options for verifying with sigstore-go. @@ -291,177 +273,6 @@ func (co *CheckOpts) verificationOptions() (trustedMaterial root.TrustedMaterial return vTrustedMaterial, verifierOptions, policyOptions, nil } -// This is a substitutable signature verification function that can be used for verifying -// attestations of blobs. -type signatureVerificationFn func( - ctx context.Context, verifier signature.Verifier, sig payloader) error - -// For unit testing -type payloader interface { - // no-op for attestations - Base64Signature() (string, error) - Payload() ([]byte, error) -} - -func verifyOCIAttestation(ctx context.Context, verifier signature.Verifier, att payloader) error { - payload, err := att.Payload() - if err != nil { - return err - } - - env := ssldsse.Envelope{} - if err := json.Unmarshal(payload, &env); err != nil { - return err - } - - if env.PayloadType != types.IntotoPayloadType { - return &VerificationFailure{ - fmt.Errorf("invalid payloadType %s on envelope. Expected %s", env.PayloadType, types.IntotoPayloadType), - } - } - dssev, err := ssldsse.NewEnvelopeVerifier(&dsse.VerifierAdapter{SignatureVerifier: verifier}) - if err != nil { - return err - } - _, err = dssev.Verify(ctx, &env) - return err -} - -func verifyOCISignature(ctx context.Context, verifier signature.Verifier, sig payloader) error { - b64sig, err := sig.Base64Signature() - if err != nil { - return err - } - signature, err := base64.StdEncoding.DecodeString(b64sig) - if err != nil { - return err - } - payload, err := sig.Payload() - if err != nil { - return err - } - return verifier.VerifySignature(bytes.NewReader(signature), bytes.NewReader(payload), options.WithContext(ctx)) -} - -type verifierWithCertChain struct { - signature.Verifier - cert *x509.Certificate - chain []*x509.Certificate -} - -func (v *verifierWithCertChain) GetCert() *x509.Certificate { - return v.cert -} - -func (v *verifierWithCertChain) GetChain() []*x509.Certificate { - return v.chain -} - -// ValidateAndUnpackCert creates a Verifier from a certificate. Verifies that the -// certificate chains up to a trusted root using intermediate certificate chain coming from CheckOpts. -// Optionally verifies the subject and issuer of the certificate. -func ValidateAndUnpackCert(cert *x509.Certificate, co *CheckOpts) (signature.Verifier, error) { - verifier, _, err := ValidateAndUnpackCertWithIntermediates(cert, co, co.IntermediateCerts) - return verifier, err -} - -// ValidateAndUnpackCertWithIntermediates creates a Verifier from a certificate. Verifies that the -// certificate chains up to a trusted root using intermediate cert passed as separate argument. -// Optionally verifies the subject and issuer of the certificate. Returns the chain built from the -// certificate pools. Clients must verify the validity of this chain against a provided timestamp. -func ValidateAndUnpackCertWithIntermediates(cert *x509.Certificate, co *CheckOpts, intermediateCerts *x509.CertPool) (signature.Verifier, []*x509.Certificate, error) { - verifier, err := signature.LoadVerifier(cert.PublicKey, crypto.SHA256) - if err != nil { - return nil, nil, fmt.Errorf("invalid certificate found on signature: %w", err) - } - - // Handle certificates where the Subject Alternative Name is not set to a supported - // GeneralName (RFC 5280 4.2.1.6). Go only supports DNS, IP addresses, email addresses, - // or URIs as SANs. Fulcio can issue a certificate with an OtherName GeneralName, so - // remove the unhandled critical SAN extension before verifying. - if len(cert.UnhandledCriticalExtensions) > 0 { - var unhandledExts []asn1.ObjectIdentifier - for _, oid := range cert.UnhandledCriticalExtensions { - if !oid.Equal(cryptoutils.SANOID) { - unhandledExts = append(unhandledExts, oid) - } - } - cert.UnhandledCriticalExtensions = unhandledExts - } - - // Now verify the cert, then the signature. - - // If trusted root is available, use the verifiers from sigstore-go (preferred). - var chains [][]*x509.Certificate - if co.TrustedMaterial != nil { - if chains, err = verify.VerifyLeafCertificate(cert.NotBefore, cert, co.TrustedMaterial); err != nil { - return nil, nil, err - } - } else { - // If the trusted root is not available, use the verifiers from cosign (legacy). - chains, err = TrustedCert(cert, co.RootCerts, intermediateCerts) - if err != nil { - return nil, nil, err - } - } - - // handle if chains has more than one chain - grab first and print message - if len(chains) > 1 { - fmt.Fprintf(os.Stderr, "**Info** Multiple valid certificate chains found. Selecting the first for further verification.\n") - } - chain := chains[0] - - err = CheckCertificatePolicy(cert, co) - if err != nil { - return nil, nil, err - } - - // If IgnoreSCT is set, skip the SCT check - if co.IgnoreSCT { - return &verifierWithCertChain{Verifier: verifier, cert: cert, chain: chains[0]}, chains[0], nil - } - contains, err := ContainsSCT(cert.Raw) - if err != nil { - return nil, nil, err - } - if !contains && len(co.SCT) == 0 { - return nil, nil, &VerificationFailure{ - fmt.Errorf("certificate does not include required embedded SCT and no detached SCT was set"), - } - } - - // If trusted root is available and the SCT is embedded, use the verifiers from sigstore-go (preferred). - if co.TrustedMaterial != nil && contains { - if err := verify.VerifySignedCertificateTimestamp(chains, 1, co.TrustedMaterial); err != nil { - return nil, nil, err - } - return &verifierWithCertChain{Verifier: verifier, cert: cert, chain: chain}, chain, nil - } - - if len(chain) < 2 { - return nil, nil, errors.New("certificate chain must contain at least a certificate and its issuer") - } - if contains { - if err := VerifyEmbeddedSCT(context.Background(), chain, co.CTLogPubKeys); err != nil { - return nil, nil, err - } - return &verifierWithCertChain{Verifier: verifier, cert: cert, chain: chain}, chain, nil - } - certPEM, err := cryptoutils.MarshalCertificateToPEM(chain[0]) - if err != nil { - return nil, nil, err - } - chainPEM, err := cryptoutils.MarshalCertificatesToPEM(chain[1:]) - if err != nil { - return nil, nil, err - } - if err := VerifySCT(context.Background(), certPEM, chainPEM, co.SCT, co.CTLogPubKeys); err != nil { - return nil, nil, err - } - - return &verifierWithCertChain{Verifier: verifier, cert: cert, chain: chain}, chain, nil -} - // CheckCertificatePolicy checks that the certificate subject and issuer match // the expected values. func CheckCertificatePolicy(cert *x509.Certificate, co *CheckOpts) error { @@ -574,408 +385,6 @@ func validateCertExtensions(ce CertExtensions, co *CheckOpts) error { return nil } -// ValidateAndUnpackCertWithChain creates a Verifier from a certificate. Verifies that the certificate -// chains up to the provided root. Chain should start with the parent of the certificate and end with the root. -// Optionally verifies the subject and issuer of the certificate. -func ValidateAndUnpackCertWithChain(cert *x509.Certificate, chain []*x509.Certificate, co *CheckOpts) (signature.Verifier, error) { - if len(chain) == 0 { - return nil, errors.New("no chain provided to validate certificate") - } - rootPool := x509.NewCertPool() - rootPool.AddCert(chain[len(chain)-1]) - co.RootCerts = rootPool - - subPool := x509.NewCertPool() - for _, c := range chain[:len(chain)-1] { - subPool.AddCert(c) - } - co.IntermediateCerts = subPool - - return ValidateAndUnpackCert(cert, co) -} - -func tlogValidateEntry(ctx context.Context, client *client.Rekor, rekorPubKeys *TrustedTransparencyLogPubKeys, trustedMaterial root.TrustedMaterial, - sig oci.Signature, pem []byte) (*models.LogEntryAnon, error) { - b64sig, err := sig.Base64Signature() - if err != nil { - return nil, err - } - payload, err := sig.Payload() - if err != nil { - return nil, err - } - tlogEntries, err := FindTlogEntry(ctx, client, b64sig, payload, pem) - if err != nil { - return nil, err - } - if len(tlogEntries) == 0 { - return nil, fmt.Errorf("no valid tlog entries found with proposed entry") - } - // Always return the earliest integrated entry. That - // always suffices for verification of signature time. - var earliestLogEntry models.LogEntryAnon - var earliestLogEntryTime *time.Time - entryVerificationErrs := make([]string, 0) - for _, e := range tlogEntries { - entry := e - if err := VerifyTLogEntryOffline(ctx, &entry, rekorPubKeys, trustedMaterial); err != nil { - entryVerificationErrs = append(entryVerificationErrs, err.Error()) - continue - } - entryTime := time.Unix(*entry.IntegratedTime, 0) - if earliestLogEntryTime == nil || entryTime.Before(*earliestLogEntryTime) { - earliestLogEntryTime = &entryTime - earliestLogEntry = entry - } - } - if earliestLogEntryTime == nil { - return nil, fmt.Errorf("no valid tlog entries found %s", strings.Join(entryVerificationErrs, ", ")) - } - return &earliestLogEntry, nil -} - -type fakeOCISignatures struct { - oci.Signatures - signatures []oci.Signature -} - -func (fos *fakeOCISignatures) Get() ([]oci.Signature, error) { - return fos.signatures, nil -} - -// VerifyImageSignatures does all the main cosign checks in a loop, returning the verified signatures. -// If there were no valid signatures, we return an error. -// Note that if co.ExperimentlOCI11 is set, we will attempt to verify -// signatures using the experimental OCI 1.1 behavior. -func VerifyImageSignatures(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { - // Try first using OCI 1.1 behavior if experimental flag is set. - if co.ExperimentalOCI11 { - verified, bundleVerified, err := verifyImageSignaturesExperimentalOCI(ctx, signedImgRef, co) - if err == nil { - return verified, bundleVerified, nil - } - } - - 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 && co.TrustedMaterial == nil { - return nil, false, errors.New("one of verifier, root certs, or trusted root is required") - } - - // 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, false, &ErrImageTagNotFound{ - fmt.Errorf("image tag not found: %w", err), - } - } - return nil, false, err - } - h, err := v1.NewHash(digest.Identifier()) - if err != nil { - return nil, false, err - } - - var sigs oci.Signatures - sigRef := co.SignatureRef - if sigRef == "" { - st, err := ociremote.SignatureTag(digest, co.RegistryClientOpts...) - if err != nil { - return nil, false, err - } - sigs, err = ociremote.Signatures(st, co.RegistryClientOpts...) - if err != nil { - return nil, false, err - } - } else { - sigs, err = loadSignatureFromFile(ctx, sigRef, signedImgRef, co) - if err != nil { - return nil, false, err - } - } - - return verifySignatures(ctx, sigs, h, co) -} - -// VerifyLocalImageSignatures verifies signatures from a saved, local image, without any network calls, returning the verified signatures. -// If there were no valid signatures, we return an error. -func VerifyLocalImageSignatures(ctx context.Context, path string, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { - // Enforce this up front. - if co.RootCerts == nil && co.SigVerifier == nil && co.TrustedMaterial == nil { - return nil, false, errors.New("one of verifier, root certs, or trusted root is required") - } - - se, err := layout.SignedImageIndex(path) - if err != nil { - return nil, false, err - } - - var h v1.Hash - // Verify either an image index or image. - ii, err := se.SignedImageIndex(v1.Hash{}) - if err != nil { - return nil, false, err - } - i, err := se.SignedImage(v1.Hash{}) - if err != nil { - return nil, false, err - } - switch { - case ii != nil: - h, err = ii.Digest() - if err != nil { - return nil, false, err - } - case i != nil: - h, err = i.Digest() - if err != nil { - return nil, false, err - } - default: - return nil, false, errors.New("must verify either an image index or image") - } - - sigs, err := se.Signatures() - if err != nil { - return nil, false, err - } - if sigs == nil { - return nil, false, fmt.Errorf("no signatures associated with the image saved in %s", path) - } - - return verifySignatures(ctx, sigs, h, co) -} - -func verifySignatures(ctx context.Context, sigs oci.Signatures, h v1.Hash, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { - sl, err := sigs.Get() - if err != nil { - return nil, false, err - } - - if len(sl) == 0 { - return nil, false, &ErrNoSignaturesFound{ - errors.New("no signatures found"), - } - } - - signatures := make([]oci.Signature, len(sl)) - bundlesVerified := make([]bool, len(sl)) - - workers := co.MaxWorkers - if co.MaxWorkers == 0 { - workers = cosign.DefaultMaxWorkers - } - t := throttler.New(workers, len(sl)) - for i, sig := range sl { - go func(sig oci.Signature, index int) { - sig, err := static.Copy(sig) - if err != nil { - t.Done(err) - return - } - - verified, err := VerifyImageSignature(ctx, sig, h, co) - bundlesVerified[index] = verified - if err != nil { - t.Done(err) - return - } - signatures[index] = sig - - t.Done(nil) - }(sig, i) - - // wait till workers are available - t.Throttle() - } - - for _, s := range signatures { - if s != nil { - checkedSignatures = append(checkedSignatures, s) - } - } - - for _, verified := range bundlesVerified { - bundleVerified = bundleVerified || verified - } - - if len(checkedSignatures) == 0 { - var combinedErrors []string - for _, err := range t.Errs() { - combinedErrors = append(combinedErrors, err.Error()) - } - // TODO: ErrNoMatchingSignatures.Unwrap should return []error, - // or we should replace "...%s" strings.Join with "...%w", errors.Join. - return nil, false, &ErrNoMatchingSignatures{ - fmt.Errorf("no matching signatures: %s", strings.Join(combinedErrors, "\n ")), - } - } - - return checkedSignatures, bundleVerified, nil -} - -// verifyInternal holds the main verification flow for signatures and attestations. -// 1. Verifies the signature using the provided verifier. -// 2. Checks for transparency log entry presence: -// a. Verifies the Rekor entry in the bundle, if provided. This works offline OR -// b. If we don't have a Rekor entry retrieved via cert, do an online lookup (assuming -// we are in experimental mode). -// 3. If a certificate is provided, check its expiration using the transparency log timestamp. -func verifyInternal(ctx context.Context, sig oci.Signature, h v1.Hash, - verifyFn signatureVerificationFn, co *CheckOpts) ( - bundleVerified bool, err error) { - var acceptableRFC3161Time, acceptableRekorBundleTime *time.Time // Timestamps for the signature we accept, or nil if not applicable. - - var acceptableRFC3161Timestamp *timestamp.Timestamp - if co.UseSignedTimestamps { - acceptableRFC3161Timestamp, err = VerifyRFC3161Timestamp(sig, co) - if err != nil { - return false, fmt.Errorf("unable to verify RFC3161 timestamp bundle: %w", err) - } - if acceptableRFC3161Timestamp != nil { - acceptableRFC3161Time = &acceptableRFC3161Timestamp.Time - } - } - - if !co.IgnoreTlog { - bundleVerified, err = VerifyBundle(sig, co) - if err != nil { - return false, fmt.Errorf("error verifying bundle: %w", err) - } - - if bundleVerified { - // Update with the verified bundle's integrated time. - t, err := getBundleIntegratedTime(sig) - if err != nil { - return false, fmt.Errorf("error getting bundle integrated time: %w", err) - } - acceptableRekorBundleTime = &t - } else { - // If the --offline flag was specified, fail here. bundleVerified returns false with - // no error when there was no bundle provided. - if co.Offline { - return false, fmt.Errorf("offline verification failed") - } - - // no Rekor client provided for an online lookup - if co.RekorClient == nil { - return false, fmt.Errorf("rekor client not provided for online verification") - } - - pemBytes, err := keyBytes(sig, co) - if err != nil { - return false, err - } - - e, err := tlogValidateEntry(ctx, co.RekorClient, co.RekorPubKeys, co.TrustedMaterial, sig, pemBytes) - if err != nil { - return false, err - } - t := time.Unix(*e.IntegratedTime, 0) - acceptableRekorBundleTime = &t - bundleVerified = true - } - } - - verifier := co.SigVerifier - var verifierChain []*x509.Certificate - if verifier == nil { - // If we don't have a public key to check against, we can try a root cert. - cert, err := sig.Cert() - if err != nil { - return false, err - } - if cert == nil { - return false, &ErrNoCertificateFoundOnSignature{ - fmt.Errorf("no certificate found on signature"), - } - } - // Create a certificate pool for intermediate CA certificates, excluding the root - chain, err := sig.Chain() - if err != nil { - return false, err - } - // If there is no chain annotation present, we preserve the pools set in the CheckOpts. - var pool *x509.CertPool - if len(chain) > 1 { - if co.IntermediateCerts == nil { - // If the intermediate certs have not been loaded in by TUF - pool = x509.NewCertPool() - for _, cert := range chain[:len(chain)-1] { - pool.AddCert(cert) - } - } - } - // In case pool is not set than set it from co.IntermediateCerts - if pool == nil { - pool = co.IntermediateCerts - } - verifier, chain, err = ValidateAndUnpackCertWithIntermediates(cert, co, pool) - if err != nil { - return false, err - } - // Remove the end-entity certificate from the chain, as that will come from sig.Cert() - verifierChain = chain[1:] - } - - // 1. Perform cryptographic verification of the signature using the certificate's public key. - if err := verifyFn(ctx, verifier, sig); err != nil { - return false, err - } - - // We can't check annotations without claims, both require unmarshalling the payload. - if co.ClaimVerifier != nil { - if err := co.ClaimVerifier(sig, h, co.Annotations); err != nil { - return false, err - } - } - - // 2. if a certificate was used, verify the certificate expiration against a time - cert, err := sig.Cert() - if err != nil { - return false, err - } - if cert != nil { - // use the provided Rekor bundle or RFC3161 timestamp to check certificate expiration - expirationChecked := false - - if acceptableRFC3161Time != nil { - // Verify the cert against the timestamp time. - if err := CheckExpiry(cert, verifierChain, *acceptableRFC3161Time); err != nil { - return false, fmt.Errorf("checking expiry on certificate with timestamp: %w", err) - } - expirationChecked = true - } - - if acceptableRekorBundleTime != nil { - if err := CheckExpiry(cert, verifierChain, *acceptableRekorBundleTime); err != nil { - return false, fmt.Errorf("checking expiry on certificate with bundle: %w", err) - } - expirationChecked = true - } - - // if no timestamp has been provided, use the current time - if !expirationChecked { - if err := CheckExpiry(cert, verifierChain, time.Now()); err != nil { - // If certificate is expired and not signed timestamp was provided then error the following message. Otherwise throw an expiration error. - if co.IgnoreTlog && acceptableRFC3161Time == nil { - return false, &VerificationFailure{ - fmt.Errorf("expected a signed timestamp to verify an expired certificate"), - } - } - return false, fmt.Errorf("checking expiry on certificate with bundle: %w", err) - } - } - } - - return bundleVerified, nil -} - func keyBytes(sig oci.Signature, co *CheckOpts) ([]byte, error) { cert, err := sig.Cert() if err != nil { @@ -1000,240 +409,26 @@ func keyBytes(sig oci.Signature, co *CheckOpts) ([]byte, error) { return cryptoutils.MarshalPublicKeyToPEM(pub) } -// VerifyBlobSignature verifies a blob signature. -func VerifyBlobSignature(ctx context.Context, sig oci.Signature, co *CheckOpts) (bundleVerified bool, err error) { - // The hash of the artifact is unused. - return verifyInternal(ctx, sig, v1.Hash{}, verifyOCISignature, co) -} - -// VerifyImageSignature verifies a signature -func VerifyImageSignature(ctx context.Context, sig oci.Signature, h v1.Hash, co *CheckOpts) (bundleVerified bool, err error) { - return verifyInternal(ctx, sig, h, verifyOCISignature, co) -} - -func loadSignatureFromFile(ctx context.Context, sigRef string, signedImgRef name.Reference, co *CheckOpts) (oci.Signatures, error) { - var b64sig string - targetSig, err := blob.LoadFileOrURL(sigRef) - if err != nil { - if !errors.Is(err, fs.ErrNotExist) { - return nil, err - } - targetSig = []byte(sigRef) - } - - _, err = base64.StdEncoding.DecodeString(string(targetSig)) - - if err == nil { - b64sig = string(targetSig) - } else { - b64sig = base64.StdEncoding.EncodeToString(targetSig) - } - - var payload []byte - if co.PayloadRef != "" { - payload, err = blob.LoadFileOrURL(co.PayloadRef) - if err != nil { - return nil, err - } - } else { - digest, err := ociremote.ResolveDigest(signedImgRef, co.RegistryClientOpts...) - if err != nil { - return nil, err - } - payload, err = ObsoletePayload(ctx, digest) - if err != nil { - return nil, err - } - } - - var opts []static.Option - if co.SigVerifier != nil { - if cb, ok := co.SigVerifier.(interface{ GetCert() *x509.Certificate }); ok { - if cert := cb.GetCert(); cert != nil { - var chain []*x509.Certificate - if ch, ok := co.SigVerifier.(interface{ GetChain() []*x509.Certificate }); ok { - chain = ch.GetChain() - } - - certPEM, err := cryptoutils.MarshalCertificateToPEM(cert) - if err != nil { - return nil, err - } - var chainPEM []byte - if len(chain) > 0 { - chainPEM, err = cryptoutils.MarshalCertificatesToPEM(chain) - if err != nil { - return nil, err - } - } - - opts = append(opts, static.WithCertChain(certPEM, chainPEM)) - } - } - } - - sig, err := static.NewSignature(payload, b64sig, opts...) - if err != nil { - return nil, err - } - return &fakeOCISignatures{ - signatures: []oci.Signature{sig}, - }, nil -} - // VerifyImageAttestations does all the main cosign checks in a loop, returning the verified attestations. // If there were no valid attestations, we return an error. func VerifyImageAttestations(ctx context.Context, signedImgRef name.Reference, co *CheckOpts, nameOpts ...name.Option) (checkedAttestations []oci.Signature, bundleVerified bool, err error) { - // Enforce this up front. - 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, nameOpts...) - } - - // This is a carefully optimized sequence for fetching the attestations of - // the entity that minimizes registry requests when supplied with a digest - // input. - digest, err := ociremote.ResolveDigest(signedImgRef, co.RegistryClientOpts...) - if err != nil { - return nil, false, err - } - h, err := v1.NewHash(digest.Identifier()) - if err != nil { - return nil, false, err - } - st, err := ociremote.AttestationTag(digest, co.RegistryClientOpts...) - if err != nil { - return nil, false, err - } - atts, err := ociremote.Signatures(st, co.RegistryClientOpts...) + bundles, hash, err := GetBundles(ctx, signedImgRef, co.RegistryClientOpts, nameOpts...) if err != nil { return nil, false, err } - - return VerifyImageAttestation(ctx, atts, h, co) + return verifyImageAttestationsSigstoreBundles(ctx, bundles, hash, co) } // VerifyLocalImageAttestations verifies attestations from a saved, local image, without any network calls, // returning the verified attestations. // If there were no valid signatures, we return an error. func VerifyLocalImageAttestations(ctx context.Context, path string, co *CheckOpts) (checkedAttestations []oci.Signature, bundleVerified bool, err error) { - // Enforce this up front. - if co.RootCerts == nil && co.SigVerifier == nil && co.TrustedMaterial == nil { - return nil, false, errors.New("one of verifier, root certs, or trusted root is required") - } - - // Check for v3 bundles first (if NewBundleFormat is enabled) - if co.NewBundleFormat { - return verifyLocalImageAttestationsSigstoreBundle(ctx, path, co) - } - - se, err := layout.SignedImageIndex(path) - if err != nil { - return nil, false, err - } - - var h v1.Hash - // Verify either an image index or image. - ii, err := se.SignedImageIndex(v1.Hash{}) - if err != nil { - return nil, false, err - } - i, err := se.SignedImage(v1.Hash{}) - if err != nil { - return nil, false, err - } - switch { - case ii != nil: - h, err = ii.Digest() - if err != nil { - return nil, false, err - } - case i != nil: - h, err = i.Digest() - if err != nil { - return nil, false, err - } - default: - return nil, false, errors.New("must verify either an image index or image") - } - - atts, err := se.Attestations() - if err != nil { - return nil, false, err - } - return VerifyImageAttestation(ctx, atts, h, co) -} - -func VerifyBlobAttestation(ctx context.Context, att oci.Signature, h v1.Hash, co *CheckOpts) ( - bool, error) { - return verifyInternal(ctx, att, h, verifyOCIAttestation, co) -} - -func VerifyImageAttestation(ctx context.Context, atts oci.Signatures, h v1.Hash, co *CheckOpts) (checkedAttestations []oci.Signature, bundleVerified bool, err error) { - if atts == nil { - return nil, false, errors.New("no attestations provided") - } - sl, err := atts.Get() + bundles, hash, err := GetLocalBundles(path) if err != nil { return nil, false, err } - attestations := make([]oci.Signature, len(sl)) - bundlesVerified := make([]bool, len(sl)) - - workers := co.MaxWorkers - if co.MaxWorkers == 0 { - workers = cosign.DefaultMaxWorkers - } - t := throttler.New(workers, len(sl)) - for i, att := range sl { - go func(att oci.Signature, index int) { - att, err := static.Copy(att) - if err != nil { - t.Done(err) - return - } - if err := func(att oci.Signature) error { - verified, err := verifyInternal(ctx, att, h, verifyOCIAttestation, co) - bundlesVerified[index] = verified - return err - }(att); err != nil { - t.Done(err) - return - } - - attestations[index] = att - t.Done(nil) - }(att, 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 + return verifyImageAttestationsSigstoreBundles(ctx, bundles, hash, co) } // CheckExpiry confirms the time provided is within the valid period of the certificate and optionally @@ -1271,16 +466,6 @@ func CheckExpiry(cert *x509.Certificate, issuingChain []*x509.Certificate, it ti return nil } -func getBundleIntegratedTime(sig oci.Signature) (time.Time, error) { - bundle, err := sig.Bundle() - if err != nil { - return time.Now(), err - } else if bundle == nil { - return time.Now(), nil - } - return time.Unix(bundle.Payload.IntegratedTime, 0), nil -} - // This verifies an offline bundle contained in the sig against the trusted // Rekor publicKeys. func VerifyBundle(sig oci.Signature, co *CheckOpts) (bool, error) { @@ -1666,64 +851,6 @@ func correctAnnotations(wanted, have map[string]interface{}) bool { return true } -// verifyImageSignaturesExperimentalOCI does all the main cosign checks in a loop, returning the verified signatures. -// If there were no valid signatures, we return an error, using OCI 1.1+ behavior. -func verifyImageSignaturesExperimentalOCI(ctx context.Context, signedImgRef name.Reference, co *CheckOpts) (checkedSignatures []oci.Signature, bundleVerified bool, err error) { - // Enforce this up front. - if co.RootCerts == nil && co.SigVerifier == nil && co.TrustedMaterial == nil { - return nil, false, errors.New("one of verifier, root certs, or trusted root is required") - } - - // 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 { - return nil, false, err - } - h, err := v1.NewHash(digest.Identifier()) - if err != nil { - return nil, false, err - } - - var sigs oci.Signatures - sigRef := co.SignatureRef - if sigRef == "" { - artifactType := ociexperimental.ArtifactType("sig") - index, err := ociremote.Referrers(digest, artifactType, co.RegistryClientOpts...) - if err != nil { - return nil, false, err - } - results := index.Manifests - numResults := len(results) - if numResults == 0 { - return nil, false, fmt.Errorf("unable to locate reference with artifactType %s", artifactType) - } else if numResults > 1 { - // TODO: if there is more than 1 result.. what does that even mean? - ui.Warnf(ctx, "there were a total of %d references with artifactType %s\n", numResults, artifactType) - } - // TODO: do this smarter using "created" annotations - lastResult := results[numResults-1] - st, err := name.ParseReference(fmt.Sprintf("%s@%s", digest.Repository, lastResult.Digest.String())) - if err != nil { - return nil, false, err - } - sigs, err = ociremote.Signatures(st, co.RegistryClientOpts...) - if err != nil { - return nil, false, err - } - } else { - if co.PayloadRef == "" { - return nil, false, errors.New("payload is required with a manually-provided signature") - } - sigs, err = loadSignatureFromFile(ctx, sigRef, signedImgRef, co) - if err != nil { - return nil, false, err - } - } - - return verifySignatures(ctx, sigs, h, co) -} - func GetBundles(_ context.Context, signedImgRef name.Reference, registryClientOpts []ociremote.Option, nameOpts ...name.Option) ([]*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 @@ -1865,7 +992,7 @@ func getLocalBundleDescriptors(path string) ([]bundleDescriptor, *v1.Hash, error // Find the target image digest from the index manifest var targetDigest v1.Hash for _, m := range manifest.Manifests { - if val, ok := m.Annotations["kind"]; ok && val == "dev.cosignproject.cosign/image" { + if val, ok := m.Annotations["kind"]; ok && (val == "dev.cosignproject.cosign/image" || val == "dev.cosignproject.cosign/imageIndex") { targetDigest = m.Digest break } @@ -1920,11 +1047,11 @@ func getLocalBundleDescriptors(path string) ([]bundleDescriptor, *v1.Hash, error return descriptors, &targetDigest, nil } -// verifyImageAttestationsSigstoreBundle verifies attestations from attached sigstore bundles -func verifyImageAttestationsSigstoreBundle(ctx context.Context, signedImgRef name.Reference, co *CheckOpts, nameOpts ...name.Option) (checkedAttestations []oci.Signature, atLeastOneBundleVerified bool, err error) { - bundles, hash, err := GetBundles(ctx, signedImgRef, co.RegistryClientOpts, nameOpts...) - if err != nil { - return nil, false, err +// verifyImageAttestationsSigstoreBundles verifies attestations from attached sigstore bundles +func verifyImageAttestationsSigstoreBundles(ctx context.Context, bundles []*sgbundle.Bundle, hash *v1.Hash, co *CheckOpts) (checkedAttestations []oci.Signature, atLeastOneBundleVerified bool, err error) { + // Enforce this up front. + if co.SigVerifier == nil && co.TrustedMaterial == nil { + return nil, false, errors.New("one of verifier or trusted root is required") } digestBytes, err := hex.DecodeString(hash.Hex) @@ -2009,72 +1136,3 @@ func verifyImageAttestationsSigstoreBundle(ctx context.Context, signedImgRef nam return checkedAttestations, atLeastOneBundleVerified, nil } - -// verifyLocalImageAttestationsSigstoreBundle verifies attestations from local sigstore bundles -func verifyLocalImageAttestationsSigstoreBundle(ctx context.Context, path string, co *CheckOpts) (checkedAttestations []oci.Signature, bundleVerified bool, err error) { - bundles, hash, err := GetLocalBundles(path, co.BundleOptions()...) - 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) - - // For local bundles, we verify sequentially (local I/O is fast, no need for parallel throttler) - var atLeastOneBundleVerified bool - var errs []error - for _, bundle := range bundles { - _, err := VerifyNewBundle(ctx, co, artifactPolicyOption, bundle) - if err != nil { - // Log error and accumulate for final error message - errs = append(errs, err) - ui.Warnf(ctx, "Failed to verify bundle: %v", err) - continue - } - - dsse, ok := bundle.Content.(*protobundle.Bundle_DsseEnvelope) - if !ok { - err := fmt.Errorf("bundle does not contain a DSSE envelope") - errs = append(errs, err) - ui.Warnf(ctx, "%v", err) - continue - } - - payload, err := json.Marshal(dsse.DsseEnvelope) - if err != nil { - errs = append(errs, fmt.Errorf("marshaling DSSE envelope: %w", err)) - ui.Warnf(ctx, "Failed to marshal DSSE envelope: %v", err) - continue - } - - att, err := static.NewAttestation(payload) - if err != nil { - errs = append(errs, fmt.Errorf("creating attestation: %w", err)) - ui.Warnf(ctx, "Failed to create attestation: %v", err) - continue - } - - if co.ClaimVerifier != nil { - if err := co.ClaimVerifier(att, *hash, co.Annotations); err != nil { - errs = append(errs, fmt.Errorf("claim verification: %w", err)) - ui.Warnf(ctx, "Claim verification failed: %v", err) - continue - } - } - - checkedAttestations = append(checkedAttestations, att) - atLeastOneBundleVerified = true - } - - if len(checkedAttestations) == 0 { - return nil, false, &ErrNoMatchingAttestations{ - fmt.Errorf("no matching attestations: %w", errors.Join(errs...)), - } - } - - return checkedAttestations, atLeastOneBundleVerified, nil -} diff --git a/pkg/cosign/verify_oci_test.go b/pkg/cosign/verify_oci_test.go index afcf1ee7e06..78accc5f2a6 100644 --- a/pkg/cosign/verify_oci_test.go +++ b/pkg/cosign/verify_oci_test.go @@ -210,7 +210,6 @@ func TestVerifyImageAttestationsSigstoreBundle(t *testing.T) { // Attempt to verify non-existent attestation atts, bundleVerified, err := VerifyImageAttestations(context.Background(), ref, &CheckOpts{ TrustedMaterial: trustedRoot, - NewBundleFormat: true, Identities: []Identity{ { IssuerRegExp: ".*", @@ -235,7 +234,6 @@ func TestVerifyImageAttestationsSigstoreBundle(t *testing.T) { // Verify the attestation atts, bundleVerified, err = VerifyImageAttestations(context.Background(), ref, &CheckOpts{ TrustedMaterial: trustedRoot, - NewBundleFormat: true, Identities: []Identity{ { IssuerRegExp: ".*", @@ -250,7 +248,6 @@ func TestVerifyImageAttestationsSigstoreBundle(t *testing.T) { // Wrong identity should not verify atts, bundleVerified, err = VerifyImageAttestations(context.Background(), ref, &CheckOpts{ TrustedMaterial: trustedRoot, - NewBundleFormat: true, Identities: []Identity{ { IssuerRegExp: ".*", @@ -275,7 +272,6 @@ func TestVerifyImageAttestationsSigstoreBundle(t *testing.T) { // Verify the attestation atts, bundleVerified, err = VerifyImageAttestations(context.Background(), ref2, &CheckOpts{ TrustedMaterial: trustedRoot, - NewBundleFormat: true, Identities: []Identity{ { IssuerRegExp: ".*", diff --git a/pkg/cosign/verify_test.go b/pkg/cosign/verify_test.go index f08234acba8..f49bee0348f 100644 --- a/pkg/cosign/verify_test.go +++ b/pkg/cosign/verify_test.go @@ -16,16 +16,10 @@ package cosign import ( "bytes" - "context" "crypto" - "crypto/ecdsa" - "crypto/elliptic" "crypto/rand" - "crypto/rsa" "crypto/sha256" "crypto/x509" - "crypto/x509/pkix" - "encoding/asn1" "encoding/base64" "encoding/hex" "encoding/json" @@ -33,28 +27,19 @@ import ( "errors" "fmt" "io" - "math/big" - "net" - "net/url" "os" "path/filepath" "strings" "testing" "time" - "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" "github.com/digitorus/timestamp" - "github.com/go-openapi/strfmt" - "github.com/go-openapi/swag/conv" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/empty" ggcrlayout "github.com/google/go-containerregistry/pkg/v1/layout" gcrMutate "github.com/google/go-containerregistry/pkg/v1/mutate" "github.com/google/go-containerregistry/pkg/v1/random" "github.com/google/go-containerregistry/pkg/v1/stream" - "github.com/in-toto/in-toto-golang/in_toto" - "github.com/secure-systems-lab/go-securesystemslib/dsse" - "github.com/sigstore/cosign/v3/internal/pkg/cosign/rekor/mock" tsaMock "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa/mock" "github.com/sigstore/cosign/v3/internal/test" "github.com/sigstore/cosign/v3/pkg/cosign/bundle" @@ -63,20 +48,11 @@ import ( "github.com/sigstore/cosign/v3/pkg/oci/mutate" "github.com/sigstore/cosign/v3/pkg/oci/signed" "github.com/sigstore/cosign/v3/pkg/oci/static" - "github.com/sigstore/cosign/v3/pkg/types" - "github.com/sigstore/rekor/pkg/generated/client" - "github.com/sigstore/rekor/pkg/generated/client/entries" - "github.com/sigstore/rekor/pkg/generated/models" - rtypes "github.com/sigstore/rekor/pkg/types" - hashedrekord_v001 "github.com/sigstore/rekor/pkg/types/hashedrekord/v0.0.1" "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/options" - "github.com/sigstore/sigstore/pkg/tuf" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/transparency-dev/merkle/rfc6962" ) type mockVerifier struct { @@ -96,1565 +72,16 @@ func (m *mockVerifier) VerifySignature(signature, message io.Reader, opts ...sig var _ signature.Verifier = (*mockVerifier)(nil) -type mockAttestation struct { - payload interface{} -} - -var _ payloader = (*mockAttestation)(nil) - -func (m *mockAttestation) Annotations() (map[string]string, error) { - return nil, nil -} - -func (m *mockAttestation) Payload() ([]byte, error) { - return json.Marshal(m.payload) -} - -func (m *mockAttestation) Base64Signature() (string, error) { - b, err := json.Marshal(m.payload) - return string(b), err -} - -func appendSlices(slices [][]byte) []byte { - totalLen := 0 - for _, s := range slices { - totalLen += len(s) - } - tmp := make([]byte, 0, totalLen) - for _, s := range slices { - tmp = append(tmp, s...) - } - return tmp -} - -func Test_verifyOCIAttestation(t *testing.T) { - stmt, err := json.Marshal(in_toto.ProvenanceStatementSLSA02{}) - if err != nil { - t.Fatal(err) - } - valid := map[string]interface{}{ - "payloadType": types.IntotoPayloadType, - "payload": stmt, - "signatures": []dsse.Signature{{Sig: base64.StdEncoding.EncodeToString([]byte("foobar"))}}, - } - // Should Verify - if err := verifyOCIAttestation(context.TODO(), &mockVerifier{}, &mockAttestation{payload: valid}); err != nil { - t.Errorf("verifyOCIAttestation() error = %v", err) - } - - invalid := map[string]interface{}{ - "payloadType": "not valid type", - "payload": stmt, - "signatures": []dsse.Signature{{Sig: base64.StdEncoding.EncodeToString([]byte("foobar"))}}, - } - - // Should Not Verify - if err := verifyOCIAttestation(context.TODO(), &mockVerifier{}, &mockAttestation{payload: invalid}); err == nil { - t.Error("verifyOCIAttestation() expected invalid payload type error, got nil") - } - - if err := verifyOCIAttestation(context.TODO(), &mockVerifier{shouldErr: true}, &mockAttestation{payload: valid}); err == nil { - t.Error("verifyOCIAttestation() expected invalid payload type error, got nil") - } -} - -func TestVerifyImageSignature(t *testing.T) { - rootCert, rootKey, _ := test.GenerateRootCa() - subCert, subKey, _ := test.GenerateSubordinateCa(rootCert, rootKey) - leafCert, privKey, _ := test.GenerateLeafCert("subject@mail.com", "oidc-issuer", subCert, subKey) - pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) - pemSub := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert.Raw}) - pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - payload := []byte{1, 2, 3, 4} - h := sha256.Sum256(payload) - signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) - - ociSig, _ := static.NewSignature(payload, - base64.StdEncoding.EncodeToString(signature), - static.WithCertChain(pemLeaf, appendSlices([][]byte{pemSub, pemRoot}))) - verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, - &CheckOpts{ - RootCerts: rootPool, - IgnoreSCT: true, - IgnoreTlog: true, - Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}}) - if err != nil { - t.Fatalf("unexpected error while verifying signature, expected no error, got %v", err) - } - // TODO: Create fake bundle and test verification - if verified == true { - t.Fatalf("expected verified=false, got verified=true") - } -} - -func TestVerifyImageSignatureMultipleSubs(t *testing.T) { - rootCert, rootKey, _ := test.GenerateRootCa() - subCert1, subKey1, _ := test.GenerateSubordinateCa(rootCert, rootKey) - subCert2, subKey2, _ := test.GenerateSubordinateCa(subCert1, subKey1) - subCert3, subKey3, _ := test.GenerateSubordinateCa(subCert2, subKey2) - leafCert, privKey, _ := test.GenerateLeafCert("subject@mail.com", "oidc-issuer", subCert3, subKey3) - pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) - pemSub1 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert1.Raw}) - pemSub2 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert2.Raw}) - pemSub3 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert3.Raw}) - pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - payload := []byte{1, 2, 3, 4} - h := sha256.Sum256(payload) - signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) - - ociSig, _ := static.NewSignature(payload, - base64.StdEncoding.EncodeToString(signature), static.WithCertChain(pemLeaf, appendSlices([][]byte{pemSub3, pemSub2, pemSub1, pemRoot}))) - verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, &CheckOpts{ - RootCerts: rootPool, - IgnoreSCT: true, IgnoreTlog: true, - Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}}) - if err != nil { - t.Fatalf("unexpected error while verifying signature, expected no error, got %v", err) - } - // TODO: Create fake bundle and test verification - if verified == true { - t.Fatalf("expected verified=false, got verified=true") - } -} - -func signEntry(ctx context.Context, t *testing.T, signer signature.Signer, entry bundle.RekorPayload) []byte { - payload, err := json.Marshal(entry) - if err != nil { - t.Fatalf("marshalling error: %v", err) - } - canonicalized, err := jsoncanonicalizer.Transform(payload) - if err != nil { - t.Fatalf("canonicalizing error: %v", err) - } - signature, err := signer.SignMessage(bytes.NewReader(canonicalized), options.WithContext(ctx)) - if err != nil { - t.Fatalf("signing error: %v", err) - } - return signature -} - -func CreateTestBundle(ctx context.Context, t *testing.T, rekor signature.Signer, leaf []byte) *bundle.RekorBundle { - // generate log ID according to rekor public key - pk, _ := rekor.PublicKey(nil) - keyID, _ := GetTransparencyLogID(pk) - pyld := bundle.RekorPayload{ - Body: base64.StdEncoding.EncodeToString(leaf), - IntegratedTime: time.Now().Unix(), - LogIndex: 693591, - LogID: keyID, - } - // Sign with root. - signature := signEntry(ctx, t, rekor, pyld) - b := &bundle.RekorBundle{ - SignedEntryTimestamp: strfmt.Base64(signature), - Payload: pyld, - } - return b -} - -func Test_verifySignaturesErrNoSignaturesFound(t *testing.T) { - _, _, err := verifySignatures(context.Background(), &fakeOCISignatures{}, v1.Hash{}, nil) - var e *ErrNoSignaturesFound - if !errors.As(err, &e) { - t.Fatalf("%T{%q} is not a %T", err, err, &ErrNoSignaturesFound{}) - } -} - -func Test_verifySignaturesErrNoMatchingSignatures(t *testing.T) { - rootCert, rootKey, _ := test.GenerateRootCa() - subCert, subKey, _ := test.GenerateSubordinateCa(rootCert, rootKey) - leafCert, privKey, _ := test.GenerateLeafCert("subject@mail.com", "oidc-issuer", subCert, subKey) - pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) - pemSub := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert.Raw}) - pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - payload := []byte{1, 2, 3, 4} - h := sha256.Sum256(payload) - signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) - - ociSig, _ := static.NewSignature(payload, - base64.StdEncoding.EncodeToString(signature), - static.WithCertChain(pemLeaf, appendSlices([][]byte{pemSub, pemRoot}))) - _, _, err := verifySignatures(context.Background(), &fakeOCISignatures{signatures: []oci.Signature{ociSig}}, v1.Hash{}, &CheckOpts{ - RootCerts: rootPool, - IgnoreSCT: true, - IgnoreTlog: true, - Identities: []Identity{{Subject: "another-subject@mail.com", Issuer: "oidc-issuer"}}}) - - var e *ErrNoMatchingSignatures - if !errors.As(err, &e) { - t.Fatalf("%T{%q} is not a %T", err, err, &ErrNoMatchingSignatures{}) - } -} - -func TestVerifyImageSignatureWithNoChain(t *testing.T) { - ctx := context.Background() - rootCert, rootKey, _ := test.GenerateRootCa() - sv, _, err := signature.NewECDSASignerVerifier(elliptic.P256(), rand.Reader, crypto.SHA256) - if err != nil { - t.Fatalf("creating signer: %v", err) - } - - leafCert, privKey, _ := test.GenerateLeafCert("subject@mail.com", "oidc-issuer", rootCert, rootKey) - pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - payload := []byte{1, 2, 3, 4} - h := sha256.Sum256(payload) - signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) - - // Create a fake bundle - pe, _ := proposedEntries(base64.StdEncoding.EncodeToString(signature), payload, pemLeaf) - entry, _ := rtypes.UnmarshalEntry(pe[0]) - leaf, _ := entry.Canonicalize(ctx) - rekorBundle := CreateTestBundle(ctx, t, sv, leaf) - pemBytes, _ := cryptoutils.MarshalPublicKeyToPEM(sv.Public()) - rekorPubKeys := NewTrustedTransparencyLogPubKeys() - rekorPubKeys.AddTransparencyLogPubKey(pemBytes, tuf.Active) - - opts := []static.Option{static.WithCertChain(pemLeaf, []byte{}), static.WithBundle(rekorBundle)} - ociSig, _ := static.NewSignature(payload, base64.StdEncoding.EncodeToString(signature), opts...) - - verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, - &CheckOpts{ - RootCerts: rootPool, - IgnoreSCT: true, - Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}, - RekorPubKeys: &rekorPubKeys}) - if err != nil { - t.Fatalf("unexpected error %v", err) - } - if verified == false { - t.Fatalf("expected verified=true, got verified=false") - } -} - -func TestVerifyImageSignatureWithKeyAndCert(t *testing.T) { - ctx := context.Background() - rootCert, rootKey, _ := test.GenerateRootCa() - sv, _, err := signature.NewECDSASignerVerifier(elliptic.P256(), rand.Reader, crypto.SHA256) - if err != nil { - t.Fatalf("creating signer: %v", err) - } - - leafCert, privKey, _ := test.GenerateLeafCert("subject@mail.com", "oidc-issuer", rootCert, rootKey) - pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - payload := []byte{1, 2, 3, 4} - h := sha256.Sum256(payload) - sig, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) - - // Create a fake bundle - pe, _ := proposedEntries(base64.StdEncoding.EncodeToString(sig), payload, pemLeaf) - entry, _ := rtypes.UnmarshalEntry(pe[0]) - leaf, _ := entry.Canonicalize(ctx) - rekorBundle := CreateTestBundle(ctx, t, sv, leaf) - pemBytes, _ := cryptoutils.MarshalPublicKeyToPEM(sv.Public()) - rekorPubKeys := NewTrustedTransparencyLogPubKeys() - rekorPubKeys.AddTransparencyLogPubKey(pemBytes, tuf.Active) - - opts := []static.Option{static.WithCertChain(pemLeaf, []byte{}), static.WithBundle(rekorBundle)} - ociSig, _ := static.NewSignature(payload, base64.StdEncoding.EncodeToString(sig), opts...) - - leafSV, err := signature.LoadECDSASignerVerifier(privKey, crypto.SHA256) - if err != nil { - t.Fatal(err) - } - - verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, - &CheckOpts{ - SigVerifier: leafSV, - RootCerts: rootPool, - IgnoreSCT: true, - Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}, - RekorPubKeys: &rekorPubKeys}) - if err != nil { - t.Fatalf("unexpected error %v", err) - } - if verified == false { - t.Fatalf("expected verified=true, got verified=false") - } -} - -func TestVerifyImageSignatureWithInvalidPublicKeyType(t *testing.T) { - ctx := context.Background() - rootCert, rootKey, _ := test.GenerateRootCa() - sv, _, err := signature.NewECDSASignerVerifier(elliptic.P256(), rand.Reader, crypto.SHA256) - if err != nil { - t.Fatalf("creating signer: %v", err) - } - - leafCert, privKey, _ := test.GenerateLeafCert("subject@mail.com", "oidc-issuer", rootCert, rootKey) - pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - payload := []byte{1, 2, 3, 4} - h := sha256.Sum256(payload) - signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) - - // Create a fake bundle - pe, _ := proposedEntries(base64.StdEncoding.EncodeToString(signature), payload, pemLeaf) - entry, _ := rtypes.UnmarshalEntry(pe[0]) - leaf, _ := entry.Canonicalize(ctx) - rekorBundle := CreateTestBundle(ctx, t, sv, leaf) - pemBytes, _ := cryptoutils.MarshalPublicKeyToPEM(sv.Public()) - rekorPubKeys := NewTrustedTransparencyLogPubKeys() - // Add one valid key here. - rekorPubKeys.AddTransparencyLogPubKey(pemBytes, tuf.Active) - - opts := []static.Option{static.WithCertChain(pemLeaf, []byte{}), static.WithBundle(rekorBundle)} - ociSig, _ := static.NewSignature(payload, base64.StdEncoding.EncodeToString(signature), opts...) - - // Then try to validate with keys that are not ecdsa.PublicKey and should - // fail. - var rsaPrivKey crypto.PrivateKey - rsaPrivKey, err = rsa.GenerateKey(rand.Reader, 4096) - if err != nil { - t.Fatalf("Unable to create RSA test key: %v", err) - } - var signer crypto.Signer - var ok bool - if signer, ok = rsaPrivKey.(crypto.Signer); !ok { - t.Fatalf("Unable to create signer out of RSA test key: %v", err) - } - rsaPEM, err := cryptoutils.MarshalPublicKeyToPEM(signer.Public()) - if err != nil { - t.Fatalf("Unable to marshal RSA test key: %v", err) - } - if err = rekorPubKeys.AddTransparencyLogPubKey(rsaPEM, tuf.Active); err != nil { - t.Fatalf("failed to add RSA key to transparency log public keys: %v", err) - } - verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, - &CheckOpts{ - RootCerts: rootPool, - IgnoreSCT: true, - Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}, - RekorPubKeys: &rekorPubKeys}) - if err == nil { - t.Fatal("expected error got none") - } - if !strings.Contains(err.Error(), "is not type ecdsa.PublicKey") { - t.Errorf("did not get expected failure message, wanted 'is not type ecdsa.PublicKey' got: %v", err) - } - if verified == true { - t.Fatalf("expected verified=false, got verified=true") - } -} - -func TestVerifyImageSignatureWithInvalidBundleBodyType(t *testing.T) { - ctx := context.Background() - rootCert, rootKey, _ := test.GenerateRootCa() - sv, _, err := signature.NewECDSASignerVerifier(elliptic.P256(), rand.Reader, crypto.SHA256) - if err != nil { - t.Fatalf("creating signer: %v", err) - } - - leafCert, privKey, _ := test.GenerateLeafCert("subject@mail.com", "oidc-issuer", rootCert, rootKey) - pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - payload := []byte{1, 2, 3, 4} - h := sha256.Sum256(payload) - signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) - - // Create a fake bundle - pe, _ := proposedEntries(base64.StdEncoding.EncodeToString(signature), payload, pemLeaf) - entry, _ := rtypes.UnmarshalEntry(pe[0]) - leaf, _ := entry.Canonicalize(ctx) - rekorBundle := CreateTestBundle(ctx, t, sv, leaf) - // Set Body to an invalid type - rekorBundle.Payload.Body = 12345 - - pemBytes, _ := cryptoutils.MarshalPublicKeyToPEM(sv.Public()) - rekorPubKeys := NewTrustedTransparencyLogPubKeys() - rekorPubKeys.AddTransparencyLogPubKey(pemBytes, tuf.Active) - - opts := []static.Option{static.WithCertChain(pemLeaf, []byte{}), static.WithBundle(rekorBundle)} - ociSig, _ := static.NewSignature(payload, base64.StdEncoding.EncodeToString(signature), opts...) - - _, err = VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, - &CheckOpts{ - RootCerts: rootPool, - IgnoreSCT: true, - Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}, - RekorPubKeys: &rekorPubKeys}) - if err == nil { - t.Fatal("expected error got none") - } - if !strings.Contains(err.Error(), "bundle payload body is not a string") { - t.Errorf("did not get expected failure message, wanted 'bundle payload body is not a string' got: %v", err) - } -} - -func TestVerifyImageSignatureWithOnlyRoot(t *testing.T) { - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, privKey, _ := test.GenerateLeafCert("subject@mail.com", "oidc-issuer", rootCert, rootKey) - pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) - pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - payload := []byte{1, 2, 3, 4} - h := sha256.Sum256(payload) - signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) - - ociSig, _ := static.NewSignature(payload, base64.StdEncoding.EncodeToString(signature), static.WithCertChain(pemLeaf, pemRoot)) - verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, - &CheckOpts{ - RootCerts: rootPool, - IgnoreSCT: true, - Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}, - IgnoreTlog: true}) - if err != nil { - t.Fatalf("unexpected error while verifying signature, expected no error, got %v", err) - } - // TODO: Create fake bundle and test verification - if verified == true { - t.Fatalf("expected verified=false, got verified=true") - } -} - -func TestVerifyImageSignatureWithMissingSub(t *testing.T) { - rootCert, rootKey, _ := test.GenerateRootCa() - subCert, subKey, _ := test.GenerateSubordinateCa(rootCert, rootKey) - leafCert, privKey, _ := test.GenerateLeafCert("subject@mail.com", "oidc-issuer", subCert, subKey) - pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) - pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - payload := []byte{1, 2, 3, 4} - h := sha256.Sum256(payload) - signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) - - ociSig, _ := static.NewSignature(payload, base64.StdEncoding.EncodeToString(signature), static.WithCertChain(pemLeaf, pemRoot)) - verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, - &CheckOpts{ - RootCerts: rootPool, - IgnoreSCT: true, - Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}, - IgnoreTlog: true}) - if err == nil { - t.Fatal("expected error while verifying signature") - } - if !strings.Contains(err.Error(), "certificate signed by unknown authority") { - t.Fatal("expected error while verifying signature") - } - // TODO: Create fake bundle and test verification - if verified == true { - t.Fatalf("expected verified=false, got verified=true") - } -} - -func TestVerifyImageSignatureWithExistingSub(t *testing.T) { - rootCert, rootKey, _ := test.GenerateRootCa() - subCert, subKey, _ := test.GenerateSubordinateCa(rootCert, rootKey) - leafCert, privKey, _ := test.GenerateLeafCert("subject@mail.com", "oidc-issuer", subCert, subKey) - pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) - pemSub := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert.Raw}) - pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) - - otherSubCert, _, _ := test.GenerateSubordinateCa(rootCert, rootKey) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - subPool := x509.NewCertPool() - // Load in different sub cert so the chain doesn't verify - rootPool.AddCert(otherSubCert) - - payload := []byte{1, 2, 3, 4} - h := sha256.Sum256(payload) - signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) - - ociSig, _ := static.NewSignature(payload, - base64.StdEncoding.EncodeToString(signature), - static.WithCertChain(pemLeaf, appendSlices([][]byte{pemSub, pemRoot}))) - verified, err := VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, - &CheckOpts{ - RootCerts: rootPool, - IntermediateCerts: subPool, - IgnoreSCT: true, - Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}, - IgnoreTlog: true}) - if err == nil { - t.Fatal("expected error while verifying signature") - } - if !strings.Contains(err.Error(), "certificate signed by unknown authority") { - t.Fatal("expected error while verifying signature") - } - // TODO: Create fake bundle and test verification - if verified == true { - t.Fatalf("expected verified=false, got verified=true") - } -} - -var ( - lea = models.LogEntryAnon{ - Attestation: &models.LogEntryAnonAttestation{}, - Body: base64.StdEncoding.EncodeToString([]byte("asdf")), - IntegratedTime: new(int64), - LogID: new(string), - LogIndex: new(int64), - Verification: &models.LogEntryAnonVerification{ - InclusionProof: &models.InclusionProof{ - RootHash: new(string), - TreeSize: new(int64), - LogIndex: new(int64), - }, - }, - } - data = models.LogEntry{ - uuid(lea): lea, - } -) - -// uuid generates the UUID for the given LogEntry. -// This is effectively a reimplementation of -// pkg/cosign/tlog.go -> verifyUUID / ComputeLeafHash, but separated -// to avoid a circular dependency. -// TODO?: Perhaps we should refactor the tlog libraries into a separate -// package? -func uuid(e models.LogEntryAnon) string { - entryBytes, err := base64.StdEncoding.DecodeString(e.Body.(string)) - if err != nil { - panic(err) - } - return hex.EncodeToString(rfc6962.DefaultHasher.HashLeaf(entryBytes)) -} - -func TestImageSignatureVerificationWithRekor(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - - // Generate ECDSA signer and public key for signing the blob. - signer, publicKey := generateSigner(t) - blob, blobSignature, blobSignatureBase64 := generateBlobSignature(t, signer) - - // Create an OCI signature which will be verified. - ociSignature, err := static.NewSignature(blob, blobSignatureBase64) - require.NoError(t, err, "error creating OCI signature") - - // Set up mock Rekor signer and log ID. - rekorSigner, rekorPublicKey := generateSigner(t) - logID := calculateLogID(t, rekorPublicKey) - - // Create a mock Rekor log entry to simulate Rekor behavior. - rekorEntry := createRekorEntry(ctx, t, logID, rekorSigner, blob, blobSignature, publicKey) - - // Mock Rekor client to return the mock log entry for verification. - mockClient := &client.Rekor{ - Entries: &mockEntriesClient{ - searchLogQueryFunc: func(_ *entries.SearchLogQueryParams, _ ...entries.ClientOption) (*entries.SearchLogQueryOK, error) { - return &entries.SearchLogQueryOK{ - Payload: []models.LogEntry{*rekorEntry}, - }, nil - }, - }, - } - - // Define trusted Rekor public keys for verification. - trustedRekorPubKeys := &TrustedTransparencyLogPubKeys{ - Keys: map[string]TransparencyLogPubKey{ - logID: { - PubKey: rekorPublicKey, - Status: tuf.Active, - }, - }, - } - - // Generate non-matching public key for failure test cases. - _, nonMatchingPublicKey := generateSigner(t) - nonMatchingRekorPubKeys := &TrustedTransparencyLogPubKeys{ - Keys: map[string]TransparencyLogPubKey{ - logID: { - PubKey: nonMatchingPublicKey, - Status: tuf.Active, - }, - }, - } - - tests := []struct { - name string - checkOpts CheckOpts - rekorClient *client.Rekor - expectError bool - errorMsg string - }{ - { - name: "Verification succeeds with valid Rekor public keys", - checkOpts: CheckOpts{ - SigVerifier: signer, - RekorClient: mockClient, - RekorPubKeys: trustedRekorPubKeys, - Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}, - }, - rekorClient: mockClient, - expectError: false, - }, - { - name: "Verification fails with no Rekor public keys", - checkOpts: CheckOpts{ - SigVerifier: signer, - RekorClient: mockClient, - Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}, - }, - rekorClient: mockClient, - expectError: true, - errorMsg: "no valid tlog entries found no trusted rekor public keys provided", - }, - { - name: "Verification fails with non-matching Rekor public keys", - checkOpts: CheckOpts{ - SigVerifier: signer, - RekorClient: mockClient, - RekorPubKeys: nonMatchingRekorPubKeys, - Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}, - }, - rekorClient: mockClient, - expectError: true, - errorMsg: "verifying signedEntryTimestamp: unable to verify SET", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - bundleVerified, err := VerifyImageSignature(ctx, ociSignature, v1.Hash{}, &tt.checkOpts) - if tt.expectError { - assert.Error(t, err) - assert.Contains(t, err.Error(), tt.errorMsg) - } else { - assert.NoError(t, err) - assert.True(t, bundleVerified, "bundle verification failed") - } - }) - } -} - -func TestVerifyImageSignatureWithSigVerifierAndTSA(t *testing.T) { - client, err := tsaMock.NewTSAClient((tsaMock.TSAClientOptions{Time: time.Now()})) - if err != nil { - t.Fatal(err) - } - - sv, _, err := signature.NewDefaultECDSASignerVerifier() - if err != nil { - t.Fatalf("error generating verifier: %v", err) - } - certChainPEM, err := cryptoutils.MarshalCertificatesToPEM(client.CertChain) - if err != nil { - t.Fatalf("unexpected error marshalling cert chain: %v", err) - } - - leaves, intermediates, roots, err := splitPEMCertificateChain(certChainPEM) - if err != nil { - t.Fatal("error splitting response into certificate chain") - } - - payload := []byte{1, 2, 3, 4} - sigBytes, err := sv.SignMessage(bytes.NewReader(payload)) - if err != nil { - t.Fatalf("error signing the payload: %v", err) - } - - client.Message = sigBytes - timestampResponse, err := client.GetTimestampResponse(nil) - if err != nil { - t.Fatalf("error getting timestamp response: %v", err) - } - - b64sig := base64.StdEncoding.EncodeToString(sigBytes) - sig, err := static.NewSignature(payload, b64sig, static.WithRFC3161Timestamp(bundle.TimestampToRFC3161Timestamp(timestampResponse))) - if err != nil { - t.Fatalf("error creating oci signature: %v", err) - } - if bundleVerified, err := VerifyImageSignature(context.TODO(), sig, v1.Hash{}, &CheckOpts{ - SigVerifier: sv, - TSACertificate: leaves[0], - TSAIntermediateCertificates: intermediates, - TSARootCertificates: roots, - IgnoreTlog: true, - }); err != nil || bundleVerified { // bundle is not verified since there's no Rekor bundle - t.Fatalf("unexpected error while verifying signature, got %v", err) - } -} - -func TestVerifyImageSignatureWithSigVerifierAndRekorTSA(t *testing.T) { - // Add a fake rekor client - this makes it look like there's a matching - // tlog entry for the signature during validation (even though it does not - // match the underlying data / key) - mClient := new(client.Rekor) - mClient.Entries = &mock.EntriesClient{ - Entries: []*models.LogEntry{&data}, - } - - client, err := tsaMock.NewTSAClient((tsaMock.TSAClientOptions{Time: time.Now()})) - if err != nil { - t.Fatal(err) - } - sv, _, err := signature.NewDefaultECDSASignerVerifier() - if err != nil { - t.Fatalf("error generating verifier: %v", err) - } - certChainPEM, err := cryptoutils.MarshalCertificatesToPEM(client.CertChain) - if err != nil { - t.Fatalf("unexpected error marshalling cert chain: %v", err) - } - - leaves, intermediates, roots, err := splitPEMCertificateChain(certChainPEM) - if err != nil { - t.Fatal("error splitting response into certificate chain") - } - - payload := []byte{1, 2, 3, 4} - sigBytes, err := sv.SignMessage(bytes.NewReader(payload)) - if err != nil { - t.Fatalf("error signing the payload: %v", err) - } - - client.Message = sigBytes - timestampResponse, err := client.GetTimestampResponse(nil) - if err != nil { - t.Fatalf("error getting timestamp response: %v", err) - } - - b64sig := base64.StdEncoding.EncodeToString(sigBytes) - sig, err := static.NewSignature(payload, b64sig, static.WithRFC3161Timestamp(bundle.TimestampToRFC3161Timestamp(timestampResponse))) - if err != nil { - t.Fatalf("error creating oci signature: %v", err) - } - if _, err := VerifyImageSignature(context.TODO(), sig, v1.Hash{}, &CheckOpts{ - SigVerifier: sv, - TSACertificate: leaves[0], - TSAIntermediateCertificates: intermediates, - TSARootCertificates: roots, - RekorClient: mClient, - }); err == nil || !strings.Contains(err.Error(), "no trusted rekor public keys provided") { - // TODO(wlynch): This is a weak test, since this is really failing because - // there is no inclusion proof for the Rekor entry rather than failing to - // validate the Rekor public key itself. At the very least this ensures - // that we're hitting tlog validation during signature checking, - // but we should look into improving this once there is an in-memory - // Rekor client that is capable of performing inclusion proof validation - // in unit tests. - t.Fatalf("expected error while verifying signature, got %s", err) - } -} - -func TestVerifyImageSignatureWithMismatchedBundleAndTrustedRoot(t *testing.T) { - ctx := context.Background() - var ca root.FulcioCertificateAuthority - rootCert, rootKey, _ := test.GenerateRootCa() - ca.Root = rootCert - sv, _, err := signature.NewECDSASignerVerifier(elliptic.P256(), rand.Reader, crypto.SHA256) - if err != nil { - t.Fatalf("creating signer: %v", err) - } - - leafCert, privKey, _ := test.GenerateLeafCert("subject@mail.com", "oidc-issuer", rootCert, rootKey) - pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - payload := []byte{1, 2, 3, 4} - h := sha256.Sum256(payload) - signature1, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) - - // Create a fake bundle - pe, _ := proposedEntries(base64.StdEncoding.EncodeToString(signature1), payload, pemLeaf) - entry, _ := rtypes.UnmarshalEntry(pe[0]) - leaf, _ := entry.Canonicalize(ctx) - rekorBundle := CreateTestBundle(ctx, t, sv, leaf) - pemBytes, _ := cryptoutils.MarshalPublicKeyToPEM(sv.Public()) - rekorPubKeys := NewTrustedTransparencyLogPubKeys() - rekorPubKeys.AddTransparencyLogPubKey(pemBytes, tuf.Active) - - tlogs := make(map[string]*root.TransparencyLog) - for k, v := range rekorPubKeys.Keys { - tlogs[k] = &root.TransparencyLog{PublicKey: v.PubKey, HashFunc: crypto.SHA256, ValidityPeriodStart: time.Now().Add(-1 * time.Minute)} - } - - trustedRoot, err := root.NewTrustedRoot(root.TrustedRootMediaType01, []root.CertificateAuthority{&ca}, nil, nil, tlogs) - if err != nil { - t.Fatal(err) - } - - // Create a different bundle for a different signature - signature2, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) - pe2, _ := proposedEntries(base64.StdEncoding.EncodeToString(signature2), payload, pemLeaf) - entry2, _ := rtypes.UnmarshalEntry(pe2[0]) - leaf2, _ := entry2.Canonicalize(ctx) - rekorBundle2 := CreateTestBundle(ctx, t, sv, leaf2) - - opts := []static.Option{static.WithCertChain(pemLeaf, []byte{}), static.WithBundle(rekorBundle2)} - // Create a signed entity for the original signature but with the wrong bundle for that signature - ociSig, _ := static.NewSignature(payload, base64.StdEncoding.EncodeToString(signature1), opts...) - - _, err = VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, - &CheckOpts{ - RootCerts: rootPool, - IgnoreSCT: true, - Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}, - TrustedMaterial: trustedRoot}) - if err == nil || !strings.Contains(err.Error(), "signature in bundle does not match signature being verified") { - t.Fatalf("expected error for mismatched signature and bundle, got %v", err) - } - - // Create a signed entity with a different key from the bundle - leafCert2, _, _ := test.GenerateLeafCert("subject@mail.com", "oidc-issuer", rootCert, rootKey) - pemLeaf2 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert2.Raw}) - - opts = []static.Option{static.WithCertChain(pemLeaf2, []byte{}), static.WithBundle(rekorBundle)} - ociSig, _ = static.NewSignature(payload, base64.StdEncoding.EncodeToString(signature1), opts...) - - _, err = VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, - &CheckOpts{ - RootCerts: rootPool, - IgnoreSCT: true, - Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}, - TrustedMaterial: trustedRoot}) - if err == nil || !strings.Contains(err.Error(), "error verifying bundle: comparing public key PEMs") { - t.Fatal(err) - } -} - -func TestValidateAndUnpackCertSuccess(t *testing.T) { - subject := "email@email" - oidcIssuer := "https://accounts.google.com" - - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - co := &CheckOpts{ - RootCerts: rootPool, - IgnoreSCT: true, - Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, - } - - _, err := ValidateAndUnpackCert(leafCert, co) - if err != nil { - t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) - } - err = CheckCertificatePolicy(leafCert, co) - if err != nil { - t.Errorf("CheckCertificatePolicy expected no error, got err = %v", err) - } -} - -func TestValidateAndUnpackCertSuccessAllowAllValues(t *testing.T) { - subject := "email@email" - oidcIssuer := "https://accounts.google.com" - - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - co := &CheckOpts{ - RootCerts: rootPool, - IgnoreSCT: true, - Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, - } - - _, err := ValidateAndUnpackCert(leafCert, co) - if err != nil { - t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) - } - err = CheckCertificatePolicy(leafCert, co) - if err != nil { - t.Errorf("CheckCertificatePolicy expected no error, got err = %v", err) - } -} - -func TestValidateAndUnpackCertWithoutRequiredSCT(t *testing.T) { - subject := "email@email" - oidcIssuer := "https://accounts.google.com" - - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - co := &CheckOpts{ - RootCerts: rootPool, - Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, - // explicitly set to false - IgnoreSCT: false, - } - - _, err := ValidateAndUnpackCert(leafCert, co) - require.Contains(t, err.Error(), "certificate does not include required embedded SCT") -} - -func TestValidateAndUnpackCertSuccessWithDnsSan(t *testing.T) { - subject := "example.com" - oidcIssuer := "https://accounts.google.com" - - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, _, _ := test.GenerateLeafCertWithSubjectAlternateNames( - []string{subject}, /* dnsNames */ - nil, /* emailAddresses */ - nil, /* ipAddresses */ - nil, /* uris */ - oidcIssuer, /* oidcIssuer */ - rootCert, - rootKey) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - co := &CheckOpts{ - RootCerts: rootPool, - Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, - IgnoreSCT: true, - } - - _, err := ValidateAndUnpackCert(leafCert, co) - if err != nil { - t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) - } - err = CheckCertificatePolicy(leafCert, co) - if err != nil { - t.Errorf("CheckCertificatePolicy expected no error, got err = %v", err) - } -} - -func TestValidateAndUnpackCertSuccessWithEmailSan(t *testing.T) { - subject := "email@email" - oidcIssuer := "https://accounts.google.com" - - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, _, _ := test.GenerateLeafCertWithSubjectAlternateNames( - nil, /* dnsNames */ - []string{subject}, /* emailAddresses */ - nil, /* ipAddresses */ - nil, /* uris */ - oidcIssuer, /* oidcIssuer */ - rootCert, - rootKey) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - co := &CheckOpts{ - RootCerts: rootPool, - Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, - IgnoreSCT: true, - } - - _, err := ValidateAndUnpackCert(leafCert, co) - if err != nil { - t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) - } - err = CheckCertificatePolicy(leafCert, co) - if err != nil { - t.Errorf("CheckCertificatePolicy expected no error, got err = %v", err) - } -} - -func TestValidateAndUnpackCertSuccessWithIpAddressSan(t *testing.T) { - subject := "127.0.0.1" - oidcIssuer := "https://accounts.google.com" - - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, _, _ := test.GenerateLeafCertWithSubjectAlternateNames( - nil, /* dnsNames */ - nil, /* emailAddresses */ - []net.IP{net.ParseIP(subject)}, /* ipAddresses */ - nil, /* uris */ - oidcIssuer, /* oidcIssuer */ - rootCert, - rootKey) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - co := &CheckOpts{ - RootCerts: rootPool, - Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, - IgnoreSCT: true, - } - - _, err := ValidateAndUnpackCert(leafCert, co) - if err != nil { - t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) - } - err = CheckCertificatePolicy(leafCert, co) - if err != nil { - t.Errorf("CheckCertificatePolicy expected no error, got err = %v", err) - } -} - -func TestValidateAndUnpackCertSuccessWithUriSan(t *testing.T) { - subject, _ := url.Parse("scheme://userinfo@host") - oidcIssuer := "https://accounts.google.com" - - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, _, _ := test.GenerateLeafCertWithSubjectAlternateNames( - nil, /* dnsNames */ - nil, /* emailAddresses */ - nil, /* ipAddresses */ - []*url.URL{subject}, /* uris */ - oidcIssuer, /* oidcIssuer */ - rootCert, - rootKey) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - co := &CheckOpts{ - RootCerts: rootPool, - Identities: []Identity{{Subject: "scheme://userinfo@host", Issuer: oidcIssuer}}, - IgnoreSCT: true, - } - - _, err := ValidateAndUnpackCert(leafCert, co) - if err != nil { - t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) - } - err = CheckCertificatePolicy(leafCert, co) - if err != nil { - t.Errorf("CheckCertificatePolicy expected no error, got err = %v", err) - } -} - -func TestValidateAndUnpackCertSuccessWithOtherNameSan(t *testing.T) { - // generate with OtherName, which will override other SANs - subject := "subject-othername" - ext, err := cryptoutils.MarshalOtherNameSAN(subject, true) - if err != nil { - t.Fatalf("error marshalling SANs: %v", err) - } - exts := []pkix.Extension{*ext} - - oidcIssuer := "https://accounts.google.com" - - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, _, _ := test.GenerateLeafCert("unused", oidcIssuer, rootCert, rootKey, exts...) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - co := &CheckOpts{ - RootCerts: rootPool, - Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, - IgnoreSCT: true, - } - - _, err = ValidateAndUnpackCert(leafCert, co) - if err != nil { - t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) - } - err = CheckCertificatePolicy(leafCert, co) - if err != nil { - t.Errorf("CheckCertificatePolicy expected no error, got err = %v", err) - } -} - -func TestValidateAndUnpackCertInvalidRoot(t *testing.T) { - subject := "email@email" - oidcIssuer := "https://accounts.google.com" - - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey) - - otherRoot, _, _ := test.GenerateRootCa() - - rootPool := x509.NewCertPool() - rootPool.AddCert(otherRoot) - - co := &CheckOpts{ - RootCerts: rootPool, - Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, - IgnoreSCT: true, - } - - _, err := ValidateAndUnpackCert(leafCert, co) - require.Contains(t, err.Error(), "certificate signed by unknown authority") -} - -func TestValidateAndUnpackCertInvalidOidcIssuer(t *testing.T) { - subject := "email@email" - oidcIssuer := "https://accounts.google.com" - - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - co := &CheckOpts{ - RootCerts: rootPool, - Identities: []Identity{{Subject: subject, Issuer: "other"}}, - IgnoreSCT: true, - } - - _, err := ValidateAndUnpackCert(leafCert, co) - require.Contains(t, err.Error(), "none of the expected identities matched what was in the certificate") - err = CheckCertificatePolicy(leafCert, co) - require.Contains(t, err.Error(), "none of the expected identities matched what was in the certificate") -} - -func TestValidateAndUnpackCertInvalidEmail(t *testing.T) { - subject := "email@email" - oidcIssuer := "https://accounts.google.com" - - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - co := &CheckOpts{ - RootCerts: rootPool, - Identities: []Identity{{Subject: "other", Issuer: oidcIssuer}}, - IgnoreSCT: true, - } - - _, err := ValidateAndUnpackCert(leafCert, co) - require.Contains(t, err.Error(), "none of the expected identities matched what was in the certificate") - err = CheckCertificatePolicy(leafCert, co) - require.Contains(t, err.Error(), "none of the expected identities matched what was in the certificate") -} - -func TestValidateAndUnpackCertInvalidGithubWorkflowTrigger(t *testing.T) { - subject := "email@email" - oidcIssuer := "https://accounts.google.com" - githubWorkFlowTrigger := "myTrigger" - - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, _, _ := test.GenerateLeafCertWithGitHubOIDs(subject, oidcIssuer, githubWorkFlowTrigger, "", "", "", "", rootCert, rootKey) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - co := &CheckOpts{ - RootCerts: rootPool, - Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, - CertGithubWorkflowTrigger: "otherTrigger", - IgnoreSCT: true, - } - - _, err := ValidateAndUnpackCert(leafCert, co) - require.Contains(t, err.Error(), "expected GitHub Workflow Trigger not found in certificate") - err = CheckCertificatePolicy(leafCert, co) - require.Contains(t, err.Error(), "expected GitHub Workflow Trigger not found in certificate") -} - -func TestValidateAndUnpackCertInvalidGithubWorkflowSHA(t *testing.T) { - subject := "email@email" - oidcIssuer := "https://accounts.google.com" - githubWorkFlowSha := "mySHA" - - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, _, _ := test.GenerateLeafCertWithGitHubOIDs(subject, oidcIssuer, "", githubWorkFlowSha, "", "", "", rootCert, rootKey) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - co := &CheckOpts{ - RootCerts: rootPool, - Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, - CertGithubWorkflowSha: "otherSHA", - IgnoreSCT: true, - } - - _, err := ValidateAndUnpackCert(leafCert, co) - require.Contains(t, err.Error(), "expected GitHub Workflow SHA not found in certificate") - err = CheckCertificatePolicy(leafCert, co) - require.Contains(t, err.Error(), "expected GitHub Workflow SHA not found in certificate") -} - -func TestValidateAndUnpackCertInvalidGithubWorkflowName(t *testing.T) { - subject := "email@email" - oidcIssuer := "https://accounts.google.com" - githubWorkFlowName := "myName" - - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, _, _ := test.GenerateLeafCertWithGitHubOIDs(subject, oidcIssuer, "", "", githubWorkFlowName, "", "", rootCert, rootKey) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - co := &CheckOpts{ - RootCerts: rootPool, - Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, - CertGithubWorkflowName: "otherName", - IgnoreSCT: true, - } - - _, err := ValidateAndUnpackCert(leafCert, co) - require.Contains(t, err.Error(), "expected GitHub Workflow Name not found in certificate") - err = CheckCertificatePolicy(leafCert, co) - require.Contains(t, err.Error(), "expected GitHub Workflow Name not found in certificate") -} - -func TestValidateAndUnpackCertInvalidGithubWorkflowRepository(t *testing.T) { - subject := "email@email" - oidcIssuer := "https://accounts.google.com" - githubWorkFlowRepository := "myRepository" - - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, _, _ := test.GenerateLeafCertWithGitHubOIDs(subject, oidcIssuer, "", "", "", githubWorkFlowRepository, "", rootCert, rootKey) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - co := &CheckOpts{ - RootCerts: rootPool, - Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, - CertGithubWorkflowRepository: "otherRepository", - IgnoreSCT: true, - } - - _, err := ValidateAndUnpackCert(leafCert, co) - require.Contains(t, err.Error(), "expected GitHub Workflow Repository not found in certificate") - err = CheckCertificatePolicy(leafCert, co) - require.Contains(t, err.Error(), "expected GitHub Workflow Repository not found in certificate") -} - -func TestValidateAndUnpackCertInvalidGithubWorkflowRef(t *testing.T) { - subject := "email@email" - oidcIssuer := "https://accounts.google.com" - githubWorkFlowRef := "myRef" - - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, _, _ := test.GenerateLeafCertWithGitHubOIDs(subject, oidcIssuer, "", "", "", "", githubWorkFlowRef, rootCert, rootKey) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - co := &CheckOpts{ - RootCerts: rootPool, - Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, - CertGithubWorkflowRef: "otherRef", - IgnoreSCT: true, - } - - _, err := ValidateAndUnpackCert(leafCert, co) - require.Contains(t, err.Error(), "expected GitHub Workflow Ref not found in certificate") - err = CheckCertificatePolicy(leafCert, co) - require.Contains(t, err.Error(), "expected GitHub Workflow Ref not found in certificate") -} - -func TestValidateAndUnpackCertWithChainSuccess(t *testing.T) { - subject := "email@email" - oidcIssuer := "https://accounts.google.com" - - rootCert, rootKey, _ := test.GenerateRootCa() - subCert, subKey, _ := test.GenerateSubordinateCa(rootCert, rootKey) - leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, subCert, subKey) - - co := &CheckOpts{ - Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, - IgnoreSCT: true, - } - - _, err := ValidateAndUnpackCertWithChain(leafCert, []*x509.Certificate{subCert, leafCert}, co) - if err != nil { - t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) - } -} - -func TestValidateAndUnpackCertWithChainSuccessWithRoot(t *testing.T) { - subject := "email@email" - oidcIssuer := "https://accounts.google.com" - - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey) - - co := &CheckOpts{ - Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, - IgnoreSCT: true, - } - - _, err := ValidateAndUnpackCertWithChain(leafCert, []*x509.Certificate{rootCert}, co) - if err != nil { - t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) - } -} - -func TestValidateAndUnpackCertWithChainFailsWithoutChain(t *testing.T) { - subject := "email@email" - oidcIssuer := "https://accounts.google.com" - - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey) - - co := &CheckOpts{ - Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, - IgnoreSCT: true, - } - - _, err := ValidateAndUnpackCertWithChain(leafCert, []*x509.Certificate{}, co) - if err == nil || err.Error() != "no chain provided to validate certificate" { - t.Errorf("expected error without chain, got %v", err) - } -} - -func TestValidateAndUnpackCertWithChainFailsWithInvalidChain(t *testing.T) { - subject := "email@email" - oidcIssuer := "https://accounts.google.com" - - rootCert, rootKey, _ := test.GenerateRootCa() - leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey) - rootCertOther, _, _ := test.GenerateRootCa() - - co := &CheckOpts{ - Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, - IgnoreSCT: true, - } - - _, err := ValidateAndUnpackCertWithChain(leafCert, []*x509.Certificate{rootCertOther}, co) - if err == nil || !strings.Contains(err.Error(), "certificate signed by unknown authority") { - t.Errorf("expected error without valid chain, got %v", err) - } -} - -func TestValidateAndUnpackCertWithIdentities(t *testing.T) { - u, err := url.Parse("http://url.example.com") - if err != nil { - t.Fatal("failed to parse url", err) - } - emailSubject := "email@example.com" - dnsSubjects := []string{"dnssubject.example.com"} - ipSubjects := []net.IP{net.ParseIP("1.2.3.4")} - uriSubjects := []*url.URL{u} - otherName := "email!example.com" - oidcIssuer := "https://accounts.google.com" - - tests := []struct { - identities []Identity - wantErrSubstring string - dnsNames []string - emailAddresses []string - ipAddresses []net.IP - uris []*url.URL - otherName string - }{ - {identities: nil /* No matches required, checks out */}, - {identities: []Identity{ // Strict match on both - {Subject: emailSubject, Issuer: oidcIssuer}}, - emailAddresses: []string{emailSubject}}, - {identities: []Identity{ // just issuer - {Issuer: oidcIssuer}}, - emailAddresses: []string{emailSubject}}, - {identities: []Identity{ // just subject - {Subject: emailSubject}}, - emailAddresses: []string{emailSubject}}, - {identities: []Identity{ // mis-match - {Subject: "wrongsubject", Issuer: oidcIssuer}, - {Subject: emailSubject, Issuer: "wrongissuer"}}, - emailAddresses: []string{emailSubject}, - wantErrSubstring: "none of the expected identities matched"}, - {identities: []Identity{ // one good identity, other does not match - {Subject: "wrongsubject", Issuer: "wrongissuer"}, - {Subject: emailSubject, Issuer: oidcIssuer}}, - emailAddresses: []string{emailSubject}}, - {identities: []Identity{ // illegal regex for subject - {SubjectRegExp: "****", Issuer: oidcIssuer}}, - emailAddresses: []string{emailSubject}, - wantErrSubstring: "malformed subject in identity"}, - {identities: []Identity{ // illegal regex for issuer - {Subject: emailSubject, IssuerRegExp: "****"}}, - wantErrSubstring: "malformed issuer in identity"}, - {identities: []Identity{ // regex matches - {SubjectRegExp: ".*example.com", IssuerRegExp: ".*accounts.google.*"}}, - emailAddresses: []string{emailSubject}, - wantErrSubstring: ""}, - {identities: []Identity{ // regex matches dnsNames - {SubjectRegExp: ".*ubject.example.com", IssuerRegExp: ".*accounts.google.*"}}, - dnsNames: dnsSubjects, - wantErrSubstring: ""}, - {identities: []Identity{ // regex matches ip - {SubjectRegExp: "1.2.3.*", IssuerRegExp: ".*accounts.google.*"}}, - ipAddresses: ipSubjects, - wantErrSubstring: ""}, - {identities: []Identity{ // regex matches urls - {SubjectRegExp: ".*url.examp.*", IssuerRegExp: ".*accounts.google.*"}}, - uris: uriSubjects, - wantErrSubstring: ""}, - {identities: []Identity{ // regex matches otherName - {SubjectRegExp: ".*example.com", IssuerRegExp: ".*accounts.google.*"}}, - otherName: otherName, - wantErrSubstring: ""}, - } - for _, tc := range tests { - rootCert, rootKey, _ := test.GenerateRootCa() - var leafCert *x509.Certificate - if len(tc.otherName) == 0 { - leafCert, _, _ = test.GenerateLeafCertWithSubjectAlternateNames(tc.dnsNames, tc.emailAddresses, tc.ipAddresses, tc.uris, oidcIssuer, rootCert, rootKey) - } else { - // generate with OtherName, which will override other SANs - ext, err := cryptoutils.MarshalOtherNameSAN(tc.otherName, true) - if err != nil { - t.Fatalf("error marshalling SANs: %v", err) - } - exts := []pkix.Extension{*ext} - leafCert, _, _ = test.GenerateLeafCert("unused", oidcIssuer, rootCert, rootKey, exts...) - } - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - co := &CheckOpts{ - RootCerts: rootPool, - Identities: tc.identities, - IgnoreSCT: true, - } - - _, err := ValidateAndUnpackCert(leafCert, co) - if err == nil && tc.wantErrSubstring != "" { - t.Errorf("Expected error %s got none", tc.wantErrSubstring) - } else if err != nil { - if tc.wantErrSubstring == "" { - t.Errorf("Did not expect an error, got err = %v", err) - } else if !strings.Contains(err.Error(), tc.wantErrSubstring) { - t.Errorf("Did not get the expected error %s, got err = %v", tc.wantErrSubstring, err) - } - } - // Test CheckCertificatePolicy - err = CheckCertificatePolicy(leafCert, co) - if err == nil && tc.wantErrSubstring != "" { - t.Errorf("Expected error %s got none", tc.wantErrSubstring) - } else if err != nil { - if tc.wantErrSubstring == "" { - t.Errorf("Did not expect an error, got err = %v", err) - } else if !strings.Contains(err.Error(), tc.wantErrSubstring) { - t.Errorf("Did not get the expected error %s, got err = %v", tc.wantErrSubstring, err) - } - } - } -} - -func TestValidateAndUnpackCertWithIntermediatesSuccess(t *testing.T) { - subject := "email@email" - oidcIssuer := "https://accounts.google.com" - - rootCert, rootKey, _ := test.GenerateRootCa() - subCert, subKey, _ := test.GenerateSubordinateCa(rootCert, rootKey) - leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, subCert, subKey) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - subPool := x509.NewCertPool() - rootPool.AddCert(subCert) - - co := &CheckOpts{ - RootCerts: rootPool, - IgnoreSCT: true, - Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, - } - - _, chain, err := ValidateAndUnpackCertWithIntermediates(leafCert, co, subPool) - if err != nil { - t.Errorf("ValidateAndUnpackCertWithIntermediates expected no error, got err = %v", err) - } - if len(chain) == 0 { - t.Errorf("expected certificate chain") - } - err = CheckCertificatePolicy(leafCert, co) - if err != nil { - t.Errorf("CheckCertificatePolicy expected no error, got err = %v", err) - } -} - -func TestValidateUnpackCertWithTrustedMaterial(t *testing.T) { - subject := "email@email" - oidcIssuer := "https://accounts.google.com" - var ca root.FulcioCertificateAuthority - rootCert, rootKey, _ := test.GenerateRootCa() - ca.Root = rootCert - leafCert, _, _ := test.GenerateLeafCert(subject, oidcIssuer, rootCert, rootKey) - trustedRoot, err := root.NewTrustedRoot(root.TrustedRootMediaType01, []root.CertificateAuthority{&ca}, nil, nil, nil) - if err != nil { - t.Fatal(err) - } - co := &CheckOpts{ - TrustedMaterial: trustedRoot, - IgnoreSCT: true, - Identities: []Identity{{Subject: subject, Issuer: oidcIssuer}}, - } - _, err = ValidateAndUnpackCert(leafCert, co) - assert.NoError(t, err) -} - -func TestValidateAndUnpackCertWithSCT(t *testing.T) { - chain, err := cryptoutils.UnmarshalCertificatesFromPEM([]byte(strings.Join([]string{testEmbeddedCertPEM, testRootCertPEM}, "\n"))) - if err != nil { - t.Fatalf("error unmarshalling certificate chain: %v", err) - } - - rootPool := x509.NewCertPool() - rootPool.AddCert(chain[1]) - - // Grab the CTLog public keys - pubKeys, err := GetCTLogPubs(context.Background()) - if err != nil { - t.Fatalf("Failed to get CTLog public keys from TUF: %v", err) - } - - co := &CheckOpts{ - RootCerts: rootPool, - // explicitly set to false - IgnoreSCT: false, - CTLogPubKeys: pubKeys, - } - - // write SCT verification key to disk - tmpPrivFile, err := os.CreateTemp(t.TempDir(), "cosign_verify_sct_*.key") - if err != nil { - t.Fatalf("failed to create temp key file: %v", err) - } - defer tmpPrivFile.Close() - if _, err := tmpPrivFile.Write([]byte(testCTLogPublicKeyPEM)); err != nil { - t.Fatalf("failed to write key file: %v", err) - } - t.Setenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE", tmpPrivFile.Name()) - - // Grab the CTLog public keys again so we get them from env. - co.CTLogPubKeys, err = GetCTLogPubs(context.Background()) - if err != nil { - t.Fatalf("Failed to get CTLog public keys from TUF: %v", err) - } - _, err = ValidateAndUnpackCert(chain[0], co) - if err != nil { - t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) +func appendSlices(slices [][]byte) []byte { + totalLen := 0 + for _, s := range slices { + totalLen += len(s) } - - // validate again, explicitly setting ignore SCT to false - co.IgnoreSCT = false - _, err = ValidateAndUnpackCert(chain[0], co) - if err != nil { - t.Errorf("ValidateAndUnpackCert expected no error, got err = %v", err) + tmp := make([]byte, 0, totalLen) + for _, s := range slices { + tmp = append(tmp, s...) } + return tmp } func TestCompareSigs(t *testing.T) { @@ -1872,273 +299,6 @@ func TestVerifyRFC3161Timestamp(t *testing.T) { } } -// This test verifies that artifact verification rejects signatures -// where a CA certificate in the issuing chain is expired. This is a contrived -// example because CAs shouldn't issue certificates where the leaf's validity -// outlives any certificate in the chain, but this is checked for thoroughness. -func TestVerifyImageSignatureExpiredCACertificate(t *testing.T) { - now := time.Now().UTC() - - rootCert, rootKey, _ := test.GenerateRootCa() // Valid +/- 5 hours - - subTemplate := &x509.Certificate{ - SerialNumber: big.NewInt(1), - Subject: pkix.Name{ - CommonName: "sigstore-sub-expired", - Organization: []string{"sigstore.dev"}, - }, - NotBefore: now.Add(-2 * time.Hour), // Valid during root validity - NotAfter: now.Add(-5 * time.Minute), - KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, - BasicConstraintsValid: true, - IsCA: true, - } - subKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatalf("generating subordinate key: %v", err) - } - subBytes, err := x509.CreateCertificate(rand.Reader, subTemplate, rootCert, &subKey.PublicKey, rootKey) - if err != nil { - t.Fatalf("creating subordinate cert: %v", err) - } - subCert, err := x509.ParseCertificate(subBytes) - if err != nil { - t.Fatalf("parsing subordinate cert: %v", err) - } - - leafTemplate := &x509.Certificate{ - SerialNumber: big.NewInt(2), - EmailAddresses: []string{"subject@mail.com"}, - NotBefore: now.Add(-30 * time.Minute), // Valid during intermediate... - NotAfter: now.Add(30 * time.Minute), // but is valid past intermediate expiration - KeyUsage: x509.KeyUsageDigitalSignature, - ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageCodeSigning}, - IsCA: false, - ExtraExtensions: []pkix.Extension{{ - Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}, - Critical: false, - Value: []byte("oidc-issuer"), - }}, - } - leafKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - if err != nil { - t.Fatalf("generating leaf key: %v", err) - } - leafBytes, err := x509.CreateCertificate(rand.Reader, leafTemplate, subCert, &leafKey.PublicKey, subKey) - if err != nil { - t.Fatalf("creating leaf cert: %v", err) - } - leafCert, err := x509.ParseCertificate(leafBytes) - if err != nil { - t.Fatalf("parsing leaf cert: %v", err) - } - pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) - pemSub := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert.Raw}) - pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - payload := []byte{1, 2, 3, 4} - h := sha256.Sum256(payload) - sigBytes, _ := leafKey.Sign(rand.Reader, h[:], crypto.SHA256) - - ociSig, _ := static.NewSignature(payload, - base64.StdEncoding.EncodeToString(sigBytes), - static.WithCertChain(pemLeaf, appendSlices([][]byte{pemSub, pemRoot}))) - - co := &CheckOpts{ - RootCerts: rootPool, - IgnoreSCT: true, - IgnoreTlog: true, - Identities: []Identity{{Subject: "subject@mail.com", Issuer: "oidc-issuer"}}, - } - - // Verify expected failure, where the current time is used - _, err = VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, co) - if err == nil { - t.Fatalf("expected error verifying signature with expired intermediate") - } - var vf *VerificationFailure - if !errors.As(err, &vf) { - t.Fatalf("expected %T, got %T (%v)", &VerificationFailure{}, err, err) - } - - // Verify expected failure with time provided by a signed timestamp - client, err := tsaMock.NewTSAClient((tsaMock.TSAClientOptions{Time: time.Now()})) - if err != nil { - t.Fatal(err) - } - tsBytes, err := getTimestampedSignature(payload, client) - if err != nil { - t.Fatalf("unexpected error creating timestamp: %v", err) - } - rfc3161TS := bundle.RFC3161Timestamp{SignedRFC3161Timestamp: tsBytes} - - certChainPEM, err := cryptoutils.MarshalCertificatesToPEM(client.CertChain) - if err != nil { - t.Fatalf("unexpected error marshalling cert chain: %v", err) - } - - leaves, intermediates, roots, err := splitPEMCertificateChain(certChainPEM) - if err != nil { - t.Fatal("error splitting response into certificate chain") - } - co.TSACertificate = leaves[0] - co.TSAIntermediateCertificates = intermediates - co.TSARootCertificates = roots - - ociSig, _ = static.NewSignature(payload, - base64.StdEncoding.EncodeToString(sigBytes), - static.WithCertChain(pemLeaf, appendSlices([][]byte{pemSub, pemRoot})), - static.WithRFC3161Timestamp(&rfc3161TS)) - _, err = VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, co) - if err == nil { - t.Fatalf("expected error verifying signature with expired intermediate") - } - if !errors.As(err, &vf) { - t.Fatalf("expected %T, got %T (%v)", &VerificationFailure{}, err, err) - } - - // Verify expected failure where the chain is provided via trusted material - // rather than bundled with the image - tm := &trustedMaterialWithFulcioCAs{ - cas: []root.CertificateAuthority{ - &root.FulcioCertificateAuthority{ - Root: rootCert, - Intermediates: []*x509.Certificate{subCert}, - }, - }, - } - co.TrustedMaterial = tm - co.RootCerts = nil - - ociSig, _ = static.NewSignature(payload, - base64.StdEncoding.EncodeToString(sigBytes), - static.WithCertChain(pemLeaf, appendSlices([][]byte{})), - static.WithRFC3161Timestamp(&rfc3161TS)) - _, err = VerifyImageSignature(context.TODO(), ociSig, v1.Hash{}, co) - if err == nil { - t.Fatalf("expected error verifying signature with expired intermediate") - } - if !errors.As(err, &vf) { - t.Fatalf("expected %T, got %T (%v)", &VerificationFailure{}, err, err) - } -} - -type trustedMaterialWithFulcioCAs struct { - root.BaseTrustedMaterial - cas []root.CertificateAuthority -} - -func (tm *trustedMaterialWithFulcioCAs) FulcioCertificateAuthorities() []root.CertificateAuthority { - return tm.cas -} - -func TestVerifyImageAttestation(t *testing.T) { - if _, _, err := VerifyImageAttestation(context.TODO(), nil, v1.Hash{}, nil); err == nil { - t.Error("VerifyImageAttestation() should error when given nil attestations") - } -} - -// Mock Rekor client -type mockEntriesClient struct { - entries.ClientService - searchLogQueryFunc func(params *entries.SearchLogQueryParams, opts ...entries.ClientOption) (*entries.SearchLogQueryOK, error) -} - -func (m *mockEntriesClient) SearchLogQuery(params *entries.SearchLogQueryParams, opts ...entries.ClientOption) (*entries.SearchLogQueryOK, error) { - if m.searchLogQueryFunc != nil { - return m.searchLogQueryFunc(params, opts...) - } - return nil, nil -} - -// createRekorEntry creates a mock Rekor log entry. -func createRekorEntry(ctx context.Context, t *testing.T, logID string, signer signature.Signer, payload, signature []byte, publicKey crypto.PublicKey) *models.LogEntry { - payloadHash := sha256.Sum256(payload) - - publicKeyBytes, err := cryptoutils.MarshalPublicKeyToPEM(publicKey) - require.NoError(t, err) - - artifactProperties := rtypes.ArtifactProperties{ - ArtifactHash: hex.EncodeToString(payloadHash[:]), - SignatureBytes: signature, - PublicKeyBytes: [][]byte{publicKeyBytes}, - PKIFormat: "x509", - } - - // Create and canonicalize Rekor entry - entryProps, err := hashedrekord_v001.V001Entry{}.CreateFromArtifactProperties(ctx, artifactProperties) - require.NoError(t, err) - - rekorEntry, err := rtypes.UnmarshalEntry(entryProps) - require.NoError(t, err) - - canonicalEntry, err := rekorEntry.Canonicalize(ctx) - require.NoError(t, err) - - // Create log entry - integratedTime := time.Now().Unix() - logEntry := models.LogEntryAnon{ - Body: base64.StdEncoding.EncodeToString(canonicalEntry), - IntegratedTime: conv.Pointer(integratedTime), - LogIndex: conv.Pointer(int64(0)), - LogID: conv.Pointer(logID), - } - - // Canonicalize the log entry and sign it - jsonLogEntry, err := json.Marshal(logEntry) - require.NoError(t, err) - - canonicalPayload, err := jsoncanonicalizer.Transform(jsonLogEntry) - require.NoError(t, err) - - signedEntryTimestamp, err := signer.SignMessage(bytes.NewReader(canonicalPayload)) - require.NoError(t, err) - - // Calculate leaf hash and add verification - entryUUID, err := ComputeLeafHash(&logEntry) - require.NoError(t, err) - - logEntry.Verification = &models.LogEntryAnonVerification{ - SignedEntryTimestamp: signedEntryTimestamp, - InclusionProof: &models.InclusionProof{ - LogIndex: conv.Pointer(int64(0)), - TreeSize: conv.Pointer(int64(1)), - RootHash: conv.Pointer(hex.EncodeToString(entryUUID)), - Hashes: []string{}, - }, - } - - // Return the constructed log entry - return &models.LogEntry{hex.EncodeToString(entryUUID): logEntry} -} - -// generateSigner creates an ECDSA signer and public key. -func generateSigner(t *testing.T) (signature.SignerVerifier, crypto.PublicKey) { - privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) - require.NoError(t, err, "error generating private key") - - signer, err := signature.LoadECDSASignerVerifier(privateKey, crypto.SHA256) - require.NoError(t, err, "error loading signer") - - publicKey, err := signer.PublicKey() - require.NoError(t, err, "error getting public key") - - return signer, publicKey -} - -// generateBlobSignature signs a blob and returns the blob, its signature, and the base64-encoded signature. -func generateBlobSignature(t *testing.T, signer signature.Signer) ([]byte, []byte, string) { - blob := []byte("foo") - blobSignature, err := signer.SignMessage(bytes.NewReader(blob)) - require.NoError(t, err, "error signing blob") - blobSignatureBase64 := base64.StdEncoding.EncodeToString(blobSignature) - return blob, blobSignature, blobSignatureBase64 -} - // calculateLogID generates a SHA-256 hash of the given public key and returns it as a hexadecimal string. func calculateLogID(t *testing.T, pub crypto.PublicKey) string { pubBytes, err := x509.MarshalPKIXPublicKey(pub) diff --git a/test/e2e_attach_test.go b/test/e2e_attach_test.go index f157d5294ac..d3d43ade7a4 100644 --- a/test/e2e_attach_test.go +++ b/test/e2e_attach_test.go @@ -40,7 +40,6 @@ import ( "github.com/sigstore/cosign/v3/cmd/cosign/cli/download" "github.com/sigstore/cosign/v3/cmd/cosign/cli/generate" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" - cliverify "github.com/sigstore/cosign/v3/cmd/cosign/cli/verify" cert_test "github.com/sigstore/cosign/v3/internal/test" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/bundle" @@ -66,10 +65,8 @@ func TestAttachSignature(t *testing.T) { hash := sha256.Sum256(b.Bytes()) // Scenario 1: attach a single signature with certificate and certificate chain to an artifact - // and verify it using the root certificate. rootCert1, rootKey1, _ := cert_test.GenerateRootCa() pemRoot1 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert1.Raw}) - pemRootRef1 := mkfile(string(pemRoot1), td, t) subCert1, subKey1, _ := cert_test.GenerateSubordinateCa(rootCert1, rootKey1) leafCert1, privKey1, _ := cert_test.GenerateLeafCert("foo@example.com", "oidc-issuer", subCert1, subKey1) pemSub1 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert1.Raw}) @@ -102,23 +99,10 @@ func TestAttachSignature(t *testing.T) { _, chainOk := manifest.Layers[0].Annotations["dev.sigstore.cosign/chain"] equals(chainOk, true, t) - verifyCmd := cliverify.VerifyCommand{ - IgnoreSCT: true, - IgnoreTlog: true, - CertChain: pemRootRef1, - CertVerifyOptions: options.CertVerifyOptions{ - CertOidcIssuerRegexp: ".*", - CertIdentityRegexp: ".*", - }, - } - args := []string{imgName} - must(verifyCmd.Exec(ctx, args), t) - - // Scenario 2: Attaches second signature with another certificate and certificate chain to the - // same artifact and verify it using both root certificates separately. + // Scenario 2: Attaches second signature with another certificate and certificate chain to the + // same artifact. rootCert2, rootKey2, _ := cert_test.GenerateRootCa() pemRoot2 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert2.Raw}) - pemRootRef2 := mkfile(string(pemRoot2), td, t) subCert2, subKey2, _ := cert_test.GenerateSubordinateCa(rootCert2, rootKey2) leafCert2, privKey2, _ := cert_test.GenerateLeafCert("foo@exampleclient.com", "oidc-issuer", subCert2, subKey2) pemSub2 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert2.Raw}) @@ -132,32 +116,6 @@ func TestAttachSignature(t *testing.T) { err = attach.SignatureCmd(ctx, options.RegistryOptions{}, sigRef2, payloadRef, pemLeafRef2, certChainRef2, "", "", imgName) must(err, t) - - // verify using first root certificate - verifyCmd = cliverify.VerifyCommand{ - IgnoreSCT: true, - IgnoreTlog: true, - CertChain: pemRootRef1, - CertVerifyOptions: options.CertVerifyOptions{ - CertOidcIssuerRegexp: ".*", - CertIdentityRegexp: ".*", - }, - } - args = []string{imgName} - must(verifyCmd.Exec(ctx, args), t) - - // verify using second root cert - verifyCmd = cliverify.VerifyCommand{ - IgnoreSCT: true, - IgnoreTlog: true, - CertChain: pemRootRef2, - CertVerifyOptions: options.CertVerifyOptions{ - CertOidcIssuerRegexp: ".*", - CertIdentityRegexp: ".*", - }, - } - args = []string{imgName} - must(verifyCmd.Exec(ctx, args), t) } func TestAttachWithRekorBundle(t *testing.T) { diff --git a/test/e2e_insecure_registry_test.go b/test/e2e_insecure_registry_test.go index d59273797e4..ba7519c2366 100644 --- a/test/e2e_insecure_registry_test.go +++ b/test/e2e_insecure_registry_test.go @@ -59,8 +59,6 @@ func TestInsecureRegistry(t *testing.T) { useOCI11 := os.Getenv("oci11Var") != "" - rekorURL := os.Getenv(rekorURLVar) - ctx := context.Background() tufLocalCache := t.TempDir() t.Setenv("TUF_ROOT", tufLocalCache) @@ -69,9 +67,9 @@ func TestInsecureRegistry(t *testing.T) { must(initialize.DoInitialize(ctx, rootPath, mirror), t) ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKey, PassFunc: passFunc, - RekorURL: rekorURL, SkipConfirmation: true, } trustedMaterial, err := cosign.TrustedRoot() @@ -80,8 +78,7 @@ func TestInsecureRegistry(t *testing.T) { // Sign without bundle format so := options.SignOptions{ - Upload: true, - TlogUpload: true, + Upload: true, } mustErr(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) so.Registry = options.RegistryOptions{ @@ -94,7 +91,7 @@ func TestInsecureRegistry(t *testing.T) { } } must(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) - mustErr(verify(pubKey, imgName, true, nil, "", false), t) + mustErr(verify(pubKey, imgName, true, nil, false), t) cmd := cliverify.VerifyCommand{ KeyRef: pubKey, CheckClaims: true, @@ -103,9 +100,6 @@ func TestInsecureRegistry(t *testing.T) { AllowHTTPRegistry: true, }, } - if useOCI11 { - cmd.ExperimentalOCI11 = true - } must(cmd.Exec(context.Background(), []string{imgName}), t) // Sign new image with new bundle format @@ -113,10 +107,7 @@ func TestInsecureRegistry(t *testing.T) { imgName = path.Join(repo, "cosign-registry-e2e-2") cleanup2 := makeImageIndexWithInsecureRegistry(t, imgName) defer cleanup2() - - so.NewBundleFormat = true must(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) - cmd.NewBundleFormat = true must(cmd.Exec(context.Background(), []string{imgName}), t) } @@ -134,8 +125,6 @@ func TestAttestInsecureRegistry(t *testing.T) { _, privKey, pubKey := keypair(t, td) - rekorURL := os.Getenv(rekorURLVar) - ctx := context.Background() tufLocalCache := t.TempDir() t.Setenv("TUF_ROOT", tufLocalCache) @@ -144,9 +133,9 @@ func TestAttestInsecureRegistry(t *testing.T) { must(initialize.DoInitialize(ctx, rootPath, mirror), t) ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKey, PassFunc: passFunc, - RekorURL: rekorURL, SkipConfirmation: true, } trustedMaterial, err := cosign.TrustedRoot() @@ -161,12 +150,10 @@ func TestAttestInsecureRegistry(t *testing.T) { // Attest without bundle attestCmd := attest.AttestCommand{ - KeyOpts: ko, - PredicatePath: slsaAttestationPath, - PredicateType: "slsaprovenance", - Timeout: 30 * time.Second, - RekorEntryType: "dsse", - TlogUpload: true, + KeyOpts: ko, + PredicatePath: slsaAttestationPath, + PredicateType: "slsaprovenance", + Timeout: 30 * time.Second, RegistryOptions: options.RegistryOptions{ AllowInsecure: true, AllowHTTPRegistry: true, @@ -188,10 +175,8 @@ func TestAttestInsecureRegistry(t *testing.T) { cleanup2 := makeImageIndexWithInsecureRegistry(t, imgName) defer cleanup2() - ko.NewBundleFormat = true attestCmd.KeyOpts = ko must(attestCmd.Exec(ctx, imgName), t) - verifyAttestation.CommonVerifyOptions.NewBundleFormat = true verifyAttestation.IgnoreTlog = false must(verifyAttestation.Exec(ctx, []string{imgName}), t) } diff --git a/test/e2e_kms_test.go b/test/e2e_kms_test.go index 2d212c85d52..143beeeede5 100644 --- a/test/e2e_kms_test.go +++ b/test/e2e_kms_test.go @@ -59,7 +59,7 @@ func TestSecretsKMS(t *testing.T) { privKey := kms // Verify should fail at first - mustErr(verify(pubKey, imgName, true, nil, "", false), t) + mustErr(verify(pubKey, imgName, true, nil, false), t) rekorURL := os.Getenv(rekorURLVar) @@ -67,32 +67,30 @@ func TestSecretsKMS(t *testing.T) { // Now sign and verify with the KMS key ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKey, - RekorURL: rekorURL, SkipConfirmation: true, } so := options.SignOptions{ - Upload: true, - TlogUpload: true, + Upload: true, } must(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) - must(verify(pubKey, imgName, true, nil, "", false), t) + must(verify(pubKey, imgName, true, nil, false), t) // Sign and verify with annotations - mustErr(verify(pubKey, imgName, true, map[string]any{"foo": "bar"}, "", false), t) + mustErr(verify(pubKey, imgName, true, map[string]any{"foo": "bar"}, false), t) soAnno := options.SignOptions{ - Upload: true, - TlogUpload: true, + Upload: true, AnnotationOptions: options.AnnotationOptions{ Annotations: []string{"foo=bar"}, }, } must(sign.SignCmd(t.Context(), ro, ko, soAnno, []string{imgName}), t) - must(verify(pubKey, imgName, true, map[string]any{"foo": "bar"}, "", false), t) + must(verify(pubKey, imgName, true, map[string]any{"foo": "bar"}, false), t) // Store signatures in a different repo t.Setenv("COSIGN_REPOSITORY", path.Join(repo, "subbedrepo")) must(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) - must(verify(pubKey, imgName, true, nil, "", false), t) + must(verify(pubKey, imgName, true, nil, false), t) os.Unsetenv("COSIGN_REPOSITORY") } diff --git a/test/e2e_test.go b/test/e2e_test.go index d142b919057..6b5a2de6ff8 100644 --- a/test/e2e_test.go +++ b/test/e2e_test.go @@ -69,6 +69,7 @@ import ( "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" "github.com/sigstore/cosign/v3/cmd/cosign/cli/publickey" "github.com/sigstore/cosign/v3/cmd/cosign/cli/sign" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" "github.com/sigstore/cosign/v3/cmd/cosign/cli/signingconfig" "github.com/sigstore/cosign/v3/cmd/cosign/cli/trustedroot" cliverify "github.com/sigstore/cosign/v3/cmd/cosign/cli/verify" @@ -76,10 +77,8 @@ import ( "github.com/sigstore/cosign/v3/internal/pkg/cosign/tsa/client" cert_test "github.com/sigstore/cosign/v3/internal/test" "github.com/sigstore/cosign/v3/pkg/cosign" - "github.com/sigstore/cosign/v3/pkg/cosign/bundle" "github.com/sigstore/cosign/v3/pkg/cosign/env" "github.com/sigstore/cosign/v3/pkg/cosign/kubernetes" - "github.com/sigstore/cosign/v3/pkg/oci/mutate" ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" sigs "github.com/sigstore/cosign/v3/pkg/signature" protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" @@ -113,98 +112,37 @@ func TestSignVerify(t *testing.T) { ctx := context.Background() // Verify should fail at first - mustErr(verify(pubKeyPath, imgName, true, nil, "", false), t) + mustErr(verify(pubKeyPath, imgName, true, nil, false), t) // So should download mustErr(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName, os.Stdout), t) // Now sign the image ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKeyPath, PassFunc: passFunc, - RekorURL: rekorURL, SkipConfirmation: true, } so := options.SignOptions{ - Upload: true, - TlogUpload: true, + Upload: true, } must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) // Now verify and download should work! - must(verify(pubKeyPath, imgName, true, nil, "", false), t) + must(verify(pubKeyPath, imgName, true, nil, false), t) must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName, os.Stdout), t) // Ensure it verifies if you default to the new protobuf bundle format cmd := cliverify.VerifyCommand{ - KeyRef: pubKeyPath, - RekorURL: rekorURL, - NewBundleFormat: true, + KeyRef: pubKeyPath, + CommonVerifyOptions: options.CommonVerifyOptions{ + TrustedRootPath: getTestTrustedRootPath(), + }, } must(cmd.Exec(ctx, []string{imgName}), t) // Look for a specific annotation - mustErr(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar"}, "", false), t) - - so.AnnotationOptions = options.AnnotationOptions{ - Annotations: []string{"foo=bar"}, - } - // Sign the image with an annotation - must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) - - // It should match this time. - must(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar"}, "", false), t) - - // But two doesn't work - mustErr(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar", "baz": "bat"}, "", false), t) -} - -func TestSignVerifyCertBundle(t *testing.T) { - td := t.TempDir() - err := downloadAndSetEnv(t, rekorURL+"/api/v1/log/publicKey", env.VariableSigstoreRekorPublicKey.String(), td) - if err != nil { - t.Fatal(err) - } - - repo, stop := reg(t) - defer stop() - - imgName := path.Join(repo, "cosign-e2e") - - _, _, cleanup := mkimage(t, imgName) - defer cleanup() - - _, privKeyPath, pubKeyPath := keypair(t, td) - caCertFile, _ /* caPrivKeyFile */, caIntermediateCertFile, _ /* caIntermediatePrivKeyFile */, certFile, certChainFile, err := generateCertificateBundleFiles(td, true, "foobar") - must(err, t) - - ctx := context.Background() - // Verify should fail at first - mustErr(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, imgName, true, nil, "", true), t) - // So should download - mustErr(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName, os.Stdout), t) - - // Now sign the image - ko := options.KeyOpts{ - KeyRef: privKeyPath, - PassFunc: passFunc, - RekorURL: rekorURL, - SkipConfirmation: true, - } - so := options.SignOptions{ - Upload: true, - TlogUpload: true, - } - must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) - - // Now verify and download should work! - ignoreTlog := true - must(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, imgName, true, nil, "", ignoreTlog), t) - // verification with certificate chain instead of root/intermediate files should work as well - must(verifyCertChain(pubKeyPath, certChainFile, certFile, imgName, true, nil, "", ignoreTlog), t) - must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName, os.Stdout), t) - - // Look for a specific annotation - mustErr(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, imgName, true, map[string]interface{}{"foo": "bar"}, "", ignoreTlog), t) + mustErr(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar"}, false), t) so.AnnotationOptions = options.AnnotationOptions{ Annotations: []string{"foo=bar"}, @@ -213,10 +151,10 @@ func TestSignVerifyCertBundle(t *testing.T) { must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) // It should match this time. - must(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, imgName, true, map[string]interface{}{"foo": "bar"}, "", ignoreTlog), t) + must(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar"}, false), t) // But two doesn't work - mustErr(verifyCertBundle(pubKeyPath, caCertFile, caIntermediateCertFile, imgName, true, map[string]interface{}{"foo": "bar", "baz": "bat"}, "", ignoreTlog), t) + mustErr(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar", "baz": "bat"}, false), t) } func TestSignVerifyClean(t *testing.T) { @@ -239,26 +177,25 @@ func TestSignVerifyClean(t *testing.T) { // Now sign the image ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKeyPath, PassFunc: passFunc, - RekorURL: rekorURL, SkipConfirmation: true, } so := options.SignOptions{ - Upload: true, - TlogUpload: true, + Upload: true, } must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) // Now verify and download should work! - must(verify(pubKeyPath, imgName, true, nil, "", false), t) + must(verify(pubKeyPath, imgName, true, nil, false), t) must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName, os.Stdout), t) // Now clean signature from the given image must(cli.CleanCmd(ctx, options.RegistryOptions{}, "all", imgName, true), t) // It doesn't work - mustErr(verify(pubKeyPath, imgName, true, nil, "", false), t) + mustErr(verify(pubKeyPath, imgName, true, nil, false), t) } func TestImportSignVerifyClean(t *testing.T) { @@ -281,48 +218,25 @@ func TestImportSignVerifyClean(t *testing.T) { // Now sign the image ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKeyPath, PassFunc: passFunc, - RekorURL: rekorURL, SkipConfirmation: true, } so := options.SignOptions{ - Upload: true, - TlogUpload: true, + Upload: true, } must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) // Now verify and download should work! - must(verify(pubKeyPath, imgName, true, nil, "", false), t) + must(verify(pubKeyPath, imgName, true, nil, false), t) must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName, os.Stdout), t) // Now clean signature from the given image must(cli.CleanCmd(ctx, options.RegistryOptions{}, "all", imgName, true), t) - // It doesn't work - mustErr(verify(pubKeyPath, imgName, true, nil, "", false), t) - - // Sign with new bundle format - so.NewBundleFormat = true - must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) - - // Verify should work again - trustedRootPath := prepareTrustedRoot(t, "") - bundleVerifyCmd := cliverify.VerifyCommand{ - CommonVerifyOptions: options.CommonVerifyOptions{ - TrustedRootPath: trustedRootPath, - }, - KeyRef: pubKeyPath, - NewBundleFormat: true, - UseSignedTimestamps: false, - } - must(bundleVerifyCmd.Exec(ctx, []string{imgName}), t) - - // Clean again - must(cli.CleanCmd(ctx, options.RegistryOptions{}, "all", imgName, true), t) - - // Verify should fail again - mustErr(bundleVerifyCmd.Exec(ctx, []string{imgName}), t) + // Verify should fail now + mustErr(verify(pubKeyPath, imgName, true, nil, false), t) } type targetInfo struct { @@ -637,6 +551,37 @@ func prepareTrustedRoot(t *testing.T, tsaURL string) string { return cmd.Out } +func prepareTrustedRootTSA(t *testing.T, tsaURL string) string { + downloadDirectory := t.TempDir() + caPath := filepath.Join(downloadDirectory, "fulcio.crt.pem") + caFP, err := os.Create(caPath) + must(err, t) + defer caFP.Close() + must(downloadFile(fulcioURL+"/api/v1/rootCert", caFP), t) + + rekorPath := filepath.Join(downloadDirectory, "rekor.pub") + rekorFP, err := os.Create(rekorPath) + must(err, t) + defer rekorFP.Close() + must(downloadFile(rekorURL+"/api/v1/log/publicKey", rekorFP), t) + + out := filepath.Join(downloadDirectory, "trusted_root.json") + cmd := &trustedroot.CreateCmd{ + CertChain: []string{caPath}, + Out: out, + RekorKeyPath: []string{rekorPath}, + } + if tsaURL != "" { + tsaPath := filepath.Join(downloadDirectory, "tsa.crt.pem") + tsaFP, err := os.Create(tsaPath) + must(err, t) + must(downloadFile(tsaURL+"/api/v1/timestamp/certchain", tsaFP), t) + cmd.TSACertChainPath = []string{tsaPath} + } + must(cmd.Exec(context.Background()), t) + return out +} + func prepareTrustedRootWithSelfSignedCertificate(t *testing.T, certPath, tsaURL string) string { td := t.TempDir() cmd := trustedRootCmd(t, td, tsaURL) @@ -657,6 +602,7 @@ func TestSignVerifyWithTUFMirror(t *testing.T) { tsaLeaf, tsaInter, tsaRoot, err := downloadTSACerts(t.TempDir(), tsaURL) must(err, t) trustedRoot := prepareTrustedRoot(t, tsaURL) + signingConfigStr := prepareSigningConfig(t, fulcioURL, rekorURL, "unused", tsaURL+"/api/v1/timestamp") tests := []struct { name string targets []targetInfo @@ -689,6 +635,14 @@ func TestSignVerifyWithTUFMirror(t *testing.T) { name: "tsa_intermediate_0.crt.pem", source: tsaInter, }, + { + name: "trusted_root.json", + source: trustedRoot, + }, + { + name: "signing_config.v0.2.json", + source: signingConfigStr, + }, }, }, { @@ -718,6 +672,10 @@ func TestSignVerifyWithTUFMirror(t *testing.T) { name: "tsachain.pem", source: tsaInter, }, + { + name: "signing_config.v0.2.json", + source: signingConfigStr, + }, }, wantVerifyErr: true, }, @@ -759,6 +717,14 @@ func TestSignVerifyWithTUFMirror(t *testing.T) { source: tsaInter, usage: "TSA", }, + { + name: "trusted_root.json", + source: trustedRoot, + }, + { + name: "signing_config.v0.2.json", + source: signingConfigStr, + }, }, }, { @@ -768,6 +734,10 @@ func TestSignVerifyWithTUFMirror(t *testing.T) { name: "trusted_root.json", source: trustedRoot, }, + { + name: "signing_config.v0.2.json", + source: signingConfigStr, + }, }, }, } @@ -794,12 +764,12 @@ func TestSignVerifyWithTUFMirror(t *testing.T) { _, _, cleanup := mkimage(t, imgName) defer cleanup() + signingConfig, err := cosign.SigningConfig() + must(err, t) ko := options.KeyOpts{ - FulcioURL: fulcioURL, - RekorURL: rekorURL, + SigningConfig: signingConfig, IDToken: identityToken, SkipConfirmation: true, - TSAServerURL: tsaURL + "/api/v1/timestamp", } trustedMaterial, err := cosign.TrustedRoot() if err == nil { @@ -807,7 +777,6 @@ func TestSignVerifyWithTUFMirror(t *testing.T) { } so := options.SignOptions{ Upload: true, - TlogUpload: true, SkipConfirmation: true, } gotErr := sign.SignCmd(ctx, ro, ko, so, []string{imgName}) @@ -820,7 +789,6 @@ func TestSignVerifyWithTUFMirror(t *testing.T) { CertOidcIssuer: issuer, CertIdentity: certID, }, - Offline: true, CheckClaims: true, UseSignedTimestamps: true, } @@ -840,10 +808,9 @@ func TestSignVerifyWithTUFMirror(t *testing.T) { } tsPath := filepath.Join(blobDir, "ts.txt") bundlePath := filepath.Join(blobDir, "bundle.sig") - // TODO(cmurphy): make this work with ko.NewBundleFormat = true ko.BundlePath = bundlePath ko.RFC3161TimestampPath = tsPath - _, gotErr = sign.SignBlobCmd(ctx, ro, ko, bp, "", "", true, "", "", true) + gotErr = sign.SignBlobCmd(ctx, ro, ko, bp, "", "") must(gotErr, t) // Verify a blob @@ -853,7 +820,6 @@ func TestSignVerifyWithTUFMirror(t *testing.T) { CertOidcIssuer: issuer, CertIdentity: certID, }, - Offline: true, UseSignedTimestamps: true, } gotErr = verifyBlobCmd.Exec(ctx, bp) @@ -944,10 +910,9 @@ func TestSignAttestVerifyBlobWithSigningConfig(t *testing.T) { t.Fatal(err) } bundlePath := filepath.Join(blobDir, "bundle.json") - ko.NewBundleFormat = true ko.BundlePath = bundlePath - _, err = sign.SignBlobCmd(ctx, ro, ko, bp, "", "", false, "", "", true) + err = sign.SignBlobCmd(ctx, ro, ko, bp, "", "") must(err, t) // Verify a blob @@ -971,14 +936,11 @@ func TestSignAttestVerifyBlobWithSigningConfig(t *testing.T) { t.Fatal(err) } attBundlePath := filepath.Join(attestDir, "attest.bundle.json") - ko.NewBundleFormat = true ko.BundlePath = attBundlePath attestBlobCmd := attest.AttestBlobCommand{ - KeyOpts: ko, - RekorEntryType: "dsse", - StatementPath: statementPath, - TlogUpload: true, + KeyOpts: ko, + StatementPath: statementPath, } must(attestBlobCmd.Exec(ctx, bp), t) @@ -1041,7 +1003,6 @@ func TestSignAttestVerifyContainerWithSigningConfig(t *testing.T) { ko := options.KeyOpts{ IDToken: identityToken, - NewBundleFormat: true, SkipConfirmation: true, } trustedMaterial, err := cosign.TrustedRoot() @@ -1053,9 +1014,7 @@ func TestSignAttestVerifyContainerWithSigningConfig(t *testing.T) { // Sign image with identity token in bundle format so := options.SignOptions{ - Upload: true, - NewBundleFormat: true, - TlogUpload: true, + Upload: true, } must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) @@ -1065,7 +1024,6 @@ func TestSignAttestVerifyContainerWithSigningConfig(t *testing.T) { CertOidcIssuer: os.Getenv("ISSUER_URL"), CertIdentity: certID, }, - NewBundleFormat: true, UseSignedTimestamps: true, } args := []string{imgName} @@ -1078,12 +1036,10 @@ func TestSignAttestVerifyContainerWithSigningConfig(t *testing.T) { t.Fatal(err) } attestCmd := attest.AttestCommand{ - KeyOpts: ko, - PredicatePath: predicatePath, - PredicateType: "slsaprovenance", - Timeout: 30 * time.Second, - RekorEntryType: "dsse", - TlogUpload: true, + KeyOpts: ko, + PredicatePath: predicatePath, + PredicateType: "slsaprovenance", + Timeout: 30 * time.Second, } must(attestCmd.Exec(ctx, imgName), t) @@ -1093,9 +1049,6 @@ func TestSignAttestVerifyContainerWithSigningConfig(t *testing.T) { CertOidcIssuer: os.Getenv("ISSUER_URL"), CertIdentity: certID, }, - CommonVerifyOptions: options.CommonVerifyOptions{ - NewBundleFormat: true, - }, PredicateType: "slsaprovenance", UseSignedTimestamps: true, CheckClaims: true, @@ -1170,7 +1123,6 @@ func TestSignVerifyContainerWithSigningConfigWithCertificate(t *testing.T) { must(initialize.DoInitialize(ctx, rootPath, mirror), t) ko := options.KeyOpts{ - NewBundleFormat: true, SkipConfirmation: true, KeyRef: importKeyPath, PassFunc: passFunc, @@ -1184,11 +1136,9 @@ func TestSignVerifyContainerWithSigningConfigWithCertificate(t *testing.T) { // Sign image with cert in bundle format so := options.SignOptions{ - Upload: true, - NewBundleFormat: true, - Key: importKeyPath, - Cert: certPath, - TlogUpload: false, + Upload: true, + Key: importKeyPath, + Cert: certPath, } must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) @@ -1198,8 +1148,7 @@ func TestSignVerifyContainerWithSigningConfigWithCertificate(t *testing.T) { CertOidcIssuerRegexp: ".*", CertIdentity: "foo@bar.com", }, - NewBundleFormat: true, - IgnoreSCT: true, + IgnoreSCT: true, } args := []string{imgName} must(cmd.Exec(ctx, args), t) @@ -1242,7 +1191,6 @@ func TestSignVerifyContainerWithCertificateChain(t *testing.T) { must(err, t) ko := options.KeyOpts{ - NewBundleFormat: true, SkipConfirmation: true, KeyRef: leafKeyPath, PassFunc: passFunc, @@ -1283,21 +1231,18 @@ func TestSignVerifyContainerWithCertificateChain(t *testing.T) { var verifyErr error if tc.attestation { attestCmd := attest.AttestCommand{ - KeyOpts: ko, - CertPath: leafCertPath, - CertChainPath: signChainPath, - PredicatePath: predicatePath, - PredicateType: predicateType, - Timeout: 30 * time.Second, - RekorEntryType: "dsse", - TlogUpload: false, + KeyOpts: ko, + CertPath: leafCertPath, + CertChainPath: signChainPath, + PredicatePath: predicatePath, + PredicateType: predicateType, + Timeout: 30 * time.Second, } must(attestCmd.Exec(ctx, imgName), t) verifyErr = (&cliverify.VerifyAttestationCommand{ CertVerifyOptions: certVerify, CommonVerifyOptions: options.CommonVerifyOptions{ - NewBundleFormat: true, AllowCertificateChain: tc.allowChain, }, IgnoreSCT: true, @@ -1306,18 +1251,15 @@ func TestSignVerifyContainerWithCertificateChain(t *testing.T) { }).Exec(ctx, []string{imgName}) } else { so := options.SignOptions{ - Upload: true, - NewBundleFormat: true, - Key: leafKeyPath, - Cert: leafCertPath, - CertChain: signChainPath, - TlogUpload: false, + Upload: true, + Key: leafKeyPath, + Cert: leafCertPath, + CertChain: signChainPath, } must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) verifyErr = (&cliverify.VerifyCommand{ CertVerifyOptions: certVerify, - NewBundleFormat: true, IgnoreSCT: true, AllowCertificateChain: tc.allowChain, }).Exec(ctx, []string{imgName}) @@ -1383,7 +1325,6 @@ func TestSignVerifyBlobWithCertificateChain(t *testing.T) { must(os.WriteFile(statementPath, []byte(statement), 0o644), t) ko := options.KeyOpts{ - NewBundleFormat: true, SkipConfirmation: true, KeyRef: leafKeyPath, PassFunc: passFunc, @@ -1412,17 +1353,15 @@ func TestSignVerifyBlobWithCertificateChain(t *testing.T) { var verifyErr error if tc.attestation { attestBlobCmd := attest.AttestBlobCommand{ - KeyOpts: ko, - CertPath: leafCertPath, - CertChainPath: signChainPath, - RekorEntryType: "dsse", - StatementPath: statementPath, - TlogUpload: false, + KeyOpts: ko, + CertPath: leafCertPath, + CertChainPath: signChainPath, + StatementPath: statementPath, } must(attestBlobCmd.Exec(ctx, bp), t) verifyErr = (&cliverify.VerifyBlobAttestationCommand{ - KeyOpts: options.KeyOpts{NewBundleFormat: true, BundlePath: bundlePath}, + KeyOpts: options.KeyOpts{BundlePath: bundlePath}, CertVerifyOptions: certVerify, IgnoreSCT: true, CheckClaims: true, @@ -1432,11 +1371,11 @@ func TestSignVerifyBlobWithCertificateChain(t *testing.T) { AllowCertificateChain: tc.allowChain, }).Exec(ctx, "") } else { - _, err = sign.SignBlobCmd(ctx, ro, ko, bp, leafCertPath, signChainPath, true, "", "", false) + err = sign.SignBlobCmd(ctx, ro, ko, bp, leafCertPath, signChainPath) must(err, t) verifyErr = (&cliverify.VerifyBlobCmd{ - KeyOpts: options.KeyOpts{NewBundleFormat: true, BundlePath: bundlePath}, + KeyOpts: options.KeyOpts{BundlePath: bundlePath}, CertVerifyOptions: certVerify, IgnoreSCT: true, AllowCertificateChain: tc.allowChain, @@ -1502,7 +1441,6 @@ func TestSignRekorV2NoTSA(t *testing.T) { ko := options.KeyOpts{ IDToken: identityToken, - NewBundleFormat: true, SkipConfirmation: true, } trustedMaterial, err := cosign.TrustedRoot() @@ -1519,9 +1457,7 @@ func TestSignRekorV2NoTSA(t *testing.T) { defer cleanup() so := options.SignOptions{ - Upload: true, - NewBundleFormat: true, - TlogUpload: true, + Upload: true, } // This should fail because we are using Rekor v2 (configured) but no TSA, with an ID token. @@ -1601,16 +1537,13 @@ func TestSignAttestVerifyRekorV2(t *testing.T) { signBundlePath := filepath.Join(t.TempDir(), "sign.bundle") ko := options.KeyOpts{ IDToken: identityToken, - NewBundleFormat: true, SkipConfirmation: true, TrustedMaterial: trustedMaterial, SigningConfig: signingConfig, } must(sign.SignCmd(ctx, ro, ko, options.SignOptions{ - Upload: true, - NewBundleFormat: true, - TlogUpload: true, - BundlePath: signBundlePath, + Upload: true, + BundlePath: signBundlePath, }, []string{imgName}), t) assertRekorV2HashedrekordEntry(t, signBundlePath) @@ -1623,12 +1556,10 @@ func TestSignAttestVerifyRekorV2(t *testing.T) { ko.BundlePath = filepath.Join(t.TempDir(), "att.bundle") must((&attest.AttestCommand{ - KeyOpts: ko, - PredicatePath: predicatePath, - PredicateType: "slsaprovenance", - Timeout: 30 * time.Second, - RekorEntryType: "dsse", - TlogUpload: true, + KeyOpts: ko, + PredicatePath: predicatePath, + PredicateType: "slsaprovenance", + Timeout: 30 * time.Second, }).Exec(ctx, imgName), t) assertRekorV2HashedrekordEntry(t, ko.BundlePath) @@ -1638,7 +1569,6 @@ func TestSignAttestVerifyRekorV2(t *testing.T) { CertOidcIssuer: os.Getenv("ISSUER_URL"), CertIdentity: certID, }, - NewBundleFormat: true, UseSignedTimestamps: true, }).Exec(ctx, []string{imgName}), t) @@ -1648,9 +1578,6 @@ func TestSignAttestVerifyRekorV2(t *testing.T) { CertOidcIssuer: os.Getenv("ISSUER_URL"), CertIdentity: certID, }, - CommonVerifyOptions: options.CommonVerifyOptions{ - NewBundleFormat: true, - }, PredicateType: "slsaprovenance", UseSignedTimestamps: true, CheckClaims: true, @@ -1730,11 +1657,10 @@ func TestSignVerifyWithSigningConfigWithKey(t *testing.T) { t.Fatal(err) } bundlePath := filepath.Join(blobDir, "bundle.json") - ko.NewBundleFormat = true ko.BundlePath = bundlePath ko.KeyRef = privKeyPath - _, err = sign.SignBlobCmd(ctx, ro, ko, bp, "", "", false, "", "", true) + err = sign.SignBlobCmd(ctx, ro, ko, bp, "", "") must(err, t) // Verify a blob with the key in the trusted root @@ -1753,15 +1679,12 @@ func TestSignVerifyWithSigningConfigWithKey(t *testing.T) { t.Fatal(err) } attBundlePath := filepath.Join(attestDir, "attest.bundle.json") - ko.NewBundleFormat = true ko.BundlePath = attBundlePath ko.KeyRef = privKeyPath attestBlobCmd := attest.AttestBlobCommand{ - KeyOpts: ko, - RekorEntryType: "dsse", - StatementPath: statementPath, - TlogUpload: true, + KeyOpts: ko, + StatementPath: statementPath, } must(attestBlobCmd.Exec(ctx, bp), t) @@ -1794,15 +1717,13 @@ func TestSignVerifyBundle(t *testing.T) { // Sign image with key in bundle format ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKeyPath, PassFunc: passFunc, - RekorURL: rekorURL, SkipConfirmation: true, } so := options.SignOptions{ - Upload: true, - NewBundleFormat: true, - TlogUpload: true, + Upload: true, } must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) @@ -1814,7 +1735,6 @@ func TestSignVerifyBundle(t *testing.T) { TrustedRootPath: trustedRootPath, }, KeyRef: pubKeyPath, - NewBundleFormat: true, UseSignedTimestamps: false, } args := []string{imgName} @@ -1823,14 +1743,13 @@ func TestSignVerifyBundle(t *testing.T) { // Sign image with key in bundle format without Rekor _, privKeyPath, pubKeyPath = keypair(t, td) ko = options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKeyPath, PassFunc: passFunc, SkipConfirmation: true, } so = options.SignOptions{ - Upload: true, - NewBundleFormat: true, - TlogUpload: false, + Upload: true, } must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) // Verify bundle without Rekor @@ -1839,7 +1758,6 @@ func TestSignVerifyBundle(t *testing.T) { TrustedRootPath: trustedRootPath, }, KeyRef: pubKeyPath, - NewBundleFormat: true, IgnoreTlog: true, UseSignedTimestamps: false, } @@ -1852,15 +1770,12 @@ func TestSignVerifyBundle(t *testing.T) { } ko = options.KeyOpts{ + SigningConfig: rekorSigningConfig, IDToken: identityToken, - FulcioURL: fulcioURL, - RekorURL: rekorURL, SkipConfirmation: true, } so = options.SignOptions{ - Upload: true, - NewBundleFormat: true, - TlogUpload: true, + Upload: true, } must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) @@ -1873,7 +1788,6 @@ func TestSignVerifyBundle(t *testing.T) { CommonVerifyOptions: options.CommonVerifyOptions{ TrustedRootPath: trustedRootPath, }, - NewBundleFormat: true, UseSignedTimestamps: false, } must(cmd.Exec(ctx, args), t) @@ -1881,15 +1795,13 @@ func TestSignVerifyBundle(t *testing.T) { // Add annotations and verify claims _, privKeyPath, pubKeyPath = keypair(t, td) ko = options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKeyPath, PassFunc: passFunc, - RekorURL: rekorURL, SkipConfirmation: true, } so = options.SignOptions{ - Upload: true, - NewBundleFormat: true, - TlogUpload: true, + Upload: true, AnnotationOptions: options.AnnotationOptions{ Annotations: []string{"foo=bar"}, }, @@ -1900,7 +1812,6 @@ func TestSignVerifyBundle(t *testing.T) { TrustedRootPath: trustedRootPath, }, KeyRef: pubKeyPath, - NewBundleFormat: true, UseSignedTimestamps: false, Annotations: sigs.AnnotationsMap{Annotations: map[string]any{"foo": "bar"}}, CheckClaims: true, @@ -1937,21 +1848,19 @@ func TestSignVerifyBundleOffline(t *testing.T) { // Sign image with key in bundle format ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKeyPath, PassFunc: passFunc, SkipConfirmation: true, } so := options.SignOptions{ - Upload: true, - NewBundleFormat: true, - TlogUpload: false, + Upload: true, } must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) // Verify bundle offline cmd := cliverify.VerifyCommand{ KeyRef: pubKeyPath, - NewBundleFormat: true, IgnoreTlog: true, UseSignedTimestamps: false, } @@ -2145,15 +2054,12 @@ func TestSigningConfigCreateFromDefaults(t *testing.T) { } func TestAttestVerify(t *testing.T) { - for _, newBundleFormat := range []bool{false, true} { - attestVerify(t, - newBundleFormat, - "slsaprovenance", - `{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }`, - `predicate: builder: id: "2"`, - `predicate: builder: id: "1"`, - ) - } + attestVerify(t, + "slsaprovenance", + `{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }`, + `predicate: builder: id: "2"`, + `predicate: builder: id: "1"`, + ) } func TestAttestVerifySPDXJSON(t *testing.T) { @@ -2161,15 +2067,12 @@ func TestAttestVerifySPDXJSON(t *testing.T) { if err != nil { t.Fatal(err) } - for _, newBundleFormat := range []bool{false, true} { - attestVerify(t, - newBundleFormat, - "spdxjson", - string(attestationBytes), - `predicate: spdxVersion: "SPDX-2.2"`, - `predicate: spdxVersion: "SPDX-9.9"`, - ) - } + attestVerify(t, + "spdxjson", + string(attestationBytes), + `predicate: spdxVersion: "SPDX-2.2"`, + `predicate: spdxVersion: "SPDX-9.9"`, + ) } func TestAttestVerifyCycloneDXJSON(t *testing.T) { @@ -2177,15 +2080,12 @@ func TestAttestVerifyCycloneDXJSON(t *testing.T) { if err != nil { t.Fatal(err) } - for _, newBundleFormat := range []bool{false, true} { - attestVerify(t, - newBundleFormat, - "cyclonedx", - string(attestationBytes), - `predicate: specVersion: "1.4"`, - `predicate: specVersion: "7.7"`, - ) - } + attestVerify(t, + "cyclonedx", + string(attestationBytes), + `predicate: specVersion: "1.4"`, + `predicate: specVersion: "7.7"`, + ) } func TestAttestVerifyURI(t *testing.T) { @@ -2193,18 +2093,15 @@ func TestAttestVerifyURI(t *testing.T) { if err != nil { t.Fatal(err) } - for _, newBundleFormat := range []bool{false, true} { - attestVerify(t, - newBundleFormat, - "https://example.com/TestResult/v1", - string(attestationBytes), - `predicate: passed: true`, - `predicate: passed: false"`, - ) - } + attestVerify(t, + "https://example.com/TestResult/v1", + string(attestationBytes), + `predicate: passed: true`, + `predicate: passed: false"`, + ) } -func attestVerify(t *testing.T, newBundleFormat bool, predicateType, attestation, goodCue, badCue string) { +func attestVerify(t *testing.T, predicateType, attestation, goodCue, badCue string) { repo, stop := reg(t) defer stop() td := t.TempDir() @@ -2233,10 +2130,6 @@ func attestVerify(t *testing.T, newBundleFormat bool, predicateType, attestation MaxWorkers: 10, } - if newBundleFormat { - verifyAttestation.NewBundleFormat = true - } - // Fail case when using without type and policy flag mustErr(verifyAttestation.Exec(ctx, []string{imgName}), t) @@ -2245,13 +2138,15 @@ func attestVerify(t *testing.T, newBundleFormat bool, predicateType, attestation } // Now attest the image - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc, NewBundleFormat: newBundleFormat} attestCmd := attest.AttestCommand{ - KeyOpts: ko, - PredicatePath: attestationPath, - PredicateType: predicateType, - Timeout: 30 * time.Second, - RekorEntryType: "dsse", + KeyOpts: options.KeyOpts{ + SigningConfig: rekorSigningConfig, + KeyRef: privKeyPath, + PassFunc: passFunc, + }, + PredicatePath: attestationPath, + PredicateType: predicateType, + Timeout: 30 * time.Second, } must(attestCmd.Exec(ctx, imgName), t) @@ -2273,7 +2168,7 @@ func attestVerify(t *testing.T, newBundleFormat bool, predicateType, attestation must(verifyAttestation.Exec(ctx, []string{imgName}), t) // Look for a specific annotation - mustErr(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar"}, "", false), t) + mustErr(verify(pubKeyPath, imgName, true, map[string]interface{}{"foo": "bar"}, false), t) } func TestAttestationDownload(t *testing.T) { @@ -2287,7 +2182,12 @@ func TestAttestationDownload(t *testing.T) { defer cleanup() _, privKeyPath, _ := keypair(t, td) - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, + KeyRef: privKeyPath, + PassFunc: passFunc, + SkipConfirmation: true, + } ctx := context.Background() @@ -2337,23 +2237,19 @@ func TestAttestationDownload(t *testing.T) { // Attest to create a slsa attestation attestCommand := attest.AttestCommand{ - KeyOpts: ko, - PredicatePath: slsaAttestationPath, - PredicateType: "slsaprovenance", - Timeout: 30 * time.Second, - Replace: true, - RekorEntryType: "dsse", + KeyOpts: ko, + PredicatePath: slsaAttestationPath, + PredicateType: "slsaprovenance", + Timeout: 30 * time.Second, } must(attestCommand.Exec(ctx, imgName), t) // Attest to create a vuln attestation attestCommand = attest.AttestCommand{ - KeyOpts: ko, - PredicatePath: vulnAttestationPath, - PredicateType: "vuln", - Timeout: 30 * time.Second, - Replace: true, - RekorEntryType: "dsse", + KeyOpts: ko, + PredicatePath: vulnAttestationPath, + PredicateType: "vuln", + Timeout: 30 * time.Second, } must(attestCommand.Exec(ctx, imgName), t) @@ -2361,12 +2257,12 @@ func TestAttestationDownload(t *testing.T) { attOpts := options.AttestationDownloadOptions{} must(download.AttestationCmd(ctx, regOpts, attOpts, imgName, os.Stdout), t) - attestations, err := cosign.FetchAttestationsForReference(ctx, ref, attOpts.PredicateType, ociremoteOpts...) + bundles, _, err := cosign.GetBundles(ctx, ref, ociremoteOpts) if err != nil { t.Fatal(err) } - if len(attestations) != 2 { - t.Fatal(fmt.Errorf("expected len(attestations) == 2, got %d", len(attestations))) + if len(bundles) != 2 { + t.Fatal(fmt.Errorf("expected len(bundles) == 2, got %d", len(bundles))) } } @@ -2381,7 +2277,12 @@ func TestAttestationDownloadWithPredicateType(t *testing.T) { defer cleanup() _, privKeyPath, _ := keypair(t, td) - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, + KeyRef: privKeyPath, + PassFunc: passFunc, + SkipConfirmation: true, + } ctx := context.Background() @@ -2431,23 +2332,19 @@ func TestAttestationDownloadWithPredicateType(t *testing.T) { // Attest to create a slsa attestation attestCommand := attest.AttestCommand{ - KeyOpts: ko, - PredicatePath: slsaAttestationPath, - PredicateType: "slsaprovenance", - Timeout: 30 * time.Second, - Replace: true, - RekorEntryType: "dsse", + KeyOpts: ko, + PredicatePath: slsaAttestationPath, + PredicateType: "slsaprovenance", + Timeout: 30 * time.Second, } must(attestCommand.Exec(ctx, imgName), t) // Attest to create a vuln attestation attestCommand = attest.AttestCommand{ - KeyOpts: ko, - PredicatePath: vulnAttestationPath, - PredicateType: "vuln", - Timeout: 30 * time.Second, - Replace: true, - RekorEntryType: "dsse", + KeyOpts: ko, + PredicatePath: vulnAttestationPath, + PredicateType: "vuln", + Timeout: 30 * time.Second, } must(attestCommand.Exec(ctx, imgName), t) @@ -2458,57 +2355,26 @@ func TestAttestationDownloadWithPredicateType(t *testing.T) { must(download.AttestationCmd(ctx, regOpts, attOpts, imgName, os.Stdout), t) predicateType, _ := options.ParsePredicateType(attOpts.PredicateType) - attestations, err := cosign.FetchAttestationsForReference(ctx, ref, predicateType, ociremoteOpts...) + bundles, _, err := cosign.GetBundles(ctx, ref, ociremoteOpts) if err != nil { t.Fatal(err) } - if len(attestations) != 1 { - t.Fatal(fmt.Errorf("expected len(attestations) == 1, got %d", len(attestations))) - } -} - -func TestAttestationDownloadWithBadPredicateType(t *testing.T) { - repo, stop := reg(t) - defer stop() - td := t.TempDir() - - imgName := path.Join(repo, "cosign-attest-download-bad-type-e2e") - - _, _, cleanup := mkimage(t, imgName) - defer cleanup() - - _, privKeyPath, _ := keypair(t, td) - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} - - ctx := context.Background() - - slsaAttestation := `{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }` - slsaAttestationPath := filepath.Join(td, "attestation.slsa.json") - if err := os.WriteFile(slsaAttestationPath, []byte(slsaAttestation), 0o600); err != nil { - t.Fatal(err) - } - - regOpts := options.RegistryOptions{} - - // Attest to create a slsa attestation - attestCommand := attest.AttestCommand{ - KeyOpts: ko, - PredicatePath: slsaAttestationPath, - PredicateType: "slsaprovenance", - Timeout: 30 * time.Second, - Replace: true, - RekorEntryType: "dsse", + var matched int + for _, b := range bundles { + envelope, err := b.Envelope() + if err == nil && envelope != nil { + statement, err := envelope.Statement() + if err == nil && statement != nil && statement.PredicateType == predicateType { + matched++ + } + } } - must(attestCommand.Exec(ctx, imgName), t) - - // Call download.AttestationCmd() to ensure failure with non-existent --predicate-type - attOpts := options.AttestationDownloadOptions{ - PredicateType: "vuln", + if matched != 1 { + t.Fatal(fmt.Errorf("expected matched == 1, got %d", matched)) } - mustErr(download.AttestationCmd(ctx, regOpts, attOpts, imgName, os.Stdout), t) } -func TestAttestationDownloadWithBadPredicateTypeNewBundle(t *testing.T) { +func TestAttestationDownloadWithBadPredicateType(t *testing.T) { repo, stop := reg(t) defer stop() td := t.TempDir() @@ -2519,9 +2385,8 @@ func TestAttestationDownloadWithBadPredicateTypeNewBundle(t *testing.T) { _, privKeyPath, _ := keypair(t, td) ko := options.KeyOpts{ - KeyRef: privKeyPath, - PassFunc: passFunc, - NewBundleFormat: true, + KeyRef: privKeyPath, + PassFunc: passFunc, } ctx := context.Background() @@ -2533,12 +2398,10 @@ func TestAttestationDownloadWithBadPredicateTypeNewBundle(t *testing.T) { regOpts := options.RegistryOptions{} attestCommand := attest.AttestCommand{ - KeyOpts: ko, - PredicatePath: slsaAttestationPath, - PredicateType: "slsaprovenance", - Timeout: 30 * time.Second, - Replace: true, - RekorEntryType: "dsse", + KeyOpts: ko, + PredicatePath: slsaAttestationPath, + PredicateType: "slsaprovenance", + Timeout: 30 * time.Second, } must(attestCommand.Exec(ctx, imgName), t) @@ -2548,18 +2411,22 @@ func TestAttestationDownloadWithBadPredicateTypeNewBundle(t *testing.T) { mustErr(download.AttestationCmd(ctx, regOpts, attOpts, imgName, os.Stdout), t) } -func TestAttestationReplaceCreate(t *testing.T) { +func TestAttestationRFC3161Timestamp(t *testing.T) { repo, stop := reg(t) defer stop() td := t.TempDir() - imgName := path.Join(repo, "cosign-attest-replace-e2e") + imgName := path.Join(repo, "cosign-attest-timestamp-e2e") _, _, cleanup := mkimage(t, imgName) defer cleanup() - _, privKeyPath, _ := keypair(t, td) - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + _, privKeyPath, pubKeyPath := keypair(t, td) + ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, + KeyRef: privKeyPath, + PassFunc: passFunc, + } ctx := context.Background() @@ -2579,206 +2446,53 @@ func TestAttestationReplaceCreate(t *testing.T) { t.Fatal(err) } - // Attest with replace=true to create an attestation + // Attest with TSA and skipping tlog creating an attestation attestCommand := attest.AttestCommand{ - KeyOpts: ko, - PredicatePath: slsaAttestationPath, - PredicateType: "slsaprovenance", - Timeout: 30 * time.Second, - Replace: true, - RekorEntryType: "dsse", + KeyOpts: ko, + PredicatePath: slsaAttestationPath, + PredicateType: "slsaprovenance", + Timeout: 30 * time.Second, } must(attestCommand.Exec(ctx, imgName), t) // Download and count the attestations - attOpts := options.AttestationDownloadOptions{} - attestations, err := cosign.FetchAttestationsForReference(ctx, ref, attOpts.PredicateType, ociremoteOpts...) + bundles, _, err := cosign.GetBundles(ctx, ref, ociremoteOpts) if err != nil { t.Fatal(err) } - if len(attestations) != 1 { - t.Fatal(fmt.Errorf("expected len(attestations) == 1, got %d", len(attestations))) + if len(bundles) != 1 { + t.Fatal(fmt.Errorf("expected len(bundles) == 1, got %d", len(bundles))) } -} - -func TestAttestationReplace(t *testing.T) { - repo, stop := reg(t) - defer stop() - td := t.TempDir() - - imgName := path.Join(repo, "cosign-attest-replace-e2e") - - _, _, cleanup := mkimage(t, imgName) - defer cleanup() - - _, privKeyPath, _ := keypair(t, td) - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} - ctx := context.Background() + client, err := tsaclient.GetTimestampClient(tsaURL) + if err != nil { + t.Error(err) + } - slsaAttestation := `{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }` - slsaAttestationPath := filepath.Join(td, "attestation.slsa.json") - if err := os.WriteFile(slsaAttestationPath, []byte(slsaAttestation), 0o600); err != nil { - t.Fatal(err) + chain, err := client.Timestamp.GetTimestampCertChain(tsatimestamp.NewGetTimestampCertChainParams()) + if err != nil { + t.Fatalf("unexpected error getting timestamp chain: %v", err) } - ref, err := name.ParseReference(imgName) + file, err := os.CreateTemp(os.TempDir(), "tempfile") if err != nil { - t.Fatal(err) + t.Fatalf("error creating temp file: %v", err) } - regOpts := options.RegistryOptions{} - ociremoteOpts, err := regOpts.ClientOpts(ctx) + defer os.Remove(file.Name()) + _, err = file.WriteString(chain.Payload) if err != nil { - t.Fatal(err) + t.Fatalf("error writing chain payload to temp file: %v", err) } - // Attest once with replace=false creating an attestation - attestCommand := attest.AttestCommand{ - KeyOpts: ko, - PredicatePath: slsaAttestationPath, - PredicateType: "slsaprovenance", - Timeout: 30 * time.Second, - RekorEntryType: "dsse", + verifyAttestation := cliverify.VerifyAttestationCommand{ + KeyRef: pubKeyPath, + IgnoreTlog: true, + PredicateType: "slsaprovenance", + MaxWorkers: 10, } - must(attestCommand.Exec(ctx, imgName), t) - // Download and count the attestations - attOpts := options.AttestationDownloadOptions{} - attestations, err := cosign.FetchAttestationsForReference(ctx, ref, attOpts.PredicateType, ociremoteOpts...) - if err != nil { - t.Fatal(err) - } - if len(attestations) != 1 { - t.Fatal(fmt.Errorf("expected len(attestations) == 1, got %d", len(attestations))) - } - - // Attest again with replace=true, replacing the previous attestation - attestCommand = attest.AttestCommand{ - KeyOpts: ko, - PredicatePath: slsaAttestationPath, - PredicateType: "slsaprovenance", - Replace: true, - Timeout: 30 * time.Second, - RekorEntryType: "dsse", - } - must(attestCommand.Exec(ctx, imgName), t) - attestations, err = cosign.FetchAttestationsForReference(ctx, ref, attOpts.PredicateType, ociremoteOpts...) - // Download and count the attestations - if err != nil { - t.Fatal(err) - } - if len(attestations) != 1 { - t.Fatal(fmt.Errorf("expected len(attestations) == 1, got %d", len(attestations))) - } - - // Attest once more replace=true using a different predicate, to ensure it adds a new attestation - attestCommand = attest.AttestCommand{ - KeyOpts: ko, - PredicatePath: slsaAttestationPath, - PredicateType: "custom", - Replace: true, - Timeout: 30 * time.Second, - RekorEntryType: "dsse", - } - must(attestCommand.Exec(ctx, imgName), t) - - // Download and count the attestations - attestations, err = cosign.FetchAttestationsForReference(ctx, ref, attOpts.PredicateType, ociremoteOpts...) - if err != nil { - t.Fatal(err) - } - if len(attestations) != 2 { - t.Fatal(fmt.Errorf("expected len(attestations) == 2, got %d", len(attestations))) - } -} - -func TestAttestationRFC3161Timestamp(t *testing.T) { - repo, stop := reg(t) - defer stop() - td := t.TempDir() - - imgName := path.Join(repo, "cosign-attest-timestamp-e2e") - - _, _, cleanup := mkimage(t, imgName) - defer cleanup() - - _, privKeyPath, pubKeyPath := keypair(t, td) - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} - - ctx := context.Background() - - slsaAttestation := `{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }` - slsaAttestationPath := filepath.Join(td, "attestation.slsa.json") - if err := os.WriteFile(slsaAttestationPath, []byte(slsaAttestation), 0o600); err != nil { - t.Fatal(err) - } - - ref, err := name.ParseReference(imgName) - if err != nil { - t.Fatal(err) - } - regOpts := options.RegistryOptions{} - ociremoteOpts, err := regOpts.ClientOpts(ctx) - if err != nil { - t.Fatal(err) - } - - // Attest with TSA and skipping tlog creating an attestation - attestCommand := attest.AttestCommand{ - KeyOpts: ko, - PredicatePath: slsaAttestationPath, - PredicateType: "slsaprovenance", - Timeout: 30 * time.Second, - TSAServerURL: tsaURL + "/api/v1/timestamp", - TlogUpload: false, - RekorEntryType: "dsse", - } - must(attestCommand.Exec(ctx, imgName), t) - - // Download and count the attestations - attOpts := options.AttestationDownloadOptions{} - attestations, err := cosign.FetchAttestationsForReference(ctx, ref, attOpts.PredicateType, ociremoteOpts...) - if err != nil { - t.Fatal(err) - } - if len(attestations) != 1 { - t.Fatal(fmt.Errorf("expected len(attestations) == 1, got %d", len(attestations))) - } - - client, err := tsaclient.GetTimestampClient(tsaURL) - if err != nil { - t.Error(err) - } - - chain, err := client.Timestamp.GetTimestampCertChain(tsatimestamp.NewGetTimestampCertChainParams()) - if err != nil { - t.Fatalf("unexpected error getting timestamp chain: %v", err) - } - - file, err := os.CreateTemp(os.TempDir(), "tempfile") - if err != nil { - t.Fatalf("error creating temp file: %v", err) - } - defer os.Remove(file.Name()) - _, err = file.WriteString(chain.Payload) - if err != nil { - t.Fatalf("error writing chain payload to temp file: %v", err) - } - - verifyAttestation := cliverify.VerifyAttestationCommand{ - KeyRef: pubKeyPath, - TSACertChainPath: file.Name(), - IgnoreTlog: true, - PredicateType: "slsaprovenance", - MaxWorkers: 10, - } - - must(verifyAttestation.Exec(ctx, []string{imgName}), t) - - // Ensure it verifies if you default to the new protobuf bundle format - verifyAttestation.NewBundleFormat = true - must(verifyAttestation.Exec(ctx, []string{imgName}), t) -} + must(verifyAttestation.Exec(ctx, []string{imgName}), t) +} func TestAttestationBlobRFC3161Timestamp(t *testing.T) { blob := "someblob" @@ -2786,9 +2500,7 @@ func TestAttestationBlobRFC3161Timestamp(t *testing.T) { predicateType := "slsaprovenance" td := t.TempDir() - t.Cleanup(func() { - os.RemoveAll(td) - }) + must(setLocalEnv(t, td), t) bp := filepath.Join(td, blob) if err := os.WriteFile(bp, []byte(blob), 0o600); err != nil { @@ -2804,21 +2516,22 @@ func TestAttestationBlobRFC3161Timestamp(t *testing.T) { _, privKeyPath, pubKeyPath := keypair(t, td) ctx := context.Background() + signingConfigPath := prepareSigningConfig(t, fulcioURL, rekorURL, "unused", tsaURL+"/api/v1/timestamp") + trustedRootPath := prepareTrustedRootTSA(t, tsaURL) ko := options.KeyOpts{ - KeyRef: privKeyPath, - BundlePath: bundlePath, - NewBundleFormat: true, - TSAServerURL: tsaURL + "/api/v1/timestamp", - PassFunc: passFunc, + KeyRef: privKeyPath, + BundlePath: bundlePath, + PassFunc: passFunc, + SkipConfirmation: true, } + err := signcommon.LoadTrustedMaterialAndSigningConfig(ctx, &ko, false, signingConfigPath, trustedRootPath, privKeyPath) + must(err, t) attestBlobCmd := attest.AttestBlobCommand{ - KeyOpts: ko, - PredicatePath: predicatePath, - PredicateType: predicateType, - Timeout: 30 * time.Second, - TlogUpload: false, - RekorEntryType: "dsse", + KeyOpts: ko, + PredicatePath: predicatePath, + PredicateType: predicateType, + Timeout: 30 * time.Second, } must(attestBlobCmd.Exec(ctx, bp), t) @@ -2855,7 +2568,7 @@ func TestAttestationBlobRFC3161Timestamp(t *testing.T) { t.Error(err) } - trustedRootPath := filepath.Join(td, "trustedroot.json") + trustedRootPath = filepath.Join(td, "trustedroot.json") trustedRootBytes, err := trustedRoot.MarshalJSON() if err != nil { t.Error(err) @@ -2865,9 +2578,8 @@ func TestAttestationBlobRFC3161Timestamp(t *testing.T) { } ko = options.KeyOpts{ - KeyRef: pubKeyPath, - BundlePath: bundlePath, - NewBundleFormat: true, + KeyRef: pubKeyPath, + BundlePath: bundlePath, } verifyBlobAttestation := cliverify.VerifyBlobAttestationCommand{ @@ -2887,53 +2599,41 @@ func TestVerifyWithCARoots(t *testing.T) { repo, stop := reg(t) defer stop() td := t.TempDir() + must(setLocalEnv(t, td), t) - imgName := path.Join(repo, "cosign-verify-caroots-e2e") - _, _, cleanup := mkimage(t, imgName) - defer cleanup() blob := "someblob2sign" - b := bytes.Buffer{} blobRef := filepath.Join(td, blob) if err := os.WriteFile(blobRef, []byte(blob), 0o644); err != nil { t.Fatal(err) } - must(generate.GenerateCmd(context.Background(), options.RegistryOptions{}, imgName, nil, &b), t) rootCert, rootKey, _ := cert_test.GenerateRootCa() subCert, subKey, _ := cert_test.GenerateSubordinateCa(rootCert, rootKey) leafCert, privKey, _ := cert_test.GenerateLeafCert("subject@mail.com", "oidc-issuer", subCert, subKey) - privKeyRef := importECDSAPrivateKey(t, privKey, td, "cosign-test-key.pem") + privKeyPath := importECDSAPrivateKey(t, privKey, td, "cosign-test-key.pem") pemRoot := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert.Raw}) pemSub := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert.Raw}) pemLeaf := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert.Raw}) rootCert02, rootKey02, _ := cert_test.GenerateRootCa() subCert02, subKey02, _ := cert_test.GenerateSubordinateCa(rootCert02, rootKey02) - leafCert02, _, _ := cert_test.GenerateLeafCert("subject02@mail.com", "oidc-issuer02", subCert02, subKey02) + leafCert02, privKey02, _ := cert_test.GenerateLeafCert("subject02@mail.com", "oidc-issuer02", subCert02, subKey02) + privKeyPath02 := importECDSAPrivateKey(t, privKey02, td, "cosign-test-key-02.pem") pemRoot02 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: rootCert02.Raw}) pemSub02 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: subCert02.Raw}) pemLeaf02 := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafCert02.Raw}) pemsubRef02 := mkfile(string(pemSub02), td, t) - pemrootRef02 := mkfile(string(pemRoot02), td, t) pemleafRef02 := mkfile(string(pemLeaf02), td, t) - - rootPool := x509.NewCertPool() - rootPool.AddCert(rootCert) - - payloadref := mkfile(b.String(), td, t) - - h := sha256.Sum256(b.Bytes()) - signature, _ := privKey.Sign(rand.Reader, h[:], crypto.SHA256) - b64signature := base64.StdEncoding.EncodeToString(signature) - sigRef := mkfile(b64signature, td, t) pemsubRef := mkfile(string(pemSub), td, t) pemrootRef := mkfile(string(pemRoot), td, t) pemleafRef := mkfile(string(pemLeaf), td, t) - certchainRef := mkfile(string(append(pemSub, pemRoot...)), td, t) - pemrootBundleRef := mkfile(string(append(pemRoot, pemRoot02...)), td, t) - pemsubBundleRef := mkfile(string(append(pemSub, pemSub02...)), td, t) + pemchainRef := mkfile(string(append(pemSub, pemRoot...)), td, t) + pemchainRef02 := mkfile(string(append(pemSub02, pemRoot02...)), td, t) + pemswitchedChainRef := mkfile(string(append(pemRoot, pemSub...)), td, t) + pemwrongRootChainRef := mkfile(string(append(pemSub, pemRoot02...)), td, t) + pemwrongIntermediatesChainRef := mkfile(string(append(pemSub02, pemRoot...)), td, t) tsclient, err := tsaclient.GetTimestampClient(tsaURL) if err != nil { @@ -2945,169 +2645,178 @@ func TestVerifyWithCARoots(t *testing.T) { t.Fatalf("unexpected error getting timestamp chain: %v", err) } - tsaChainRef, err := os.CreateTemp(os.TempDir(), "tempfile") - if err != nil { - t.Fatalf("error creating temp file: %v", err) - } - defer os.Remove(tsaChainRef.Name()) - _, err = tsaChainRef.WriteString(chain.Payload) - if err != nil { - t.Fatalf("error writing chain payload to temp file: %v", err) - } - - tsBytes, err := getTimestampedSignature(signature, client.NewTSAClient(tsaURL+"/api/v1/timestamp")) - if err != nil { - t.Fatalf("unexpected error creating timestamp: %v", err) + tsaChainRef := filepath.Join(td, "tsa-chain.pem") + if err := os.WriteFile(tsaChainRef, []byte(chain.Payload), 0o644); err != nil { + t.Fatalf("error writing TSA chain: %v", err) } - rfc3161TSRef := mkfile(string(tsBytes), td, t) - // Upload it! - err = attach.SignatureCmd(ctx, options.RegistryOptions{}, sigRef, payloadref, pemleafRef, certchainRef, rfc3161TSRef, "", imgName) - if err != nil { - t.Fatal(err) - } + rekorPubKey := os.Getenv("SIGSTORE_REKOR_PUBLIC_KEY") + ctfePubKey := os.Getenv("SIGSTORE_CT_LOG_PUBLIC_KEY_FILE") - // Now sign the blob with one key - ko := options.KeyOpts{ - KeyRef: privKeyRef, - PassFunc: passFunc, - } - blobSig, err := sign.SignBlobCmd(ctx, ro, ko, blobRef, "", "", true, "", "", false) - if err != nil { - t.Fatal(err) - } // the following fields with non-changing values are logically "factored out" for brevity - // and passed to verifyKeylessTSAWithCARoots in the testing loop: - // imageName string - // tsaCertChainRef string - // skipSCT bool - // skipTlogVerify bool + // and used to construct the trusted root in the testing loop: + // tsaChainRef string + // rekorPubKey string + // ctfePubKey string tests := []struct { name string - rootRef string - subRef string - leafRef string + chains []string + signKey string + signCert string + signChain string skipBlob bool // skip the verify-blob test (for cases that need the image) wantError bool }{ { "verify with root, intermediate and leaf certificates", - pemrootRef, - pemsubRef, + []string{pemchainRef}, + privKeyPath, pemleafRef, + pemsubRef, false, false, }, - // NB - "confusely" switching the root and intermediate PEM files does _NOT_ (currently) produce an error - // - the Go crypto/x509 package doesn't strictly verify that the certificate chain is anchored - // in a self-signed root certificate. In this case, only the chain up to the intermediate - // certificate is verified, and the root certificate is ignored. - // See also https://gist.github.com/dmitris/15160f703b3038b1b00d03d3c7b66ce0 and in particular - // https://gist.github.com/dmitris/15160f703b3038b1b00d03d3c7b66ce0#file-main-go-L133-L135 as an example. { "switch root and intermediate no error", - pemsubRef, - pemrootRef, + []string{pemswitchedChainRef}, + privKeyPath, pemleafRef, + pemsubRef, false, false, }, { "leave out the root certificate", - "", - pemsubRef, + []string{}, // no trusted CAs at all + privKeyPath, pemleafRef, + pemsubRef, false, true, }, { "leave out the intermediate certificate", - pemrootRef, - "", + []string{pemrootRef}, + privKeyPath, pemleafRef, + "", false, true, }, { "leave out the codesigning leaf certificate which is extracted from the image", - pemrootRef, + []string{pemchainRef}, + privKeyPath, + pemleafRef, pemsubRef, - "", true, false, }, { "wrong leaf certificate", - pemrootRef, - pemsubRef, + []string{pemchainRef}, + privKeyPath02, pemleafRef02, + pemsubRef02, false, true, }, { "root and intermediates bundles", - pemrootBundleRef, - pemsubBundleRef, + []string{pemchainRef, pemchainRef02}, + privKeyPath, pemleafRef, + pemsubRef, false, false, }, { "wrong root and intermediates bundles", - pemrootRef02, - pemsubRef02, + []string{pemchainRef02}, + privKeyPath, pemleafRef, + pemsubRef, false, true, }, { "wrong root bundle", - pemrootRef02, - pemsubBundleRef, + []string{pemwrongRootChainRef}, + privKeyPath, pemleafRef, + pemsubRef, false, true, }, { "wrong intermediates bundle", - pemrootRef, - pemsubRef02, + []string{pemwrongIntermediatesChainRef}, + privKeyPath, pemleafRef, + "", false, true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - err := verifyKeylessTSAWithCARoots(imgName, - tt.rootRef, - tt.subRef, - tt.leafRef, - tsaChainRef.Name(), - true, - true) + imgName := path.Join(repo, strings.ReplaceAll(strings.ReplaceAll(tt.name, " ", "-"), ",", "")) + _, _, cleanup := mkimage(t, imgName) + defer cleanup() + + ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, + KeyRef: tt.signKey, + PassFunc: passFunc, + SkipConfirmation: true, + } + so := options.SignOptions{ + Upload: true, + Cert: tt.signCert, + CertChain: tt.signChain, + } + must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) + + trustedRootPath := filepath.Join(t.TempDir(), "trusted-root.json") + createCmd := trustedroot.CreateCmd{ + CertChain: tt.chains, + Out: trustedRootPath, + RekorKeyPath: []string{rekorPubKey}, + CtfeKeyPath: []string{ctfePubKey}, + TSACertChainPath: []string{tsaChainRef}, + } + if err := createCmd.Exec(ctx); err != nil { + t.Fatalf("creating trusted root: %v", err) + } + + err = verifyWithTrustedRoot(imgName, trustedRootPath) hasErr := (err != nil) if hasErr != tt.wantError { if tt.wantError { - t.Errorf("%s - no expected error", tt.name) + t.Errorf("image: %s - no expected error", tt.name) } else { - t.Errorf("%s - unexpected error: %v", tt.name, err) + t.Errorf("image: %s - unexpected error: %v", tt.name, err) } } + if !tt.skipBlob { - err = verifyBlobKeylessWithCARoots(blobRef, - string(blobSig), - tt.rootRef, - tt.subRef, - tt.leafRef, - true, - true) + bundlePath := filepath.Join(t.TempDir(), "blob.bundle") + koBlob := options.KeyOpts{ + SigningConfig: rekorSigningConfig, + KeyRef: tt.signKey, + PassFunc: passFunc, + SkipConfirmation: true, + BundlePath: bundlePath, + } + must(sign.SignBlobCmd(ctx, ro, koBlob, blobRef, tt.signCert, tt.signChain), t) + + err = verifyBlobWithTrustedRoot(blobRef, bundlePath, trustedRootPath) hasErr = (err != nil) if hasErr != tt.wantError { if tt.wantError { - t.Errorf("%s - no expected error", tt.name) + t.Errorf("blob: %s - no expected error", tt.name) } else { - t.Errorf("%s - unexpected error: %v", tt.name, err) + t.Errorf("blob: %s - unexpected error: %v", tt.name, err) } } } @@ -3133,23 +2842,19 @@ func TestRekorBundle(t *testing.T) { _, privKeyPath, pubKeyPath := keypair(t, td) ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKeyPath, PassFunc: passFunc, - RekorURL: rekorURL, SkipConfirmation: true, } so := options.SignOptions{ - Upload: true, - TlogUpload: true, + Upload: true, } // Sign the image must(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) // Make sure verify works - must(verify(pubKeyPath, imgName, true, nil, "", false), t) - - // Make sure offline verification works with bundling - must(verifyOffline(pubKeyPath, imgName, true, nil, ""), t) + must(verify(pubKeyPath, imgName, true, nil, false), t) } func TestRekorOutput(t *testing.T) { @@ -3171,31 +2876,29 @@ func TestRekorOutput(t *testing.T) { _, privKeyPath, pubKeyPath := keypair(t, td) ko := options.KeyOpts{ - KeyRef: privKeyPath, - PassFunc: passFunc, - RekorURL: rekorURL, - BundlePath: bundlePath, + SigningConfig: rekorSigningConfig, + KeyRef: privKeyPath, + PassFunc: passFunc, + SkipConfirmation: true, } so := options.SignOptions{ Upload: true, - TlogUpload: true, + BundlePath: bundlePath, } // Sign the image must(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) // Make sure verify works - must(verify(pubKeyPath, imgName, true, nil, "", false), t) + must(verify(pubKeyPath, imgName, true, nil, false), t) if file, err := os.ReadFile(bundlePath); err != nil { t.Fatal(err) } else { - var localCosignPayload cosign.LocalSignedPayload - if err := json.Unmarshal(file, &localCosignPayload); err != nil { + var pb protobundle.Bundle + if err := protojson.Unmarshal(file, &pb); err != nil { t.Fatal(err) } } - // Make sure offline verification works with bundling - must(verifyOffline(pubKeyPath, imgName, true, nil, ""), t) } func TestFulcioBundle(t *testing.T) { @@ -3216,26 +2919,19 @@ func TestFulcioBundle(t *testing.T) { _, privKeyPath, pubKeyPath := keypair(t, td) ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKeyPath, PassFunc: passFunc, - RekorURL: rekorURL, - FulcioURL: fulcioURL, SkipConfirmation: true, } so := options.SignOptions{ - Upload: true, - TlogUpload: true, - IssueCertificate: true, + Upload: true, } // Sign the image must(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) // Make sure verify works - must(verify(pubKeyPath, imgName, true, nil, "", false), t) - - // Make sure offline verification works with bundling - // use rekor prod since we have hardcoded the public key - must(verifyOffline(pubKeyPath, imgName, true, nil, ""), t) + must(verify(pubKeyPath, imgName, true, nil, false), t) } func TestRFC3161Timestamp(t *testing.T) { @@ -3271,19 +2967,18 @@ func TestRFC3161Timestamp(t *testing.T) { _, privKeyPath, pubKeyPath := keypair(t, td) ko := options.KeyOpts{ - KeyRef: privKeyPath, - PassFunc: passFunc, - TSAServerURL: tsaURL + "/api/v1/timestamp", + SigningConfig: rekorSigningConfig, + KeyRef: privKeyPath, + PassFunc: passFunc, } so := options.SignOptions{ - Upload: true, - TlogUpload: false, + Upload: true, } // Sign the image must(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) // Make sure verify works against the TSA server - must(verifyTSA(pubKeyPath, imgName, true, nil, "", file.Name(), true), t) + must(verifyTSA(pubKeyPath, imgName, true, nil, true), t) } func TestRekorBundleAndRFC3161Timestamp(t *testing.T) { @@ -3324,21 +3019,19 @@ func TestRekorBundleAndRFC3161Timestamp(t *testing.T) { _, privKeyPath, pubKeyPath := keypair(t, td) ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKeyPath, PassFunc: passFunc, - TSAServerURL: tsaURL + "/api/v1/timestamp", - RekorURL: rekorURL, SkipConfirmation: true, } so := options.SignOptions{ - Upload: true, - TlogUpload: true, + Upload: true, } // Sign the image must(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) // Make sure verify works against the Rekor and TSA clients - must(verifyTSA(pubKeyPath, imgName, true, nil, "", file.Name(), false), t) + must(verifyTSA(pubKeyPath, imgName, true, nil, false), t) } func TestAttachWithRFC3161Timestamp(t *testing.T) { @@ -3408,7 +3101,8 @@ func TestAttachWithRFC3161Timestamp(t *testing.T) { t.Fatal(err) } - must(verifyKeylessTSA(imgName, file.Name(), pemrootRef, true, true), t) + // TODO: Re-enable verification if/once attach.SignatureCmd uploads signatures in the new bundle format. + // must(verifyKeylessTSA(imgName, pemrootRef, true, true), t) } func TestDuplicateSign(t *testing.T) { @@ -3430,14 +3124,16 @@ func TestDuplicateSign(t *testing.T) { ctx := context.Background() // Verify should fail at first - mustErr(verify(pubKeyPath, imgName, true, nil, "", true), t) + mustErr(verify(pubKeyPath, imgName, true, nil, true), t) // So should download mustErr(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName, os.Stdout), t) // Now sign the image ko := options.KeyOpts{ - KeyRef: privKeyPath, - PassFunc: passFunc, + SigningConfig: rekorSigningConfig, + KeyRef: privKeyPath, + PassFunc: passFunc, + SkipConfirmation: true, } so := options.SignOptions{ Upload: true, @@ -3446,7 +3142,7 @@ func TestDuplicateSign(t *testing.T) { // Now verify and download should work! // Ignore the tlog, because uploading to the tlog causes new signatures with new timestamp entries to be appended. - must(verify(pubKeyPath, imgName, true, nil, "", true), t) + must(verify(pubKeyPath, imgName, true, nil, true), t) must(download.SignatureCmd(ctx, options.RegistryOptions{}, imgName, os.Stdout), t) // Signing again should work just fine... @@ -3471,7 +3167,7 @@ func TestKeyURLVerify(t *testing.T) { keyRef := "https://raw.githubusercontent.com/GoogleContainerTools/distroless/main/cosign.pub" img := "gcr.io/distroless/base:latest" - must(verify(keyRef, img, true, nil, "", false), t) + must(verify(keyRef, img, true, nil, false), t) } func TestGenerateKeyPairEnvVar(t *testing.T) { @@ -3552,146 +3248,34 @@ func TestMultipleSignatures(t *testing.T) { _, priv2, pub2 := keypair(t, td2) // Verify should fail at first for both keys - mustErr(verify(pub1, imgName, true, nil, "", false), t) - mustErr(verify(pub2, imgName, true, nil, "", false), t) + mustErr(verify(pub1, imgName, true, nil, false), t) + mustErr(verify(pub2, imgName, true, nil, false), t) // Now sign the image with one key ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: priv1, PassFunc: passFunc, - RekorURL: rekorURL, SkipConfirmation: true, } so := options.SignOptions{ - Upload: true, - TlogUpload: true, + Upload: true, } must(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) // Now verify should work with that one, but not the other - must(verify(pub1, imgName, true, nil, "", false), t) - mustErr(verify(pub2, imgName, true, nil, "", false), t) + must(verify(pub1, imgName, true, nil, false), t) + mustErr(verify(pub2, imgName, true, nil, false), t) // Now sign with the other key too ko.KeyRef = priv2 must(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) // Now verify should work with both - must(verify(pub1, imgName, true, nil, "", false), t) - must(verify(pub2, imgName, true, nil, "", false), t) + must(verify(pub1, imgName, true, nil, false), t) + must(verify(pub2, imgName, true, nil, false), t) } func TestSignBlob(t *testing.T) { - td := t.TempDir() - err := downloadAndSetEnv(t, rekorURL+"/api/v1/log/publicKey", env.VariableSigstoreRekorPublicKey.String(), td) - if err != nil { - t.Fatal(err) - } - blob := "someblob" - td1 := t.TempDir() - td2 := t.TempDir() - bp := filepath.Join(td1, blob) - - if err := os.WriteFile(bp, []byte(blob), 0o644); err != nil { - t.Fatal(err) - } - - _, privKeyPath1, pubKeyPath1 := keypair(t, td1) - _, _, pubKeyPath2 := keypair(t, td2) - - ctx := context.Background() - - ko1 := options.KeyOpts{ - KeyRef: pubKeyPath1, - } - ko2 := options.KeyOpts{ - KeyRef: pubKeyPath2, - } - // Verify should fail on a bad input - cmd1 := cliverify.VerifyBlobCmd{ - KeyOpts: ko1, - SigRef: "badsig", - IgnoreTlog: true, - } - cmd2 := cliverify.VerifyBlobCmd{ - KeyOpts: ko2, - SigRef: "badsig", - IgnoreTlog: true, - } - mustErr(cmd1.Exec(ctx, blob), t) - mustErr(cmd2.Exec(ctx, blob), t) - - // Now sign the blob with one key - ko := options.KeyOpts{ - KeyRef: privKeyPath1, - PassFunc: passFunc, - } - sig, err := sign.SignBlobCmd(ctx, ro, ko, bp, "", "", true, "", "", false) - if err != nil { - t.Fatal(err) - } - // Now verify should work with that one, but not the other - cmd1.SigRef = string(sig) - cmd2.SigRef = string(sig) - must(cmd1.Exec(ctx, bp), t) - mustErr(cmd2.Exec(ctx, bp), t) -} - -func TestSignBlobBundle(t *testing.T) { - blob := "someblob" - td1 := t.TempDir() - bp := filepath.Join(td1, blob) - bundlePath := filepath.Join(td1, "bundle.sig") - - if err := os.WriteFile(bp, []byte(blob), 0o644); err != nil { - t.Fatal(err) - } - - err := downloadAndSetEnv(t, rekorURL+"/api/v1/log/publicKey", env.VariableSigstoreRekorPublicKey.String(), td1) - if err != nil { - t.Fatal(err) - } - - _, privKeyPath1, pubKeyPath1 := keypair(t, td1) - - ctx := context.Background() - - ko1 := options.KeyOpts{ - KeyRef: pubKeyPath1, - BundlePath: bundlePath, - } - // Verify should fail on a bad input - verifyBlobCmd := cliverify.VerifyBlobCmd{ - KeyOpts: ko1, - IgnoreTlog: true, - } - mustErr(verifyBlobCmd.Exec(ctx, bp), t) - - // Now sign the blob with one key - ko := options.KeyOpts{ - KeyRef: privKeyPath1, - PassFunc: passFunc, - BundlePath: bundlePath, - RekorURL: rekorURL, - SkipConfirmation: true, - } - if _, err := sign.SignBlobCmd(ctx, ro, ko, bp, "", "", true, "", "", false); err != nil { - t.Fatal(err) - } - // Now verify should work - must(verifyBlobCmd.Exec(ctx, bp), t) - - // Now we turn on the tlog and sign again - if _, err := sign.SignBlobCmd(ctx, ro, ko, bp, "", "", true, "", "", true); err != nil { - t.Fatal(err) - } - - // Point to a fake rekor server to make sure offline verification of the tlog entry works - verifyBlobCmd.RekorURL = "notreal" - verifyBlobCmd.IgnoreTlog = false - must(verifyBlobCmd.Exec(ctx, bp), t) -} - -func TestSignBlobNewBundle(t *testing.T) { td1 := t.TempDir() blob := "someblob" @@ -3706,9 +3290,8 @@ func TestSignBlobNewBundle(t *testing.T) { _, privKeyPath, pubKeyPath := keypair(t, td1) ko1 := options.KeyOpts{ - KeyRef: pubKeyPath, - BundlePath: bundlePath, - NewBundleFormat: true, + KeyRef: pubKeyPath, + BundlePath: bundlePath, } verifyBlobCmd := cliverify.VerifyBlobCmd{ @@ -3721,21 +3304,33 @@ func TestSignBlobNewBundle(t *testing.T) { // Produce signed bundle ko := options.KeyOpts{ - KeyRef: privKeyPath, - PassFunc: passFunc, - BundlePath: bundlePath, - NewBundleFormat: true, + SigningConfig: rekorSigningConfig, + KeyRef: privKeyPath, + PassFunc: passFunc, + BundlePath: bundlePath, } - if _, err := sign.SignBlobCmd(ctx, ro, ko, blobPath, "", "", true, "", "", false); err != nil { + if err := sign.SignBlobCmd(ctx, ro, ko, blobPath, "", ""); err != nil { t.Fatal(err) } // Verify should succeed now that bundle is written must(verifyBlobCmd.Exec(ctx, blobPath), t) + + // Verify should fail with a different public key + _, _, pubKeyPath2 := keypair(t, td1) + ko2 := options.KeyOpts{ + KeyRef: pubKeyPath2, + BundlePath: bundlePath, + } + verifyBlobCmd2 := cliverify.VerifyBlobCmd{ + KeyOpts: ko2, + IgnoreTlog: true, + } + mustErr(verifyBlobCmd2.Exec(ctx, blobPath), t) } -func TestSignBlobNewBundleNonSHA256(t *testing.T) { +func TestSignBlobNonSHA256(t *testing.T) { td1 := t.TempDir() blob := "someblob" @@ -3752,29 +3347,27 @@ func TestSignBlobNewBundleNonSHA256(t *testing.T) { _, privKeyPath, pubKeyPath := keypairWithAlgorithm(t, td1, v1.PublicKeyDetails_PKIX_ECDSA_P521_SHA_512) ko := options.KeyOpts{ - KeyRef: privKeyPath, - PassFunc: passFunc, - BundlePath: bundlePath, - NewBundleFormat: true, + SigningConfig: rekorSigningConfig, + KeyRef: privKeyPath, + PassFunc: passFunc, + BundlePath: bundlePath, } - if _, err := sign.SignBlobCmd(ctx, ro, ko, blobPath, "", "", true, "", "", false); err != nil { + if err := sign.SignBlobCmd(ctx, ro, ko, blobPath, "", ""); err != nil { t.Fatal(err) } ko1 := options.KeyOpts{ - KeyRef: pubKeyPath, - BundlePath: bundlePath, - NewBundleFormat: true, + KeyRef: pubKeyPath, + BundlePath: bundlePath, } verifyBlobCmd := cliverify.VerifyBlobCmd{ - KeyOpts: ko1, - IgnoreTlog: true, - HashAlgorithm: crypto.SHA512, + KeyOpts: ko1, + IgnoreTlog: true, } must(verifyBlobCmd.Exec(ctx, blobPath), t) } -func TestSignBlobNewBundleNonDefaultAlgorithm(t *testing.T) { +func TestSignBlobNonDefaultAlgorithm(t *testing.T) { tts := []struct { algo v1.PublicKeyDetails }{ @@ -3844,11 +3437,8 @@ func TestSignBlobNewBundleNonDefaultAlgorithm(t *testing.T) { verifyBlobCmd := cliverify.VerifyBlobCmd{ TrustedRootPath: trustedRootPath, KeyOpts: options.KeyOpts{ - FulcioURL: fulcioURL, - RekorURL: rekorURL, PassFunc: passFunc, BundlePath: bundlePath, - NewBundleFormat: true, SkipConfirmation: true, }, CertVerifyOptions: options.CertVerifyOptions{ @@ -3862,18 +3452,16 @@ func TestSignBlobNewBundleNonDefaultAlgorithm(t *testing.T) { // Produce signed bundle ko := options.KeyOpts{ - FulcioURL: fulcioURL, - RekorURL: rekorURL, + SigningConfig: rekorSigningConfig, IDToken: identityToken, KeyRef: privKeyPath, PassFunc: passFunc, BundlePath: bundlePath, - NewBundleFormat: true, IssueCertificateForExistingKey: true, SkipConfirmation: true, } - if _, err := sign.SignBlobCmd(ctx, ro, ko, blobPath, "", "", true, "", "", true); err != nil { + if err := sign.SignBlobCmd(ctx, ro, ko, blobPath, "", ""); err != nil { t.Fatal(err) } @@ -3893,22 +3481,38 @@ func TestSignBlobNewBundleNonDefaultAlgorithm(t *testing.T) { } } -func TestSignBlobRFC3161TimestampBundle(t *testing.T) { +func TestSignBlobRFC3161Timestamp(t *testing.T) { td := t.TempDir() - err := downloadAndSetEnv(t, rekorURL+"/api/v1/log/publicKey", env.VariableSigstoreRekorPublicKey.String(), td) - if err != nil { - t.Fatal(err) - } + must(setLocalEnv(t, td), t) blob := "someblob" bp := filepath.Join(td, blob) - bundlePath := filepath.Join(td, "bundle.sig") - tsPath := filepath.Join(td, "rfc3161Timestamp.json") + bundlePath := filepath.Join(td, "bundle.sigstore.json") + + if err := os.WriteFile(bp, []byte(blob), 0o644); err != nil { + t.Fatal(err) + } + + _, privKeyPath, pubKeyPath := keypair(t, td) + ctx := context.Background() + + signingConfigPath := prepareSigningConfig(t, fulcioURL, rekorURL, "unused", tsaURL+"/api/v1/timestamp") + trustedRootPath := prepareTrustedRootTSA(t, tsaURL) + ko := options.KeyOpts{ + KeyRef: privKeyPath, + BundlePath: bundlePath, + PassFunc: passFunc, + SkipConfirmation: true, + } + err := signcommon.LoadTrustedMaterialAndSigningConfig(ctx, &ko, false, signingConfigPath, trustedRootPath, privKeyPath) + must(err, t) - if err := os.WriteFile(bp, []byte(blob), 0o644); err != nil { + // Sign the blob + if err := sign.SignBlobCmd(ctx, ro, ko, bp, "", ""); err != nil { t.Fatal(err) } + // Build the trusted root with TSA CA client, err := tsaclient.GetTimestampClient(tsaURL) if err != nil { t.Error(err) @@ -3919,56 +3523,45 @@ func TestSignBlobRFC3161TimestampBundle(t *testing.T) { t.Fatalf("unexpected error getting timestamp chain: %v", err) } - file, err := os.CreateTemp(os.TempDir(), "tempfile") - if err != nil { - t.Fatalf("error creating temp file: %v", err) + var certs []*x509.Certificate + for block, contents := pem.Decode([]byte(chain.Payload)); ; block, contents = pem.Decode(contents) { + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Error(err) + } + certs = append(certs, cert) + if len(contents) == 0 { + break + } } - defer os.Remove(file.Name()) - _, err = file.WriteString(chain.Payload) - if err != nil { - t.Fatalf("error writing chain payload to temp file: %v", err) + tsaCA := &root.SigstoreTimestampingAuthority{ + Root: certs[len(certs)-1], + Intermediates: certs[:len(certs)-1], } - _, privKeyPath1, pubKeyPath1 := keypair(t, td) - - ctx := context.Background() - - ko1 := options.KeyOpts{ - KeyRef: pubKeyPath1, - BundlePath: bundlePath, - RFC3161TimestampPath: tsPath, - TSACertChainPath: file.Name(), + trustedRoot, err := root.NewTrustedRoot(root.TrustedRootMediaType01, nil, nil, []root.TimestampingAuthority{tsaCA}, nil) + if err != nil { + t.Error(err) } - // Verify should fail on a bad input - verifyBlobCmd := cliverify.VerifyBlobCmd{ - KeyOpts: ko1, - IgnoreTlog: true, + trustedRootPath = filepath.Join(td, "trustedroot.json") + trustedRootBytes, err := trustedRoot.MarshalJSON() + if err != nil { + t.Error(err) } - mustErr(verifyBlobCmd.Exec(ctx, bp), t) - - // Now sign the blob with one key - ko := options.KeyOpts{ - KeyRef: privKeyPath1, - PassFunc: passFunc, - BundlePath: bundlePath, - RFC3161TimestampPath: tsPath, - TSAServerURL: tsaURL + "/api/v1/timestamp", - RekorURL: rekorURL, - SkipConfirmation: true, - } - if _, err := sign.SignBlobCmd(ctx, ro, ko, bp, "", "", true, "", "", false); err != nil { + if err := os.WriteFile(trustedRootPath, trustedRootBytes, 0o600); err != nil { t.Fatal(err) } - // Now verify should work - must(verifyBlobCmd.Exec(ctx, bp), t) - // Now we turn on the tlog and sign again - if _, err := sign.SignBlobCmd(ctx, ro, ko, bp, "", "", true, "", "", true); err != nil { - t.Fatal(err) + // Verify the blob with the trusted root containing the TSA CA + koVerify := options.KeyOpts{ + KeyRef: pubKeyPath, + BundlePath: bundlePath, + } + verifyBlobCmd := cliverify.VerifyBlobCmd{ + KeyOpts: koVerify, + IgnoreTlog: true, + TrustedRootPath: trustedRootPath, } - // Point to a fake rekor server to make sure offline verification of the tlog entry works - verifyBlobCmd.RekorURL = "notreal" - verifyBlobCmd.IgnoreTlog = false must(verifyBlobCmd.Exec(ctx, bp), t) } @@ -4041,15 +3634,13 @@ func TestSaveLoad(t *testing.T) { ctx := context.Background() // Now sign the image and verify it ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKeyPath, PassFunc: passFunc, - RekorURL: rekorURL, SkipConfirmation: true, } so := options.SignOptions{ - Upload: true, - TlogUpload: true, - NewBundleFormat: test.newBundle, + Upload: true, } must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) trustedRootPath := prepareTrustedRoot(t, "") @@ -4058,14 +3649,13 @@ func TestSaveLoad(t *testing.T) { TrustedRootPath: trustedRootPath, }, KeyRef: pubKeyPath, - NewBundleFormat: true, UseSignedTimestamps: false, } if test.newBundle { must(bundleVerifyCmd.Exec(ctx, []string{imgName}), t) } else { - must(verify(pubKeyPath, imgName, true, nil, "", false), t) + must(verify(pubKeyPath, imgName, true, nil, false), t) } // save the image to a temp dir @@ -4075,7 +3665,7 @@ func TestSaveLoad(t *testing.T) { // verify the local image using a local key // if we are not using protobuf bundle format if !test.newBundle { - must(verifyLocal(pubKeyPath, imageDir, true, nil, ""), t) + must(verifyLocal(pubKeyPath, imageDir, true, nil), t) } // load the image from the temp dir into a new image and verify the new image @@ -4084,7 +3674,7 @@ func TestSaveLoad(t *testing.T) { if test.newBundle { must(bundleVerifyCmd.Exec(ctx, []string{imgName2}), t) } else { - must(verify(pubKeyPath, imgName2, true, nil, "", false), t) + must(verify(pubKeyPath, imgName2, true, nil, false), t) } }) } @@ -4103,6 +3693,8 @@ func TestSaveLoadAutoDetectFormat(t *testing.T) { // Test v2 attached signatures - this is the main use case for #4621 // where users have v2 signatures but cosign v3 defaults to --new-bundle-format=true t.Run("auto-detect v2 attached signatures", func(t *testing.T) { + // TODO: v2 attached signatures cannot be tested if all signing uses the new bundle format. + t.Skip() repo, stop := reg(t) defer stop() keysDir := t.TempDir() @@ -4117,15 +3709,13 @@ func TestSaveLoadAutoDetectFormat(t *testing.T) { ctx := context.Background() // Sign the image with v2 format (no bundle) ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKeyPath, PassFunc: passFunc, - RekorURL: rekorURL, SkipConfirmation: true, } so := options.SignOptions{ - Upload: true, - TlogUpload: true, - NewBundleFormat: false, // v2 format + Upload: true, } must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) @@ -4139,7 +3729,6 @@ func TestSaveLoadAutoDetectFormat(t *testing.T) { KeyRef: pubKeyPath, LocalImage: true, MaxWorkers: 10, - // Explicitly NOT setting NewBundleFormat - should auto-detect as v2 } must(verifyCmd.Exec(ctx, []string{imageDir}), t) }) @@ -4162,15 +3751,13 @@ func TestSaveLoadAutoDetectFormat(t *testing.T) { ctx := context.Background() // Sign the image with v3 format (bundle) ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKeyPath, PassFunc: passFunc, - RekorURL: rekorURL, SkipConfirmation: true, } so := options.SignOptions{ - Upload: true, - TlogUpload: true, - NewBundleFormat: true, // v3 format + Upload: true, } must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) @@ -4178,7 +3765,6 @@ func TestSaveLoadAutoDetectFormat(t *testing.T) { imageDir := t.TempDir() must(cli.SaveCmd(ctx, options.SaveOptions{Directory: imageDir}, imgName), t) - // Verify locally with NewBundleFormat enabled trustedRootPath := prepareTrustedRoot(t, "") verifyCmd := cliverify.VerifyCommand{ CommonVerifyOptions: options.CommonVerifyOptions{ @@ -4186,7 +3772,6 @@ func TestSaveLoadAutoDetectFormat(t *testing.T) { }, KeyRef: pubKeyPath, LocalImage: true, - NewBundleFormat: true, UseSignedTimestamps: false, MaxWorkers: 10, } @@ -4214,17 +3799,16 @@ func TestSaveLoadAttestation(t *testing.T) { ctx := context.Background() // Now sign the image and verify it ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKeyPath, PassFunc: passFunc, - RekorURL: rekorURL, SkipConfirmation: true, } so := options.SignOptions{ - Upload: true, - TlogUpload: true, + Upload: true, } must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) - must(verify(pubKeyPath, imgName, true, nil, "", false), t) + must(verify(pubKeyPath, imgName, true, nil, false), t) // now, append an attestation to the image slsaAttestation := `{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }` @@ -4234,13 +3818,16 @@ func TestSaveLoadAttestation(t *testing.T) { } // Now attest the image - ko = options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko = options.KeyOpts{ + SigningConfig: rekorSigningConfig, + KeyRef: privKeyPath, + PassFunc: passFunc, + } attestCommand := attest.AttestCommand{ - KeyOpts: ko, - PredicatePath: slsaAttestationPath, - PredicateType: "slsaprovenance", - Timeout: 30 * time.Second, - RekorEntryType: "dsse", + KeyOpts: ko, + PredicatePath: slsaAttestationPath, + PredicateType: "slsaprovenance", + Timeout: 30 * time.Second, } must(attestCommand.Exec(ctx, imgName), t) @@ -4251,7 +3838,7 @@ func TestSaveLoadAttestation(t *testing.T) { // load the image from the temp dir into a new image and verify the new image imgName2 := path.Join(repo, "save-load-2") must(cli.LoadCmd(ctx, options.LoadOptions{Directory: imageDir}, imgName2), t) - must(verify(pubKeyPath, imgName2, true, nil, "", false), t) + must(verify(pubKeyPath, imgName2, true, nil, false), t) // Use cue to verify attestation on the new image policyPath := filepath.Join(td, "policy.cue") verifyAttestation := cliverify.VerifyAttestationCommand{ @@ -4291,7 +3878,6 @@ func TestAttestDownloadAttachNewBundle(t *testing.T) { // Attest first image td := t.TempDir() _, privKeyPath, _ := keypair(t, td) - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc, NewBundleFormat: true} slsaAttestation := `{ "buildType": "x", "builder": { "id": "2" }, "recipe": {} }` slsaAttestationPath := filepath.Join(td, "attestation.slsa.json") @@ -4300,10 +3886,13 @@ func TestAttestDownloadAttachNewBundle(t *testing.T) { } attestCommand := attest.AttestCommand{ - KeyOpts: ko, - PredicatePath: slsaAttestationPath, - PredicateType: "slsaprovenance", - RekorEntryType: "dsse", + KeyOpts: options.KeyOpts{ + SigningConfig: rekorSigningConfig, + KeyRef: privKeyPath, + PassFunc: passFunc, + }, + PredicatePath: slsaAttestationPath, + PredicateType: "slsaprovenance", } must(attestCommand.Exec(ctx, imgName), t) @@ -4347,10 +3936,13 @@ func TestSignDownloadAttachNewBundle(t *testing.T) { // Sign first image td := t.TempDir() _, privKeyPath, _ := keypair(t, td) - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, + KeyRef: privKeyPath, + PassFunc: passFunc, + } so := options.SignOptions{ - NewBundleFormat: true, - Upload: true, + Upload: true, } must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) @@ -4429,25 +4021,23 @@ func TestAttachSBOM(t *testing.T) { _, _, pubKeyPath2 := keypair(t, td2) // Verify should fail on a bad input - mustErr(verify(pubKeyPath1, imgName, true, nil, "sbom", false), t) - mustErr(verify(pubKeyPath2, imgName, true, nil, "sbom", false), t) + mustErr(verify(pubKeyPath1, imgName, true, nil, false), t) + mustErr(verify(pubKeyPath2, imgName, true, nil, false), t) // Now sign the sbom with one key ko1 := options.KeyOpts{ - KeyRef: privKeyPath1, - PassFunc: passFunc, - RekorURL: rekorURL, + SigningConfig: rekorSigningConfig, + KeyRef: privKeyPath1, + PassFunc: passFunc, } so := options.SignOptions{ - Upload: true, - TlogUpload: true, - Attachment: "sbom", + Upload: true, } must(sign.SignCmd(ctx, ro, ko1, so, []string{imgName}), t) // Now verify should work with that one, but not the other - must(verify(pubKeyPath1, imgName, true, nil, "sbom", false), t) - mustErr(verify(pubKeyPath2, imgName, true, nil, "sbom", false), t) + must(verify(pubKeyPath1, imgName, true, nil, false), t) + mustErr(verify(pubKeyPath2, imgName, true, nil, false), t) } func TestNoTlog(t *testing.T) { @@ -4463,13 +4053,13 @@ func TestNoTlog(t *testing.T) { _, privKeyPath, pubKeyPath := keypair(t, td) // Verify should fail at first - mustErr(verify(pubKeyPath, imgName, true, nil, "", true), t) + mustErr(verify(pubKeyPath, imgName, true, nil, true), t) // Now sign the image without the tlog ko := options.KeyOpts{ - KeyRef: privKeyPath, - PassFunc: passFunc, - RekorURL: rekorURL, + KeyRef: privKeyPath, + PassFunc: passFunc, + SkipConfirmation: true, } so := options.SignOptions{ Upload: true, @@ -4477,7 +4067,7 @@ func TestNoTlog(t *testing.T) { must(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) // Now verify should work! - must(verify(pubKeyPath, imgName, true, nil, "", true), t) + must(verify(pubKeyPath, imgName, true, nil, true), t) } func TestGetPublicKeyCustomOut(t *testing.T) { @@ -4518,8 +4108,9 @@ func TestInvalidBundle(t *testing.T) { img1 := path.Join(regName, "cosign-e2e") - imgRef, _, cleanup := mkimage(t, img1) + imgRef, desc1, cleanup := mkimage(t, img1) defer cleanup() + imgDigest := imgRef.Context().Digest(desc1.Digest.String()) _, privKeyPath, pubKeyPath := keypair(t, td) @@ -4528,82 +4119,68 @@ func TestInvalidBundle(t *testing.T) { // Sign image1 and store the entry in rekor // (we're just using it for its bundle) remoteOpts := ociremote.WithRemoteOptions(registryClientOpts(ctx)...) - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc, RekorURL: rekorURL} + ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, + KeyRef: privKeyPath, + PassFunc: passFunc, + SkipConfirmation: true, + } so := options.SignOptions{ Upload: true, - TlogUpload: true, SkipConfirmation: true, } must(sign.SignCmd(ctx, ro, ko, so, []string{img1}), t) // verify image1 - must(verify(pubKeyPath, img1, true, nil, "", false), t) + must(verify(pubKeyPath, img1, true, nil, false), t) // extract the bundle from image1 - si, err := ociremote.SignedImage(imgRef, remoteOpts) - must(err, t) - imgSigs, err := si.Signatures() - must(err, t) - sigs, err := imgSigs.Get() - must(err, t) - if l := len(sigs); l != 1 { - t.Error("expected one signature") - } - bund, err := sigs[0].Bundle() - must(err, t) - if bund == nil { - t.Fail() - } + bundBytes := fetchReferrerBundle(t, imgDigest, remoteOpts) // Now, we move on to image2 // Sign image2 and DO NOT store the entry in rekor img2 := path.Join(regName, "unrelated") - imgRef2, _, cleanup := mkimage(t, img2) - defer cleanup() + imgRef2, desc2, cleanup2 := mkimage(t, img2) + defer cleanup2() + imgDigest2 := imgRef2.Context().Digest(desc2.Digest.String()) + koWithoutRekor := options.KeyOpts{ + KeyRef: privKeyPath, + PassFunc: passFunc, + SkipConfirmation: true, + } so = options.SignOptions{ - Upload: true, - TlogUpload: false, + Upload: true, } - must(sign.SignCmd(ctx, ro, ko, so, []string{img2}), t) - must(verify(pubKeyPath, img2, true, nil, "", true), t) + must(sign.SignCmd(ctx, ro, koWithoutRekor, so, []string{img2}), t) + must(verify(pubKeyPath, img2, true, nil, true), t) - si2, err := ociremote.SignedEntity(imgRef2, remoteOpts) - must(err, t) - sigs2, err := si2.Signatures() + // Find image2's referrer manifest and delete it + idx2, err := ociremote.Referrers(imgDigest2, "application/vnd.dev.sigstore.bundle.v0.3+json", remoteOpts) must(err, t) - gottenSigs2, err := sigs2.Get() - must(err, t) - if len(gottenSigs2) != 1 { - t.Fatal("there should be one signature") + if len(idx2.Manifests) != 1 { + t.Fatal("expected one referrer for image2") } - sigsTag, err := ociremote.SignatureTag(imgRef2) - if err != nil { - t.Fatal(err) + refDigest2 := imgDigest2.Context().Digest(idx2.Manifests[0].Digest.String()) + parts2 := strings.Split(imgDigest2.DigestStr(), ":") + if len(parts2) != 2 { + t.Fatal("invalid digest format for imgDigest2") } - - // At this point, we would mutate the signature to add the bundle annotation - // since we don't have a function for it at the moment, mock this by deleting the signature - // and pushing a new signature with the additional bundle annotation - if err := remote.Delete(sigsTag); err != nil { - t.Fatal(err) + fallbackTag2 := imgDigest2.Context().Tag("sha256-" + parts2[1]) + if err := remote.Delete(fallbackTag2, registryClientOpts(ctx)...); err != nil { + if err := remote.Delete(refDigest2, registryClientOpts(ctx)...); err != nil { + t.Fatal(err) + } } - mustErr(verify(pubKeyPath, img2, true, nil, "", false), t) - newSig, err := mutate.Signature(gottenSigs2[0], mutate.WithBundle(bund)) - must(err, t) - si2, err = ociremote.SignedEntity(imgRef2, remoteOpts) - must(err, t) - newImage, err := mutate.AttachSignatureToEntity(si2, newSig) - must(err, t) - if err := ociremote.WriteSignatures(sigsTag.Repository, newImage); err != nil { - t.Fatal(err) - } + // Verify image2 now fails (since its signature/referrer was deleted) + mustErr(verify(pubKeyPath, img2, true, nil, true), t) - // veriyfing image2 now should fail + // Re-upload image1's bundle to image2 as a referrer + must(ociremote.WriteAttestationNewBundleFormat(imgDigest2, bundBytes, "https://sigstore.dev/cosign/sign/v1", remoteOpts), t) + + // verifying image2 now should fail because the bundle is for image1, not image2 cmd := cliverify.VerifyCommand{ - KeyRef: pubKeyPath, - RekorURL: rekorURL, - CheckClaims: true, - HashAlgorithm: crypto.SHA256, - MaxWorkers: 10, + KeyRef: pubKeyPath, + CheckClaims: true, + MaxWorkers: 10, } args := []string{img2} mustErr(cmd.Exec(context.Background(), args), t) @@ -4640,17 +4217,18 @@ func TestAttestBlobSignVerify(t *testing.T) { t.Fatal(err) } - outputSignature := filepath.Join(td1, "signature") - _, privKeyPath1, pubKeyPath1 := keypair(t, td1) + bundlePath1 := filepath.Join(td1, "attest1.bundle.json") + bundlePath2 := filepath.Join(td1, "attest2.bundle.json") + ctx := context.Background() ko := options.KeyOpts{ - KeyRef: pubKeyPath1, + KeyRef: pubKeyPath1, + BundlePath: bundlePath1, } blobVerifyAttestationCmd := cliverify.VerifyBlobAttestationCommand{ KeyOpts: ko, - SignaturePath: outputSignature, PredicateType: predicateType, IgnoreTlog: true, CheckClaims: true, @@ -4660,15 +4238,16 @@ func TestAttestBlobSignVerify(t *testing.T) { // Now attest the blob with the private key ko = options.KeyOpts{ - KeyRef: privKeyPath1, - PassFunc: passFunc, + SigningConfig: rekorSigningConfig, + KeyRef: privKeyPath1, + PassFunc: passFunc, + BundlePath: bundlePath1, + SkipConfirmation: true, } attestBlobCmd := attest.AttestBlobCommand{ - KeyOpts: ko, - PredicatePath: predicatePath, - PredicateType: predicateType, - OutputSignature: outputSignature, - RekorEntryType: "dsse", + KeyOpts: ko, + PredicatePath: predicatePath, + PredicateType: predicateType, } must(attestBlobCmd.Exec(ctx, bp), t) @@ -4684,23 +4263,28 @@ func TestAttestBlobSignVerify(t *testing.T) { mustErr(blobVerifyAttestationCmd.Exec(ctx, anotherBlob), t) // Test statement signing + ko = options.KeyOpts{ + SigningConfig: rekorSigningConfig, + KeyRef: privKeyPath1, + PassFunc: passFunc, + BundlePath: bundlePath2, + SkipConfirmation: true, + } attestBlobCmd = attest.AttestBlobCommand{ - KeyOpts: ko, - StatementPath: statementPath, - OutputSignature: outputSignature, - RekorEntryType: "dsse", + KeyOpts: ko, + StatementPath: statementPath, } must(attestBlobCmd.Exec(ctx, bp), t) // Test statement verification ko = options.KeyOpts{ - KeyRef: pubKeyPath1, + KeyRef: pubKeyPath1, + BundlePath: bundlePath2, } blobVerifyAttestationCmd = cliverify.VerifyBlobAttestationCommand{ KeyOpts: ko, Digest: "7e9b6e7ba2842c91cf49f3e214d04a7a496f8214356f41d81a6e6dcad11f11e3", - DigestAlg: "alg", - SignaturePath: outputSignature, + DigestAlg: "sha256", IgnoreTlog: true, PredicateType: "something", } @@ -4719,67 +4303,66 @@ func TestOffline(t *testing.T) { img1 := path.Join(regName, "cosign-e2e") - imgRef, _, cleanup := mkimage(t, img1) + imgRef, desc, cleanup := mkimage(t, img1) defer cleanup() + imgDigest := imgRef.Context().Digest(desc.Digest.String()) _, privKeyPath, pubKeyPath := keypair(t, td) ctx := context.Background() // Sign image1 and store the entry in rekor - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc, RekorURL: rekorURL} + ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, + KeyRef: privKeyPath, + PassFunc: passFunc, + SkipConfirmation: true, + } so := options.SignOptions{ Upload: true, - TlogUpload: true, SkipConfirmation: true, } must(sign.SignCmd(ctx, ro, ko, so, []string{img1}), t) // verify image1 online and offline - must(verify(pubKeyPath, img1, true, nil, "", false), t) + must(verify(pubKeyPath, img1, true, nil, false), t) verifyCmd := &cliverify.VerifyCommand{ KeyRef: pubKeyPath, - RekorURL: "notreal", - Offline: true, CheckClaims: true, MaxWorkers: 10, + CommonVerifyOptions: options.CommonVerifyOptions{ + TrustedRootPath: getTestTrustedRootPath(), + }, } must(verifyCmd.Exec(ctx, []string{img1}), t) - // Get signatures - si, err := ociremote.SignedEntity(imgRef) - must(err, t) - sigs, err := si.Signatures() - must(err, t) - gottenSigs, err := sigs.Get() + // Find the referrer manifest of image1 and delete it + remoteOpts := ociremote.WithRemoteOptions(registryClientOpts(ctx)...) + idx, err := ociremote.Referrers(imgDigest, "application/vnd.dev.sigstore.bundle.v0.3+json", remoteOpts) must(err, t) - - fakeBundle := &bundle.RekorBundle{ - SignedEntryTimestamp: []byte(""), - Payload: bundle.RekorPayload{ - Body: "", - }, + if len(idx.Manifests) != 1 { + t.Fatal("expected one referrer for image1") } - newSig, err := mutate.Signature(gottenSigs[0], mutate.WithBundle(fakeBundle)) - must(err, t) - - sigsTag, err := ociremote.SignatureTag(imgRef) - must(err, t) - - if err := remote.Delete(sigsTag); err != nil { - t.Fatal(err) + refDigest := imgDigest.Context().Digest(idx.Manifests[0].Digest.String()) + parts := strings.Split(imgDigest.DigestStr(), ":") + if len(parts) != 2 { + t.Fatal("invalid digest format for imgDigest") + } + fallbackTag := imgDigest.Context().Tag("sha256-" + parts[1]) + if err := remote.Delete(fallbackTag, registryClientOpts(ctx)...); err != nil { + if err := remote.Delete(refDigest, registryClientOpts(ctx)...); err != nil { + t.Fatal(err) + } } - si, err = ociremote.SignedEntity(imgRef) - must(err, t) - newImage, err := mutate.AttachSignatureToEntity(si, newSig) - must(err, t) + // Verify image1 now fails online and offline (since we deleted the signature) + mustErr(verify(pubKeyPath, img1, true, nil, false), t) + mustErr(verifyCmd.Exec(ctx, []string{img1}), t) - mustErr(verify(pubKeyPath, img1, true, nil, "", false), t) - if err := ociremote.WriteSignatures(sigsTag.Repository, newImage); err != nil { - t.Fatal(err) - } + // Re-upload a fake empty bundle as a referrer to image1 + fakeBundleBytes := []byte("{}") + must(ociremote.WriteAttestationNewBundleFormat(imgDigest, fakeBundleBytes, "https://sigstore.dev/cosign/sign/v1", remoteOpts), t) - // Confirm offline verification fails + // Confirm offline verification fails on the fake bundle mustErr(verifyCmd.Exec(ctx, []string{img1}), t) } @@ -4798,6 +4381,8 @@ func TestDockerfileVerify(t *testing.T) { t.Fatal(err) } + trustedRootPath := prepareTrustedRoot(t, "") + identityToken, err := getOIDCToken() if err != nil { t.Fatal(err) @@ -4818,14 +4403,12 @@ func TestDockerfileVerify(t *testing.T) { // sign the images using --identity-token ko := options.KeyOpts{ - FulcioURL: fulcioURL, - RekorURL: rekorURL, + SigningConfig: rekorSigningConfig, IDToken: identityToken, SkipConfirmation: true, } so := options.SignOptions{ Upload: true, - TlogUpload: true, SkipConfirmation: true, } ctx := context.Background() @@ -4911,11 +4494,13 @@ from %s t.Run(test.name, func(t *testing.T) { cmd := dockerfile.VerifyDockerfileCommand{ VerifyCommand: cliverify.VerifyCommand{ + CommonVerifyOptions: options.CommonVerifyOptions{ + TrustedRootPath: trustedRootPath, + }, CertVerifyOptions: options.CertVerifyOptions{ CertOidcIssuer: issuer, CertIdentity: certID, }, - RekorURL: rekorURL, }, BaseOnly: test.baseOnly, } @@ -4964,14 +4549,12 @@ func TestManifestVerify(t *testing.T) { // sign the images using --identity-token ko := options.KeyOpts{ - FulcioURL: fulcioURL, - RekorURL: rekorURL, + SigningConfig: rekorSigningConfig, IDToken: identityToken, SkipConfirmation: true, } so := options.SignOptions{ Upload: true, - TlogUpload: true, SkipConfirmation: true, } ctx := context.Background() @@ -4994,6 +4577,7 @@ spec: unsignedManifest := mkfileWithExt(unsignedManifestContents, td, ".yaml", t) issuer := os.Getenv("ISSUER_URL") + trustedRootPath := prepareTrustedRoot(t, "") tests := []struct { name string @@ -5014,11 +4598,13 @@ spec: t.Run(test.name, func(t *testing.T) { cmd := manifest.VerifyManifestCommand{ VerifyCommand: cliverify.VerifyCommand{ + CommonVerifyOptions: options.CommonVerifyOptions{ + TrustedRootPath: trustedRootPath, + }, CertVerifyOptions: options.CertVerifyOptions{ CertOidcIssuer: issuer, CertIdentity: certID, }, - RekorURL: rekorURL, }, } args := []string{test.manifest} @@ -5071,7 +4657,7 @@ func TestSignVerifyWithRepoOverride(t *testing.T) { _, privKeyPath, pubKeyPath := keypair(t, td) // Verify should fail at first - mustErr(verify(pubKeyPath, imgName, true, nil, "", false), t) + mustErr(verify(pubKeyPath, imgName, true, nil, false), t) // No artifacts yet in the second registry _, err = crane.ListTags(cosignRepo) @@ -5086,15 +4672,14 @@ func TestSignVerifyWithRepoOverride(t *testing.T) { // Now sign the image ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKeyPath, PassFunc: passFunc, - RekorURL: rekorURL, SkipConfirmation: true, } so := options.SignOptions{ - Upload: true, - TlogUpload: true, + Upload: true, } must(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) @@ -5103,43 +4688,16 @@ func TestSignVerifyWithRepoOverride(t *testing.T) { tags, err = crane.ListTags(cosignRepo) must(err, t) assert.Len(t, tags, 1, "expected 1 signature tag in the second repo") - expectedTagName := fmt.Sprintf("%s.sig", strings.ReplaceAll(digest, ":", "-")) - assert.Equal(t, tags[0], expectedTagName, "expected signature tag to match sha256-.sig") - // but not in the first repo - tags, err = crane.ListTags(name.String()) - must(err, t) - assert.Len(t, tags, 1, "expected no extra tags in the first repo") - assert.Equal(t, tags[0], "latest", "expected tag name to be 'latest'") - - // Now verify and download should work! - must(verify(pubKeyPath, imgName, true, nil, "", false), t) - - // Sign another image with the new protobuf bundle format - so.NewBundleFormat = true - must(sign.SignCmd(t.Context(), ro, ko, so, []string{name.String()}), t) - - // The new bundle should appear under a new tag for the second repo - tags, err = crane.ListTags(cosignRepo) - must(err, t) - assert.Len(t, tags, 2, "expected new tag in the second repo") - expectedTagName = strings.ReplaceAll(digest, ":", "-") - assert.Equal(t, tags[0], expectedTagName, "expected new tag to match referrers format") + expectedTagName := strings.ReplaceAll(digest, ":", "-") + assert.Equal(t, tags[0], expectedTagName, "expected signature tag to match sha256-") // but not in the first repo tags, err = crane.ListTags(name.String()) must(err, t) assert.Len(t, tags, 1, "expected no extra tags in the first repo") assert.Equal(t, tags[0], "latest", "expected tag name to be 'latest'") - // Verify should work with new bundle format - cmd := cliverify.VerifyCommand{ - KeyRef: pubKeyPath, - RekorURL: rekorURL, - NewBundleFormat: true, - IgnoreTlog: true, - } - - ctx := context.Background() - must(cmd.Exec(ctx, []string{imgName}), t) + // Now verify should work! + must(verify(pubKeyPath, imgName, true, nil, false), t) } func TestSignVerifyMultipleIdentities(t *testing.T) { @@ -5160,27 +4718,25 @@ func TestSignVerifyMultipleIdentities(t *testing.T) { _, privKeyPath, pubKeyPath := keypair(t, td) // Verify should fail at first - mustErr(verify(pubKeyPath, imgName, true, nil, "", false), t) + mustErr(verify(pubKeyPath, imgName, true, nil, false), t) // Now sign the image with multiple container identities ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKeyPath, PassFunc: passFunc, - RekorURL: rekorURL, SkipConfirmation: true, } so := options.SignOptions{ - Upload: true, - TlogUpload: true, - SignContainerIdentities: []string{"registry/cosign-e2e:tag1", "registry/cosign-e2e:tag2"}, + Upload: true, } must(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) // Now verify should work - must(verify(pubKeyPath, imgName, true, nil, "", false), t) + must(verify(pubKeyPath, imgName, true, nil, false), t) } -func TestSignVerifyMultipleIdentitiesKeyless(t *testing.T) { +func TestSignVerifyKeyless(t *testing.T) { td := t.TempDir() // set up SIGSTORE_ variables to point to keys for the local instances @@ -5200,7 +4756,7 @@ func TestSignVerifyMultipleIdentitiesKeyless(t *testing.T) { imgName := path.Join(repo, "cosign-e2e") - imgRef, _, cleanup := mkimage(t, imgName) + _, _, cleanup := mkimage(t, imgName) defer cleanup() identityToken, err := getOIDCToken() @@ -5210,50 +4766,32 @@ func TestSignVerifyMultipleIdentitiesKeyless(t *testing.T) { // Verify should fail at first issuer := os.Getenv("ISSUER_URL") + trustedRootPath := prepareTrustedRoot(t, "") verifyCmd := cliverify.VerifyCommand{ + CommonVerifyOptions: options.CommonVerifyOptions{ + TrustedRootPath: trustedRootPath, + }, CertVerifyOptions: options.CertVerifyOptions{ CertOidcIssuer: issuer, CertIdentity: certID, }, - RekorURL: rekorURL, CheckClaims: true, } mustErr(verifyCmd.Exec(t.Context(), []string{imgName}), t) - // Now sign the image with multiple container identities + // Now sign the image ko := options.KeyOpts{ - FulcioURL: fulcioURL, - RekorURL: rekorURL, + SigningConfig: rekorSigningConfig, IDToken: identityToken, SkipConfirmation: true, } so := options.SignOptions{ - Upload: true, - TlogUpload: true, - SignContainerIdentities: []string{"registry/cosign-e2e:tag1", "registry/cosign-e2e:tag2"}, + Upload: true, } must(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) // Now verify should work must(verifyCmd.Exec(t.Context(), []string{imgName}), t) - - // Fetch signatures and check if certificates are identical - si, err := ociremote.SignedEntity(imgRef) - must(err, t) - sigs, err := si.Signatures() - must(err, t) - gottenSigs, err := sigs.Get() - must(err, t) - - assert.Len(t, gottenSigs, 2, "expected 2 signatures") - - cert1, err := gottenSigs[0].Cert() - must(err, t) - cert2, err := gottenSigs[1].Cert() - must(err, t) - - // Compare raw bytes of certificates to ensure they are the same - assert.Equal(t, cert1.Raw, cert2.Raw, "expected certificates to be identical") } func TestTree(t *testing.T) { @@ -5276,10 +4814,13 @@ func TestTree(t *testing.T) { // Sign the image td := t.TempDir() _, privKeyPath, _ := keypair(t, td) - ko := options.KeyOpts{KeyRef: privKeyPath, PassFunc: passFunc} + ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, + KeyRef: privKeyPath, + PassFunc: passFunc, + } so := options.SignOptions{ - NewBundleFormat: true, - Upload: true, + Upload: true, } must(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) @@ -5313,6 +4854,7 @@ func TestSignVerifyUploadFalse(t *testing.T) { // Now sign the image with Upload: false ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKeyPath, PassFunc: passFunc, SkipConfirmation: true, @@ -5334,7 +4876,11 @@ func TestSignVerifyUploadFalse(t *testing.T) { // Now there should be signatures out.Reset() must(cli.TreeCmd(ctx, regOpts, regExpOpts, true, imgName, &out), t) - assert.Contains(t, out.String(), fmt.Sprintf("Signatures for an image tag: %s:%s-%s.sig", name, desc.Digest.Algorithm, desc.Digest.Hex)) + expectedSigRegex := regexp.MustCompile(fmt.Sprintf( + `(Signatures for an image tag: %s:%s-%s\.sig|https://sigstore\.dev/cosign/sign/v1 artifacts via OCI referrer: %s@sha256:[a-f0-9]+)`, + regexp.QuoteMeta(name.Name()), desc.Digest.Algorithm, desc.Digest.Hex, regexp.QuoteMeta(name.Context().Name()), + )) + assert.Regexp(t, expectedSigRegex, out.String()) // Try on a new image with new bundle format imgName = path.Join(repo, "cosign-e2e-no-upload-bundle") @@ -5348,7 +4894,6 @@ func TestSignVerifyUploadFalse(t *testing.T) { // Now sign the image with Upload: false so.Upload = false - so.NewBundleFormat = true so.BundlePath = path.Join(td, "output.bundle") must(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) assert.FileExists(t, so.BundlePath) @@ -5399,6 +4944,7 @@ func TestAttestVerifyUploadFalse(t *testing.T) { // Now attest the image with NoUpload: true ko := options.KeyOpts{ + SigningConfig: rekorSigningConfig, KeyRef: privKeyPath, PassFunc: passFunc, SkipConfirmation: true, @@ -5409,11 +4955,10 @@ func TestAttestVerifyUploadFalse(t *testing.T) { t.Fatal(err) } attestCmd := attest.AttestCommand{ - KeyOpts: ko, - PredicatePath: predicatePath, - PredicateType: "slsaprovenance", - RekorEntryType: "dsse", - NoUpload: true, + KeyOpts: ko, + PredicatePath: predicatePath, + PredicateType: "slsaprovenance", + NoUpload: true, } must(attestCmd.Exec(ctx, imgName), t) @@ -5429,7 +4974,11 @@ func TestAttestVerifyUploadFalse(t *testing.T) { // Now there should be attestations out.Reset() must(cli.TreeCmd(ctx, regOpts, regExpOpts, true, imgName, &out), t) - assert.Contains(t, out.String(), fmt.Sprintf("Attestations for an image tag: %s:%s-%s.att", name, desc.Digest.Algorithm, desc.Digest.Hex)) + expectedAttestRegex := regexp.MustCompile(fmt.Sprintf( + `(Attestations for an image tag: %s:%s-%s\.att|https://slsa\.dev/provenance/v0\.2 artifacts via OCI referrer: %s@sha256:[a-f0-9]+)`, + regexp.QuoteMeta(name.Name()), desc.Digest.Algorithm, desc.Digest.Hex, regexp.QuoteMeta(name.Context().Name()), + )) + assert.Regexp(t, expectedAttestRegex, out.String()) // Try on a new image with new bundle format imgName = path.Join(repo, "cosign-e2e-no-upload-bundle") @@ -5443,7 +4992,6 @@ func TestAttestVerifyUploadFalse(t *testing.T) { // Now attest the image with NoUpload: true attestCmd.NoUpload = true - attestCmd.NewBundleFormat = true attestCmd.BundlePath = path.Join(td, "output.bundle") must(attestCmd.Exec(ctx, imgName), t) assert.FileExists(t, attestCmd.BundlePath) @@ -5569,102 +5117,6 @@ func signingCertChain(t *testing.T) (leafCertPath, signChainPath, caChainPath, l return leafCertPath, signChainPath, caChainPath, leafKeyPath } -func TestSignVerifyDetachedKeyless(t *testing.T) { - td := t.TempDir() - err := setLocalEnv(t, td) - must(err, t) - must(fulcioroots.ReInit(), t) - - repo, stop := reg(t) - defer stop() - imgName := path.Join(repo, "cosign-e2e-detached-keyless") - - _, _, cleanup := mkimage(t, imgName) - defer cleanup() - - identityToken, err := getOIDCToken() - must(err, t) - - ctx := context.Background() - sigFile := filepath.Join(td, "sig.out") - certFile := filepath.Join(td, "cert.out") - - // Verify should fail before signing - failCmd1 := cliverify.VerifyCommand{ - RekorURL: rekorURL, - CertVerifyOptions: options.CertVerifyOptions{ - CertOidcIssuer: os.Getenv("ISSUER_URL"), - CertIdentity: certID, - }, - } - mustErr(failCmd1.Exec(ctx, []string{imgName}), t) - - ko := options.KeyOpts{ - FulcioURL: fulcioURL, - RekorURL: rekorURL, - IDToken: identityToken, - SkipConfirmation: true, - } - so := options.SignOptions{ - Upload: true, - TlogUpload: true, - OutputSignature: sigFile, - OutputCertificate: certFile, - UseSigningConfig: false, - } - must(sign.SignCmd(ctx, ro, ko, so, []string{imgName}), t) - - // Verify should fail with a detached signature but no certificate - failCmd2 := cliverify.VerifyCommand{ - RekorURL: rekorURL, - SignatureRef: sigFile, - CertVerifyOptions: options.CertVerifyOptions{ - CertOidcIssuer: os.Getenv("ISSUER_URL"), - CertIdentity: certID, - }, - } - mustErr(failCmd2.Exec(ctx, []string{imgName}), t) - - // Now verify should work using the certificate - cmd := cliverify.VerifyCommand{ - RekorURL: rekorURL, - SignatureRef: sigFile, - CertRef: certFile, - CertVerifyOptions: options.CertVerifyOptions{ - CertOidcIssuer: os.Getenv("ISSUER_URL"), - CertIdentity: certID, - }, - } - must(cmd.Exec(ctx, []string{imgName}), t) - - // Save the original root file path and ensure it's restored for subsequent tests - origRootFile := os.Getenv("SIGSTORE_ROOT_FILE") - defer func() { - t.Setenv("SIGSTORE_ROOT_FILE", origRootFile) - _ = fulcioroots.ReInit() - }() - - // Invalidate the default root cert env var and re-initialize to simulate a missing/unconfigured default trust root - t.Setenv("SIGSTORE_ROOT_FILE", "/nonexistent/path") - _ = fulcioroots.ReInit() - - // Verify should now fail without explicitly passing the certificate chain - mustErr(cmd.Exec(ctx, []string{imgName}), t) - - // Now verify should work when explicitly providing the certificate chain - cmdWithChain := cliverify.VerifyCommand{ - RekorURL: rekorURL, - SignatureRef: sigFile, - CertRef: certFile, - CertChain: origRootFile, - CertVerifyOptions: options.CertVerifyOptions{ - CertOidcIssuer: os.Getenv("ISSUER_URL"), - CertIdentity: certID, - }, - } - must(cmdWithChain.Exec(ctx, []string{imgName}), t) -} - func getTimestampedSignature(sigBytes []byte, tsaClient client.TimestampAuthorityClient) ([]byte, error) { requestBytes, err := timestamp.CreateRequest(bytes.NewReader(sigBytes), ×tamp.RequestOptions{ Hash: crypto.SHA256, diff --git a/test/e2e_test.ps1 b/test/e2e_test.ps1 index 4228d769a29..f4144d87bf4 100644 --- a/test/e2e_test.ps1 +++ b/test/e2e_test.ps1 @@ -35,7 +35,7 @@ $signing_key = "cosign.key" $verification_key = "cosign.pub" Write-Output "hello world" | Out-File -FilePath "hello_world.txt" -Write-Output $pass | .\cosign.exe sign-blob --key $signing_key --bundle test.sigstore.json --use-signing-config=false --tlog-upload=false hello_world.txt +Write-Output $pass | .\cosign.exe sign-blob --key $signing_key --bundle test.sigstore.json --use-signing-config=false hello_world.txt .\cosign.exe verify-blob --key $verification_key --bundle test.sigstore.json --insecure-ignore-tlog=true hello_world.txt Pop-Location diff --git a/test/e2e_tsa_test.go b/test/e2e_tsa_test.go index 19f2354dc73..e0cb26c9d01 100644 --- a/test/e2e_tsa_test.go +++ b/test/e2e_tsa_test.go @@ -42,96 +42,6 @@ import ( "github.com/sigstore/sigstore-go/pkg/root" ) -func TestTSAMTLS(t *testing.T) { - repo, stop := reg(t) - defer stop() - td := t.TempDir() - - imgName := path.Join(repo, "cosign-tsa-mtls-e2e") - - _, _, cleanup := mkimage(t, imgName) - defer cleanup() - - pemRootRef, pemLeafRef, pemKeyRef := generateSigningKeys(t, td) - - // Set up TSA server with TLS - timestampCACert, timestampServerCert, timestampServerKey, timestampClientCert, timestampClientKey := generateMTLSKeys(t, td) - timestampServerURL, timestampChainFile, tsaCleanup := setUpTSAServerWithTLS(t, td, timestampCACert, timestampServerKey, timestampServerCert) - t.Cleanup(tsaCleanup) - - ko := options.KeyOpts{ - KeyRef: pemKeyRef, - PassFunc: passFunc, - TSAServerURL: timestampServerURL, - TSAClientCACert: timestampCACert, - TSAClientCert: timestampClientCert, - TSAClientKey: timestampClientKey, - TSAServerName: "server.example.com", - } - so := options.SignOptions{ - Upload: true, - TlogUpload: false, - Cert: pemLeafRef, - } - must(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) - - verifyCmd := cliverify.VerifyCommand{ - IgnoreTlog: true, - IgnoreSCT: true, - CheckClaims: true, - CertChain: pemRootRef, - TSACertChainPath: timestampChainFile, - CertVerifyOptions: options.CertVerifyOptions{ - CertIdentityRegexp: ".*", - CertOidcIssuerRegexp: ".*", - }, - } - must(verifyCmd.Exec(context.Background(), []string{imgName}), t) -} - -func TestSignBlobTSAMTLS(t *testing.T) { - td := t.TempDir() - blob := time.Now().Format("Mon Jan 2 15:04:05 MST 2006") - blobPath := mkfile(blob, td, t) - timestampPath := filepath.Join(td, "timestamp.txt") - bundlePath := filepath.Join(td, "cosign.bundle") - - _, privKey, pubKey := keypair(t, td) - - // Set up TSA server with TLS - timestampCACert, timestampServerCert, timestampServerKey, timestampClientCert, timestampClientKey := generateMTLSKeys(t, td) - timestampServerURL, timestampChainFile, tsaCleanup := setUpTSAServerWithTLS(t, td, timestampCACert, timestampServerKey, timestampServerCert) - t.Cleanup(tsaCleanup) - - signingKO := options.KeyOpts{ - KeyRef: privKey, - PassFunc: passFunc, - TSAServerURL: timestampServerURL, - TSAClientCACert: timestampCACert, - TSAClientCert: timestampClientCert, - TSAClientKey: timestampClientKey, - TSAServerName: "server.example.com", - RFC3161TimestampPath: timestampPath, - BundlePath: bundlePath, - } - sig, err := sign.SignBlobCmd(t.Context(), ro, signingKO, blobPath, "", "", true, "", "", false) - must(err, t) - - verifyKO := options.KeyOpts{ - KeyRef: pubKey, - TSACertChainPath: timestampChainFile, - RFC3161TimestampPath: timestampPath, - BundlePath: bundlePath, - } - - verifyCmd := cliverify.VerifyBlobCmd{ - KeyOpts: verifyKO, - SigRef: string(sig), - IgnoreTlog: true, - } - must(verifyCmd.Exec(context.Background(), blobPath), t) -} - func TestSignBlobTSAMTLSWithSigningConfig(t *testing.T) { td := t.TempDir() blob := time.Now().Format("Mon Jan 2 15:04:05 MST 2006") @@ -208,16 +118,14 @@ func TestSignBlobTSAMTLSWithSigningConfig(t *testing.T) { BundlePath: bundlePath, SigningConfig: signingConfig, TrustedMaterial: trustedRoot, - NewBundleFormat: true, } - _, err = sign.SignBlobCmd(t.Context(), ro, signingKO, blobPath, "", "", true, "", "", false) + err = sign.SignBlobCmd(t.Context(), ro, signingKO, blobPath, "", "") must(err, t) verifyKO := options.KeyOpts{ KeyRef: pubKey, BundlePath: bundlePath, TrustedMaterial: trustedRoot, - NewBundleFormat: true, } verifyCmd := cliverify.VerifyBlobCmd{ @@ -323,13 +231,11 @@ func TestTSAMTLSWithSigningConfig(t *testing.T) { TSAServerName: "server.example.com", SigningConfig: signingConfig, TrustedMaterial: trustedRoot, - NewBundleFormat: true, } so := options.SignOptions{ - Upload: true, - TlogUpload: false, - Cert: pemLeafRef, - NewBundleFormat: true, + Upload: true, + Cert: pemLeafRef, + CertChain: pemRootRef, } must(sign.SignCmd(t.Context(), ro, ko, so, []string{imgName}), t) @@ -338,13 +244,13 @@ func TestTSAMTLSWithSigningConfig(t *testing.T) { trustedRootFile := mkfile(string(trBytes), td, t) verifyCmd := cliverify.VerifyCommand{ - IgnoreTlog: true, - IgnoreSCT: true, - CheckClaims: true, - NewBundleFormat: true, + IgnoreTlog: true, + IgnoreSCT: true, + CheckClaims: true, CommonVerifyOptions: options.CommonVerifyOptions{ TrustedRootPath: trustedRootFile, }, + AllowCertificateChain: true, CertVerifyOptions: options.CertVerifyOptions{ CertIdentityRegexp: ".*", CertOidcIssuerRegexp: ".*", diff --git a/test/helpers.go b/test/helpers.go index 112e3569bd8..c6f79e6efae 100644 --- a/test/helpers.go +++ b/test/helpers.go @@ -20,7 +20,6 @@ package test import ( "bytes" "context" - "crypto" "crypto/ecdsa" "crypto/rand" "crypto/rsa" @@ -37,9 +36,12 @@ import ( "net/url" "os" "path/filepath" + "sync" "testing" "time" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/trustedroot" + "github.com/google/go-cmp/cmp" "github.com/google/go-containerregistry/pkg/authn" "github.com/google/go-containerregistry/pkg/name" @@ -52,207 +54,170 @@ import ( _ "k8s.io/client-go/plugin/pkg/client/auth" "github.com/sigstore/cosign/v3/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v3/cmd/cosign/cli/signcommon" cliverify "github.com/sigstore/cosign/v3/cmd/cosign/cli/verify" "github.com/sigstore/cosign/v3/pkg/cosign" "github.com/sigstore/cosign/v3/pkg/cosign/env" ociremote "github.com/sigstore/cosign/v3/pkg/oci/remote" sigs "github.com/sigstore/cosign/v3/pkg/signature" v1 "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1" + "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore/pkg/signature" ) -const ( - rekorURL = "http://127.0.0.1:3000" - rekorV2URL = "http://127.0.0.1:3003" - fulcioURL = "http://127.0.0.1:5555" - tsaURL = "http://127.0.0.1:3004" +var ( + rekorURL = getEnvWithFallback("REKOR_URL", "http://127.0.0.1:3000") + rekorV2URL = getEnvWithFallback("REKOR_V2_URL", "http://127.0.0.1:3003") + fulcioURL = getEnvWithFallback("FULCIO_URL", "http://127.0.0.1:5555") + tsaURL = getEnvWithFallback("TSA_URL", "http://127.0.0.1:3004") certID = "foo@bar.com" ) -var keyPass = []byte("hello") - -var passFunc = func(_ bool) ([]byte, error) { - return keyPass, nil -} - -var verify = func(keyRef, imageRef string, checkClaims bool, annotations map[string]interface{}, attachment string, skipTlogVerify bool) error { - cmd := cliverify.VerifyCommand{ - KeyRef: keyRef, - RekorURL: rekorURL, - CheckClaims: checkClaims, - Annotations: sigs.AnnotationsMap{Annotations: annotations}, - Attachment: attachment, - HashAlgorithm: crypto.SHA256, - MaxWorkers: 10, - IgnoreTlog: skipTlogVerify, +func getEnvWithFallback(key, fallback string) string { + if val := os.Getenv(key); val != "" { //nolint:forbidigo + return val } + return fallback +} - args := []string{imageRef} +var ( + testTrustedRootPath string + testTrustedRootPathOnce sync.Once +) - return cmd.Exec(context.Background(), args) +func getTestTrustedRootPath() string { + testTrustedRootPathOnce.Do(func() { + td, err := os.MkdirTemp("", "cosign-e2e-trustedroot") + if err != nil { + panic(fmt.Errorf("creating temp dir for trusted root: %w", err)) + } + rekorPath := filepath.Join(td, "rekor.pub") + rekorFP, err := os.Create(rekorPath) + if err != nil { + panic(err) + } + defer rekorFP.Close() + if err := downloadFile(rekorURL+"/api/v1/log/publicKey", rekorFP); err != nil { + panic(err) + } + outPath := filepath.Join(td, "trusted-root.json") + cmd := &trustedroot.CreateCmd{ + Out: outPath, + RekorKeyPath: []string{rekorPath}, + } + if err := cmd.Exec(context.Background()); err != nil { + panic(err) + } + testTrustedRootPath = outPath + }) + return testTrustedRootPath } -var verifyCertChain = func(keyRef, certChain, certFile, imageRef string, checkClaims bool, annotations map[string]interface{}, attachment string, skipTlogVerify bool) error { - cmd := cliverify.VerifyCommand{ - KeyRef: keyRef, - RekorURL: rekorURL, - CheckClaims: checkClaims, - Annotations: sigs.AnnotationsMap{Annotations: annotations}, - Attachment: attachment, - HashAlgorithm: crypto.SHA256, - MaxWorkers: 10, - IgnoreTlog: skipTlogVerify, - CertVerifyOptions: options.CertVerifyOptions{ - Cert: certFile, - CertChain: certChain, - }, +func initVerifyCmd(cmd *cliverify.VerifyCommand) { + if !cmd.IgnoreTlog && cmd.TrustedRootPath == "" { + cmd.TrustedRootPath = getTestTrustedRootPath() } +} - args := []string{imageRef} +var keyPass = []byte("hello") - return cmd.Exec(context.Background(), args) +var passFunc = func(_ bool) ([]byte, error) { + return keyPass, nil } -var verifyCertBundle = func(keyRef, caCertFile, caIntermediateCertFile, imageRef string, checkClaims bool, annotations map[string]interface{}, attachment string, skipTlogVerify bool) error { +var verify = func(keyRef, imageRef string, checkClaims bool, annotations map[string]interface{}, skipTlogVerify bool) error { cmd := cliverify.VerifyCommand{ - KeyRef: keyRef, - RekorURL: rekorURL, - CheckClaims: checkClaims, - Annotations: sigs.AnnotationsMap{Annotations: annotations}, - Attachment: attachment, - HashAlgorithm: crypto.SHA256, - MaxWorkers: 10, - IgnoreTlog: skipTlogVerify, - CertVerifyOptions: options.CertVerifyOptions{ - CAIntermediates: caIntermediateCertFile, - CARoots: caCertFile, - }, + KeyRef: keyRef, + CheckClaims: checkClaims, + Annotations: sigs.AnnotationsMap{Annotations: annotations}, + MaxWorkers: 10, + IgnoreTlog: skipTlogVerify, } args := []string{imageRef} + initVerifyCmd(&cmd) return cmd.Exec(context.Background(), args) } -var verifyTSA = func(keyRef, imageRef string, checkClaims bool, annotations map[string]interface{}, attachment, tsaCertChain string, skipTlogVerify bool) error { +var verifyTSA = func(keyRef, imageRef string, checkClaims bool, annotations map[string]interface{}, skipTlogVerify bool) error { cmd := cliverify.VerifyCommand{ - KeyRef: keyRef, - RekorURL: rekorURL, - CheckClaims: checkClaims, - Annotations: sigs.AnnotationsMap{Annotations: annotations}, - Attachment: attachment, - HashAlgorithm: crypto.SHA256, - TSACertChainPath: tsaCertChain, - IgnoreTlog: skipTlogVerify, - MaxWorkers: 10, + KeyRef: keyRef, + CheckClaims: checkClaims, + Annotations: sigs.AnnotationsMap{Annotations: annotations}, + IgnoreTlog: skipTlogVerify, + MaxWorkers: 10, } args := []string{imageRef} + initVerifyCmd(&cmd) return cmd.Exec(context.Background(), args) } -var verifyKeylessTSA = func(imageRef, tsaCertChain, certChain string, skipSCT, skipTlogVerify bool) error { //nolint: unused +var verifyKeylessTSA = func(imageRef, _ string, skipSCT, skipTlogVerify bool) error { //nolint: unused cmd := cliverify.VerifyCommand{ CertVerifyOptions: options.CertVerifyOptions{ CertOidcIssuerRegexp: ".*", CertIdentityRegexp: ".*", }, - CertChain: certChain, - RekorURL: rekorURL, - HashAlgorithm: crypto.SHA256, - TSACertChainPath: tsaCertChain, - IgnoreSCT: skipSCT, - IgnoreTlog: skipTlogVerify, - MaxWorkers: 10, + IgnoreSCT: skipSCT, + IgnoreTlog: skipTlogVerify, + MaxWorkers: 10, } args := []string{imageRef} + initVerifyCmd(&cmd) return cmd.Exec(context.Background(), args) } -var verifyKeylessTSAWithCARoots = func(imageRef string, - caroots string, // filename of a PEM file with CA Roots certificates - intermediates string, // empty or filename of a PEM file with Intermediate certificates - certFile string, // filename of a PEM file with the codesigning certificate - tsaCertChain string, - skipSCT bool, - skipTlogVerify bool) error { +var verifyWithTrustedRoot = func(imageRef, trustedRootPath string) error { cmd := cliverify.VerifyCommand{ + CommonVerifyOptions: options.CommonVerifyOptions{ + TrustedRootPath: trustedRootPath, + }, CertVerifyOptions: options.CertVerifyOptions{ CertOidcIssuerRegexp: ".*", CertIdentityRegexp: ".*", }, - CertRef: certFile, - CARoots: caroots, - CAIntermediates: intermediates, - RekorURL: rekorURL, - HashAlgorithm: crypto.SHA256, - TSACertChainPath: tsaCertChain, - IgnoreSCT: skipSCT, - IgnoreTlog: skipTlogVerify, - MaxWorkers: 10, + IgnoreSCT: true, + MaxWorkers: 10, + AllowCertificateChain: true, } - args := []string{imageRef} - - return cmd.Exec(context.Background(), args) + initVerifyCmd(&cmd) + return cmd.Exec(context.Background(), []string{imageRef}) } -var verifyBlobKeylessWithCARoots = func(blobRef string, - sig string, - caroots string, // filename of a PEM file with CA Roots certificates - intermediates string, // empty or filename of a PEM file with Intermediate certificates - certFile string, // filename of a PEM file with the codesigning certificate - skipSCT bool, - skipTlogVerify bool) error { +var verifyBlobWithTrustedRoot = func(blobRef, bundlePath, trustedRootPath string) error { cmd := cliverify.VerifyBlobCmd{ + KeyOpts: options.KeyOpts{ + BundlePath: bundlePath, + }, CertVerifyOptions: options.CertVerifyOptions{ CertOidcIssuerRegexp: ".*", CertIdentityRegexp: ".*", }, - SigRef: sig, - CertRef: certFile, - CARoots: caroots, - CAIntermediates: intermediates, - IgnoreSCT: skipSCT, - IgnoreTlog: skipTlogVerify, + TrustedRootPath: trustedRootPath, + IgnoreSCT: true, + AllowCertificateChain: true, } return cmd.Exec(context.Background(), blobRef) } // Used to verify local images stored on disk -var verifyLocal = func(keyRef, path string, checkClaims bool, annotations map[string]interface{}, attachment string) error { +var verifyLocal = func(keyRef, path string, checkClaims bool, annotations map[string]interface{}) error { cmd := cliverify.VerifyCommand{ - KeyRef: keyRef, - RekorURL: rekorURL, - CheckClaims: checkClaims, - Annotations: sigs.AnnotationsMap{Annotations: annotations}, - Attachment: attachment, - HashAlgorithm: crypto.SHA256, - LocalImage: true, - MaxWorkers: 10, + KeyRef: keyRef, + CheckClaims: checkClaims, + Annotations: sigs.AnnotationsMap{Annotations: annotations}, + LocalImage: true, + MaxWorkers: 10, } args := []string{path} - return cmd.Exec(context.Background(), args) -} - -var verifyOffline = func(keyRef, imageRef string, checkClaims bool, annotations map[string]interface{}, attachment string) error { - cmd := cliverify.VerifyCommand{ - KeyRef: keyRef, - RekorURL: "notreal", - Offline: true, - CheckClaims: checkClaims, - Annotations: sigs.AnnotationsMap{Annotations: annotations}, - Attachment: attachment, - HashAlgorithm: crypto.SHA256, - MaxWorkers: 10, - } - - args := []string{imageRef} - + initVerifyCmd(&cmd) return cmd.Exec(context.Background(), args) } @@ -527,7 +492,7 @@ func setLocalEnv(t *testing.T, dir string) error { // copyFile copies a file from a source to a destination. func copyFile(src, dst string) error { - f, err := os.Open(src) + f, err := os.Open(src) //nolint: gosec // test helper if err != nil { return fmt.Errorf("error opening source file: %w", err) } @@ -801,3 +766,44 @@ func generateCertificateBundle(genIntermediate bool) ( return caCertBuf, caPrivKeyBuf, caIntermediateCertBuf, caIntermediatePrivKeyBuf, certBuf, certBundleBuf, nil } + +var rekorSigningConfig = func() *root.SigningConfig { + sc := signcommon.NewEmptySigningConfig() + sc.WithRekorLogURLs(root.Service{ + URL: rekorURL, + MajorAPIVersion: 1, + ValidityPeriodStart: time.Now().Add(-24 * time.Hour), + }) + sc.WithFulcioCertificateAuthorityURLs(root.Service{ + URL: fulcioURL, + MajorAPIVersion: 1, + ValidityPeriodStart: time.Now().Add(-24 * time.Hour), + }) + return sc +}() + +func fetchReferrerBundle(t *testing.T, ref name.Digest, remoteOpts ...ociremote.Option) []byte { + idx, err := ociremote.Referrers(ref, "application/vnd.dev.sigstore.bundle.v0.3+json", remoteOpts...) + must(err, t) + if len(idx.Manifests) != 1 { + t.Fatalf("expected one referrer, got %d", len(idx.Manifests)) + } + + refDigest := ref.Context().Digest(idx.Manifests[0].Digest.String()) + img, err := ociremote.SignedImage(refDigest, remoteOpts...) + must(err, t) + + layers, err := img.Layers() + must(err, t) + if len(layers) != 1 { + t.Fatalf("expected one layer in referrer, got %d", len(layers)) + } + + rc, err := layers[0].Uncompressed() + must(err, t) + defer rc.Close() + + bundleBytes, err := io.ReadAll(rc) + must(err, t) + return bundleBytes +}