From ff8128a9dff72d9f781eda0e46da49f1b1185234 Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Wed, 9 Oct 2024 15:29:00 -0400 Subject: [PATCH 1/3] Add bundle create helper command Signed-off-by: Zach Steindler --- cmd/cosign/cli/bundle.go | 71 ++++++ cmd/cosign/cli/bundle/bundle.go | 214 ++++++++++++++++++ cmd/cosign/cli/bundle/bundle_test.go | 99 ++++++++ cmd/cosign/cli/commands.go | 1 + cmd/cosign/cli/options/bundle.go | 79 +++++++ cmd/cosign/cli/verify/verify_blob.go | 2 +- .../cli/verify/verify_blob_attestation.go | 2 +- cmd/cosign/cli/verify/verify_bundle.go | 189 ++++++++++++++-- cmd/cosign/cli/verify/verify_bundle_test.go | 97 ++++++++ doc/cosign.md | 1 + doc/cosign_bundle.md | 27 +++ doc/cosign_bundle_create.md | 42 ++++ 12 files changed, 807 insertions(+), 17 deletions(-) create mode 100644 cmd/cosign/cli/bundle.go create mode 100644 cmd/cosign/cli/bundle/bundle.go create mode 100644 cmd/cosign/cli/bundle/bundle_test.go create mode 100644 cmd/cosign/cli/options/bundle.go create mode 100644 cmd/cosign/cli/verify/verify_bundle_test.go create mode 100644 doc/cosign_bundle.md create mode 100644 doc/cosign_bundle_create.md diff --git a/cmd/cosign/cli/bundle.go b/cmd/cosign/cli/bundle.go new file mode 100644 index 00000000000..b02a870ef9d --- /dev/null +++ b/cmd/cosign/cli/bundle.go @@ -0,0 +1,71 @@ +// +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cli + +import ( + "context" + + "github.com/spf13/cobra" + + "github.com/sigstore/cosign/v2/cmd/cosign/cli/bundle" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" +) + +func Bundle() *cobra.Command { + cmd := &cobra.Command{ + Use: "bundle", + Short: "Interact with a Sigstore protobuf bundle", + Long: "Tools for interacting with a Sigstore protobuf bundle", + } + + cmd.AddCommand(bundleCreate()) + + return cmd +} + +func bundleCreate() *cobra.Command { + o := &options.BundleCreateOptions{} + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a Sigstore protobuf bundle", + Long: "Create a Sigstore protobuf bundle by supplying signed material", + RunE: func(cmd *cobra.Command, _ []string) error { + bundleCreateCmd := &bundle.CreateCmd{ + Artifact: o.Artifact, + AttestationPath: o.AttestationPath, + BundlePath: o.BundlePath, + CertificatePath: o.CertificatePath, + IgnoreTlog: o.IgnoreTlog, + KeyRef: o.KeyRef, + Out: o.Out, + RekorURL: o.RekorURL, + RFC3161TimestampPath: o.RFC3161TimestampPath, + SignaturePath: o.SignaturePath, + Sk: o.Sk, + Slot: o.Slot, + } + + ctx, cancel := context.WithTimeout(cmd.Context(), ro.Timeout) + defer cancel() + + return bundleCreateCmd.Exec(ctx) + }, + } + + o.AddFlags(cmd) + return cmd +} diff --git a/cmd/cosign/cli/bundle/bundle.go b/cmd/cosign/cli/bundle/bundle.go new file mode 100644 index 00000000000..ee4eb7c7a80 --- /dev/null +++ b/cmd/cosign/cli/bundle/bundle.go @@ -0,0 +1,214 @@ +// +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bundle + +import ( + "context" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "os" + + "github.com/secure-systems-lab/go-securesystemslib/dsse" + "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/signature" + + "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/rekor" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/verify" + "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/cosign/v2/pkg/cosign/bundle" + "github.com/sigstore/cosign/v2/pkg/cosign/pivkey" + "github.com/sigstore/cosign/v2/pkg/cosign/pkcs11key" + sigs "github.com/sigstore/cosign/v2/pkg/signature" +) + +type CreateCmd struct { + Artifact string + AttestationPath string + BundlePath string + CertificatePath string + IgnoreTlog bool + KeyRef string + Out string + RekorURL string + RFC3161TimestampPath string + SignaturePath string + Sk bool + Slot string +} + +func (c *CreateCmd) Exec(ctx context.Context) (err error) { + if c.Artifact == "" { + return fmt.Errorf("must supply --artifact") + } + + // We require some signature + if options.NOf(c.BundlePath, c.SignaturePath) == 0 { + return fmt.Errorf("must at least supply signature via --bundle or --signature") + } + + var cert *x509.Certificate + var envelope dsse.Envelope + var rekorClient *client.Rekor + var sigBytes, signedTimestamp []byte + var sigVerifier signature.Verifier + + if c.BundlePath != "" { + b, err := cosign.FetchLocalSignedPayloadFromPath(c.BundlePath) + if err != nil { + return err + } + + if b.Cert != "" { + certPEM, err := base64.StdEncoding.DecodeString(b.Cert) + if err != nil { + return err + } + certs, err := cryptoutils.UnmarshalCertificatesFromPEM(certPEM) + if err != nil { + return err + } + if len(certs) == 0 { + return fmt.Errorf("no certs found in bundle") + } + cert = certs[0] + } + + if b.Base64Signature != "" { + // Could be a DSSE envelope or plain signature + signature, err := base64.StdEncoding.DecodeString(b.Base64Signature) + if err != nil { + return err + } + + // See if DSSE JSON unmashalling succeeds + err = json.Unmarshal(signature, &envelope) + if err != nil { + // Guess that it is a plain signature + sigBytes = signature + } + } + } + + if c.SignaturePath != "" { + sigBytes, err = os.ReadFile(c.SignaturePath) + if err != nil { + return err + } + } + + if c.RFC3161TimestampPath != "" { + timestampBytes, err := os.ReadFile(c.RFC3161TimestampPath) + if err != nil { + return err + } + + var rfc3161Timestamp bundle.RFC3161Timestamp + err = json.Unmarshal(timestampBytes, &rfc3161Timestamp) + if err != nil { + return err + } + + signedTimestamp = rfc3161Timestamp.SignedRFC3161Timestamp + } + + if c.CertificatePath != "" { + certBytes, err := os.ReadFile(c.CertificatePath) + if err != nil { + return err + } + + certDecoded, err := base64.StdEncoding.DecodeString(string(certBytes)) + if err != nil { + return err + } + + block, _ := pem.Decode(certDecoded) + if block == nil { + return fmt.Errorf("unable to decode provided certificate") + } + + cert, err = x509.ParseCertificate(block.Bytes) + if err != nil { + return err + } + } + + if c.AttestationPath != "" { + attestationBytes, err := os.ReadFile(c.AttestationPath) + if err != nil { + return err + } + + err = json.Unmarshal(attestationBytes, &envelope) + if err != nil { + return err + } + } + + if c.KeyRef != "" { + sigVerifier, err = sigs.PublicKeyFromKeyRef(ctx, c.KeyRef) + if err != nil { + return fmt.Errorf("loading public key: %w", err) + } + pkcs11Key, ok := sigVerifier.(*pkcs11key.Key) + if ok { + defer pkcs11Key.Close() + } + } else if c.Sk { + sk, err := pivkey.GetKeyWithSlot(c.Slot) + if err != nil { + return fmt.Errorf("opening piv token: %w", err) + } + defer sk.Close() + sigVerifier, err = sk.Verifier() + if err != nil { + return fmt.Errorf("loading public key from token: %w", err) + } + } + + if c.RekorURL != "" { + rekorClient, err = rekor.NewClient(c.RekorURL) + if err != nil { + return err + } + } + + bundle, err := verify.AssembleNewBundle(ctx, sigBytes, signedTimestamp, &envelope, c.Artifact, cert, c.IgnoreTlog, sigVerifier, nil, rekorClient) + if err != nil { + return err + } + + bundleBytes, err := bundle.MarshalJSON() + if err != nil { + return err + } + + if c.Out != "" { + err = os.WriteFile(c.Out, bundleBytes, 0600) + if err != nil { + return err + } + } else { + fmt.Println(string(bundleBytes)) + } + + return nil +} diff --git a/cmd/cosign/cli/bundle/bundle_test.go b/cmd/cosign/cli/bundle/bundle_test.go new file mode 100644 index 00000000000..051e4f4374e --- /dev/null +++ b/cmd/cosign/cli/bundle/bundle_test.go @@ -0,0 +1,99 @@ +// +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package bundle + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "os" + "path/filepath" + "testing" + + sgBundle "github.com/sigstore/sigstore-go/pkg/bundle" +) + +func TestCreateCmd(t *testing.T) { + ctx := context.Background() + + artifact := "hello world" + digest := sha256.Sum256([]byte(artifact)) + + td := t.TempDir() + artifactPath := filepath.Join(td, "artifact") + err := os.WriteFile(artifactPath, []byte(artifact), 0600) + checkErr(t, err) + + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + checkErr(t, err) + sigBytes, err := privateKey.Sign(rand.Reader, digest[:], crypto.SHA256) + checkErr(t, err) + + signature := base64.StdEncoding.EncodeToString(sigBytes) + sigPath := filepath.Join(td, "sig") + err = os.WriteFile(sigPath, []byte(signature), 0600) + checkErr(t, err) + + publicKeyPath := filepath.Join(td, "key.pub") + pubKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) + checkErr(t, err) + pemBlock := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: pubKeyBytes, + } + err = os.WriteFile(publicKeyPath, pem.EncodeToMemory(pemBlock), 0600) + checkErr(t, err) + + outPath := filepath.Join(td, "bundle.sigstore.json") + + bundleCreate := CreateCmd{ + Artifact: artifactPath, + KeyRef: publicKeyPath, + IgnoreTlog: true, + Out: outPath, + SignaturePath: sigPath, + } + + err = bundleCreate.Exec(ctx) + checkErr(t, err) + + b, err := sgBundle.LoadJSONFromPath(outPath) + checkErr(t, err) + + if b.Bundle.VerificationMaterial == nil { + t.Fatal("bundle does not have verification material") + } + + if b.Bundle.VerificationMaterial.GetPublicKey() == nil { + t.Fatal("bundle verification material does not have public key") + } + + if b.Bundle.GetMessageSignature() == nil { + t.Fatal("bundle does not have message signature") + } +} + +func checkErr(t *testing.T, err error) { + if err != nil { + t.Fatal(err) + } +} diff --git a/cmd/cosign/cli/commands.go b/cmd/cosign/cli/commands.go index 6c67e890c40..0a324c86ae6 100644 --- a/cmd/cosign/cli/commands.go +++ b/cmd/cosign/cli/commands.go @@ -95,6 +95,7 @@ func New() *cobra.Command { cmd.AddCommand(Attach()) cmd.AddCommand(Attest()) cmd.AddCommand(AttestBlob()) + cmd.AddCommand(Bundle()) cmd.AddCommand(Clean()) cmd.AddCommand(Debug()) cmd.AddCommand(Tree()) diff --git a/cmd/cosign/cli/options/bundle.go b/cmd/cosign/cli/options/bundle.go new file mode 100644 index 00000000000..68c1cdc11c3 --- /dev/null +++ b/cmd/cosign/cli/options/bundle.go @@ -0,0 +1,79 @@ +// +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package options + +import ( + "github.com/spf13/cobra" +) + +type BundleCreateOptions struct { + Artifact string + AttestationPath string + BundlePath string + CertificatePath string + IgnoreTlog bool + KeyRef string + Out string + RekorURL string + RFC3161TimestampPath string + SignaturePath string + Sk bool + Slot string +} + +var _ Interface = (*BundleCreateOptions)(nil) + +func (o *BundleCreateOptions) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Artifact, "artifact", "", + "path to artifact FILE") + + cmd.Flags().StringVar(&o.AttestationPath, "attestation", "", + "path to attestation FILE") + + cmd.Flags().StringVar(&o.BundlePath, "bundle", "", + "path to old format bundle FILE") + + cmd.Flags().StringVar(&o.CertificatePath, "certificate", "", + "path to the signing certificate, likely from Fulco.") + + cmd.Flags().BoolVar(&o.IgnoreTlog, "ignore-tlog", false, + "ignore transparency log verification, to be used when an artifact "+ + "signature has not been uploaded to the transparency log.") + + cmd.Flags().StringVar(&o.KeyRef, "key", "", + "path to the public key file, KMS URI or Kubernetes Secret") + + cmd.Flags().StringVar(&o.Out, "out", "", "path to output bundle") + + cmd.Flags().StringVar(&o.RekorURL, "rekor-url", "https://rekor.sigstore.dev", + "address of rekor STL server") + + cmd.Flags().StringVar(&o.RFC3161TimestampPath, "rfc3161-timestamp", "", + "path to RFC3161 timestamp FILE") + + cmd.Flags().StringVar(&o.SignaturePath, "signature", "", + "path to base64-encoded signature over attestation in DSSE format") + + cmd.Flags().BoolVar(&o.Sk, "sk", false, + "whether to use a hardware security key") + + cmd.Flags().StringVar(&o.Slot, "slot", "", + "security key slot to use for generated key (default: signature) "+ + "(authentication|signature|card-authentication|key-management)") + + cmd.MarkFlagsMutuallyExclusive("bundle", "certificate") + cmd.MarkFlagsMutuallyExclusive("bundle", "signature") +} diff --git a/cmd/cosign/cli/verify/verify_blob.go b/cmd/cosign/cli/verify/verify_blob.go index 79475c90d80..24de2dbbf52 100644 --- a/cmd/cosign/cli/verify/verify_blob.go +++ b/cmd/cosign/cli/verify/verify_blob.go @@ -96,7 +96,7 @@ func (c *VerifyBlobCmd) Exec(ctx context.Context, blobRef string) error { if options.NOf(c.RFC3161TimestampPath, c.TSACertChainPath, c.RekorURL, c.CertChain, c.CARoots, c.CAIntermediates, c.CertRef, c.SigRef, c.SCTRef) > 1 { return fmt.Errorf("when using --new-bundle-format, please supply signed content with --bundle and verification content with --trusted-root") } - err := verifyNewBundle(ctx, c.BundlePath, c.TrustedRootPath, c.KeyRef, c.Slot, c.CertVerifyOptions.CertOidcIssuer, c.CertVerifyOptions.CertOidcIssuerRegexp, c.CertVerifyOptions.CertIdentity, c.CertVerifyOptions.CertIdentityRegexp, c.CertGithubWorkflowTrigger, c.CertGithubWorkflowSHA, c.CertGithubWorkflowName, c.CertGithubWorkflowRepository, c.CertGithubWorkflowRef, blobRef, c.Sk, c.IgnoreTlog, c.UseSignedTimestamps, c.IgnoreSCT) + _, err := verifyNewBundle(ctx, c.BundlePath, c.TrustedRootPath, c.KeyRef, c.Slot, c.CertVerifyOptions.CertOidcIssuer, c.CertVerifyOptions.CertOidcIssuerRegexp, c.CertVerifyOptions.CertIdentity, c.CertVerifyOptions.CertIdentityRegexp, c.CertGithubWorkflowTrigger, c.CertGithubWorkflowSHA, c.CertGithubWorkflowName, c.CertGithubWorkflowRepository, c.CertGithubWorkflowRef, blobRef, c.Sk, c.IgnoreTlog, c.UseSignedTimestamps, c.IgnoreSCT) if err == nil { ui.Infof(ctx, "Verified OK") } diff --git a/cmd/cosign/cli/verify/verify_blob_attestation.go b/cmd/cosign/cli/verify/verify_blob_attestation.go index 3f2c33cc63b..061e46d6383 100644 --- a/cmd/cosign/cli/verify/verify_blob_attestation.go +++ b/cmd/cosign/cli/verify/verify_blob_attestation.go @@ -96,7 +96,7 @@ func (c *VerifyBlobAttestationCommand) Exec(ctx context.Context, artifactPath st if options.NOf(c.RFC3161TimestampPath, c.TSACertChainPath, c.RekorURL, c.CertChain, c.CARoots, c.CAIntermediates, c.CertRef, c.SCTRef) > 1 { return fmt.Errorf("when using --new-bundle-format, please supply signed content with --bundle and verification content with --trusted-root") } - err = verifyNewBundle(ctx, c.BundlePath, c.TrustedRootPath, c.KeyRef, c.Slot, c.CertVerifyOptions.CertOidcIssuer, c.CertVerifyOptions.CertOidcIssuerRegexp, c.CertVerifyOptions.CertIdentity, c.CertVerifyOptions.CertIdentityRegexp, c.CertGithubWorkflowTrigger, c.CertGithubWorkflowSHA, c.CertGithubWorkflowName, c.CertGithubWorkflowRepository, c.CertGithubWorkflowRef, artifactPath, c.Sk, c.IgnoreTlog, c.UseSignedTimestamps, c.IgnoreSCT) + _, err = verifyNewBundle(ctx, c.BundlePath, c.TrustedRootPath, c.KeyRef, c.Slot, c.CertVerifyOptions.CertOidcIssuer, c.CertVerifyOptions.CertOidcIssuerRegexp, c.CertVerifyOptions.CertIdentity, c.CertVerifyOptions.CertIdentityRegexp, c.CertGithubWorkflowTrigger, c.CertGithubWorkflowSHA, c.CertGithubWorkflowName, c.CertGithubWorkflowRepository, c.CertGithubWorkflowRef, artifactPath, c.Sk, c.IgnoreTlog, c.UseSignedTimestamps, c.IgnoreSCT) if err == nil { fmt.Fprintln(os.Stderr, "Verified OK") } diff --git a/cmd/cosign/cli/verify/verify_bundle.go b/cmd/cosign/cli/verify/verify_bundle.go index 01921be7ffb..2245c2761d7 100644 --- a/cmd/cosign/cli/verify/verify_bundle.go +++ b/cmd/cosign/cli/verify/verify_bundle.go @@ -18,14 +18,29 @@ package verify import ( "bytes" "context" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/json" "fmt" "time" + "github.com/secure-systems-lab/go-securesystemslib/dsse" + protobundle "github.com/sigstore/protobuf-specs/gen/pb-go/bundle/v1" + protocommon "github.com/sigstore/protobuf-specs/gen/pb-go/common/v1" + protodsse "github.com/sigstore/protobuf-specs/gen/pb-go/dsse" + protorekor "github.com/sigstore/protobuf-specs/gen/pb-go/rekor/v1" + "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/tle" sgbundle "github.com/sigstore/sigstore-go/pkg/bundle" "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" "github.com/sigstore/sigstore-go/pkg/root" "github.com/sigstore/sigstore-go/pkg/verify" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/sigstore/cosign/v2/pkg/cosign" "github.com/sigstore/cosign/v2/pkg/cosign/pivkey" sigs "github.com/sigstore/cosign/v2/pkg/signature" ) @@ -39,10 +54,10 @@ func (v *verifyTrustedMaterial) PublicKeyVerifier(hint string) (root.TimeConstra return v.keyTrustedMaterial.PublicKeyVerifier(hint) } -func verifyNewBundle(ctx context.Context, bundlePath, trustedRootPath, keyRef, slot, certOIDCIssuer, certOIDCIssuerRegex, certIdentity, certIdentityRegexp, githubWorkflowTrigger, githubWorkflowSHA, githubWorkflowName, githubWorkflowRepository, githubWorkflowRef, artifactRef string, sk, ignoreTlog, useSignedTimestamps, ignoreSCT bool) error { +func verifyNewBundle(ctx context.Context, bundlePath, trustedRootPath, keyRef, slot, certOIDCIssuer, certOIDCIssuerRegex, certIdentity, certIdentityRegexp, githubWorkflowTrigger, githubWorkflowSHA, githubWorkflowName, githubWorkflowRepository, githubWorkflowRef, artifactRef string, sk, ignoreTlog, useSignedTimestamps, ignoreSCT bool) (*verify.VerificationResult, error) { bundle, err := sgbundle.LoadJSONFromPath(bundlePath) if err != nil { - return err + return nil, err } var trustedroot *root.TrustedRoot @@ -51,12 +66,12 @@ func verifyNewBundle(ctx context.Context, bundlePath, trustedRootPath, keyRef, s // Assume we're using public good instance; fetch via TUF trustedroot, err = root.FetchTrustedRoot() if err != nil { - return err + return nil, err } } else { trustedroot, err = root.NewTrustedRootFromPath(trustedRootPath) if err != nil { - return err + return nil, err } } @@ -66,7 +81,7 @@ func verifyNewBundle(ctx context.Context, bundlePath, trustedRootPath, keyRef, s if keyRef != "" { signatureVerifier, err := sigs.PublicKeyFromKeyRef(ctx, keyRef) if err != nil { - return err + return nil, err } newExpiringKey := root.NewExpiringKey(signatureVerifier, time.Time{}, time.Time{}) @@ -76,12 +91,12 @@ func verifyNewBundle(ctx context.Context, bundlePath, trustedRootPath, keyRef, s } else if sk { s, err := pivkey.GetKeyWithSlot(slot) if err != nil { - return fmt.Errorf("opening piv token: %w", err) + return nil, fmt.Errorf("opening piv token: %w", err) } defer s.Close() signatureVerifier, err := s.Verifier() if err != nil { - return fmt.Errorf("loading public key from token: %w", err) + return nil, fmt.Errorf("loading public key from token: %w", err) } newExpiringKey := root.NewExpiringKey(signatureVerifier, time.Time{}, time.Time{}) @@ -95,7 +110,7 @@ func verifyNewBundle(ctx context.Context, bundlePath, trustedRootPath, keyRef, s verificationMaterial := bundle.GetVerificationMaterial() if verificationMaterial == nil { - return fmt.Errorf("no verification material in bundle") + return nil, fmt.Errorf("no verification material in bundle") } if verificationMaterial.GetPublicKey() != nil { @@ -103,12 +118,12 @@ func verifyNewBundle(ctx context.Context, bundlePath, trustedRootPath, keyRef, s } else { sanMatcher, err := verify.NewSANMatcher(certIdentity, certIdentityRegexp) if err != nil { - return err + return nil, err } issuerMatcher, err := verify.NewIssuerMatcher(certOIDCIssuer, certOIDCIssuerRegex) if err != nil { - return err + return nil, err } extensions := certificate.Extensions{ @@ -121,7 +136,7 @@ func verifyNewBundle(ctx context.Context, bundlePath, trustedRootPath, keyRef, s certIdentity, err := verify.NewCertificateIdentity(sanMatcher, issuerMatcher, extensions) if err != nil { - return err + return nil, err } identityPolicies = append(identityPolicies, verify.WithCertificateIdentity(certIdentity)) @@ -149,15 +164,159 @@ func verifyNewBundle(ctx context.Context, bundlePath, trustedRootPath, keyRef, s // Perform verification payload, err := payloadBytes(artifactRef) if err != nil { - return err + return nil, err } buf := bytes.NewBuffer(payload) sev, err := verify.NewSignedEntityVerifier(trustedmaterial, verifierConfig...) if err != nil { - return err + return nil, err } - _, err = sev.Verify(bundle, verify.NewPolicy(verify.WithArtifact(buf), identityPolicies...)) - return err + return sev.Verify(bundle, verify.NewPolicy(verify.WithArtifact(buf), identityPolicies...)) +} + +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 { + return nil, err + } + buf := bytes.NewBuffer(payload) + digest := sha256.Sum256(buf.Bytes()) + + pb := &protobundle.Bundle{ + MediaType: "application/vnd.dev.sigstore.bundle+json;version=0.3", + VerificationMaterial: &protobundle.VerificationMaterial{}, + } + + if envelope != nil && len(envelope.Signatures) > 0 { + sigDecode, err := base64.StdEncoding.DecodeString(envelope.Signatures[0].Sig) + if err != nil { + return nil, err + } + + sig := &protodsse.Signature{ + Sig: sigDecode, + } + + payloadDecode, err := base64.StdEncoding.DecodeString(envelope.Payload) + if err != nil { + return nil, err + } + + pb.Content = &protobundle.Bundle_DsseEnvelope{ + DsseEnvelope: &protodsse.Envelope{ + Payload: payloadDecode, + PayloadType: envelope.PayloadType, + Signatures: []*protodsse.Signature{sig}, + }, + } + } else { + pb.Content = &protobundle.Bundle_MessageSignature{ + MessageSignature: &protocommon.MessageSignature{ + MessageDigest: &protocommon.HashOutput{ + Algorithm: protocommon.HashAlgorithm_SHA2_256, + Digest: digest[:], + }, + Signature: sigBytes, + }, + } + } + + if cert != nil { + pb.VerificationMaterial.Content = &protobundle.VerificationMaterial_Certificate{ + Certificate: &protocommon.X509Certificate{ + RawBytes: cert.Raw, + }, + } + } else if sigVerifier != nil { + pub, err := sigVerifier.PublicKey(pkOpts...) + if err != nil { + return nil, err + } + pubKeyBytes, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return nil, err + } + hashedBytes := sha256.Sum256(pubKeyBytes) + + pb.VerificationMaterial.Content = &protobundle.VerificationMaterial_PublicKey{ + PublicKey: &protocommon.PublicKeyIdentifier{ + Hint: base64.StdEncoding.EncodeToString(hashedBytes[:]), + }, + } + } + + if len(signedTimestamp) > 0 { + ts := &protocommon.RFC3161SignedTimestamp{ + SignedTimestamp: signedTimestamp, + } + + pb.VerificationMaterial.TimestampVerificationData = &protobundle.TimestampVerificationData{ + Rfc3161Timestamps: []*protocommon.RFC3161SignedTimestamp{ts}, + } + } + + if !ignoreTlog { + var pem []byte + var err error + if cert != nil { + pem, err = cryptoutils.MarshalCertificateToPEM(cert) + if err != nil { + return nil, err + } + } else if sigVerifier != nil { + pub, err := sigVerifier.PublicKey(pkOpts...) + if err != nil { + return nil, err + } + pem, err = cryptoutils.MarshalPublicKeyToPEM(pub) + if err != nil { + return nil, err + } + } + var sigB64 string + var payload []byte + if envelope != nil { + payload, err = json.Marshal(*envelope) + if err != nil { + return nil, err + } + } else { + sigB64 = base64.StdEncoding.EncodeToString(sigBytes) + payload = buf.Bytes() + } + + tlogEntries, err := cosign.FindTlogEntry(ctx, rekorClient, sigB64, payload, pem) + if err != nil { + return nil, err + } + if len(tlogEntries) == 0 { + return nil, fmt.Errorf("unable to find tlog entry") + } + // Attempt to verify with the earliest integrated entry + var earliestLogEntry models.LogEntryAnon + var earliestLogEntryTime *time.Time + for _, e := range tlogEntries { + entryTime := time.Unix(*e.IntegratedTime, 0) + if earliestLogEntryTime == nil || entryTime.Before(*earliestLogEntryTime) { + earliestLogEntryTime = &entryTime + earliestLogEntry = e + } + } + + tlogEntry, err := tle.GenerateTransparencyLogEntry(earliestLogEntry) + if err != nil { + return nil, err + } + + pb.VerificationMaterial.TlogEntries = []*protorekor.TransparencyLogEntry{tlogEntry} + } + + b, err := sgbundle.NewBundle(pb) + if err != nil { + return nil, err + } + + return b, nil } diff --git a/cmd/cosign/cli/verify/verify_bundle_test.go b/cmd/cosign/cli/verify/verify_bundle_test.go new file mode 100644 index 00000000000..0bfad165f13 --- /dev/null +++ b/cmd/cosign/cli/verify/verify_bundle_test.go @@ -0,0 +1,97 @@ +// +// Copyright 2024 The Sigstore Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package verify + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/sha256" + "crypto/x509" + "encoding/pem" + "os" + "path/filepath" + "testing" + + "github.com/sigstore/cosign/v2/pkg/signature" +) + +func TestVerifyBundleWithKey(t *testing.T) { + // First assemble bundle + ctx := context.Background() + artifact := "hello world" + digest := sha256.Sum256([]byte(artifact)) + + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + checkErr(t, err) + sigBytes, err := privateKey.Sign(rand.Reader, digest[:], crypto.SHA256) + checkErr(t, err) + + td := t.TempDir() + artifactPath := filepath.Join(td, "artifact") + err = os.WriteFile(artifactPath, []byte(artifact), 0600) + checkErr(t, err) + + pubKeyBytes, err := x509.MarshalPKIXPublicKey(&privateKey.PublicKey) + checkErr(t, err) + pemBlock := &pem.Block{ + Type: "PUBLIC KEY", + Bytes: pubKeyBytes, + } + verifier, err := signature.LoadPublicKeyRaw( + pem.EncodeToMemory(pemBlock), crypto.SHA256, + ) + checkErr(t, err) + + bundle, err := AssembleNewBundle(ctx, sigBytes, nil, nil, artifactPath, nil, + true, verifier, nil, nil, + ) + checkErr(t, err) + + if bundle == nil { + t.Fatal("invalid bundle") + } + + // The verify assembled bundle + trustedRootPath := filepath.Join(td, "trusted_root.json") + err = os.WriteFile(trustedRootPath, []byte(`{"mediaType":"application/vnd.dev.sigstore.trustedroot+json;version=0.1"}`), 0600) + checkErr(t, err) + + publicKeyPath := filepath.Join(td, "key.pub") + err = os.WriteFile(publicKeyPath, pem.EncodeToMemory(pemBlock), 0600) + checkErr(t, err) + + bundlePath := filepath.Join(td, "bundle.sigstore.json") + bundleBytes, err := bundle.MarshalJSON() + checkErr(t, err) + err = os.WriteFile(bundlePath, bundleBytes, 0600) + checkErr(t, err) + + result, err := verifyNewBundle(ctx, bundlePath, trustedRootPath, publicKeyPath, "", "", "", "", "", "", "", "", "", "", artifactPath, false, true, false, true) + checkErr(t, err) + + if result == nil { + t.Fatal("invalid verification result") + } +} + +func checkErr(t *testing.T, err error) { + if err != nil { + t.Fatal(err) + } +} diff --git a/doc/cosign.md b/doc/cosign.md index d7f90aae469..cfb5c77eb8a 100644 --- a/doc/cosign.md +++ b/doc/cosign.md @@ -16,6 +16,7 @@ A tool for Container Signing, Verification and Storage in an OCI registry. * [cosign attach](cosign_attach.md) - Provides utilities for attaching artifacts to other artifacts in a registry * [cosign attest](cosign_attest.md) - Attest the supplied container image. * [cosign attest-blob](cosign_attest-blob.md) - Attest the supplied blob. +* [cosign bundle](cosign_bundle.md) - Interact with a Sigstore protobuf bundle * [cosign clean](cosign_clean.md) - Remove all signatures from an image. * [cosign completion](cosign_completion.md) - Generate completion script * [cosign copy](cosign_copy.md) - Copy the supplied container image and signatures. diff --git a/doc/cosign_bundle.md b/doc/cosign_bundle.md new file mode 100644 index 00000000000..27c77009d25 --- /dev/null +++ b/doc/cosign_bundle.md @@ -0,0 +1,27 @@ +## cosign bundle + +Interact with a Sigstore protobuf bundle + +### Synopsis + +Tools for interacting with a Sigstore protobuf bundle + +### Options + +``` + -h, --help help for bundle +``` + +### Options inherited from parent commands + +``` + --output-file string log output to a file + -t, --timeout duration timeout for commands (default 3m0s) + -d, --verbose log debug output +``` + +### SEE ALSO + +* [cosign](cosign.md) - A tool for Container Signing, Verification and Storage in an OCI registry. +* [cosign bundle create](cosign_bundle_create.md) - Create a Sigstore protobuf bundle + diff --git a/doc/cosign_bundle_create.md b/doc/cosign_bundle_create.md new file mode 100644 index 00000000000..5fe3f6a76f9 --- /dev/null +++ b/doc/cosign_bundle_create.md @@ -0,0 +1,42 @@ +## cosign bundle create + +Create a Sigstore protobuf bundle + +### Synopsis + +Create a Sigstore protobuf bundle by supplying signed material + +``` +cosign bundle create [flags] +``` + +### Options + +``` + --artifact string path to artifact FILE + --attestation string path to attestation FILE + --bundle string path to old format bundle FILE + --certificate string path to the signing certificate, likely from Fulco. + -h, --help help for create + --ignore-tlog ignore transparency log verification, to be used when an artifact signature has not been uploaded to the transparency log. + --key string path to the public key file, KMS URI or Kubernetes Secret + --out string path to output bundle + --rekor-url string address of rekor STL server (default "https://rekor.sigstore.dev") + --rfc3161-timestamp string path to RFC3161 timestamp FILE + --signature string path to base64-encoded signature over attestation in DSSE format + --sk whether to use a hardware security key + --slot string security key slot to use for generated key (default: signature) (authentication|signature|card-authentication|key-management) +``` + +### Options inherited from parent commands + +``` + --output-file string log output to a file + -t, --timeout duration timeout for commands (default 3m0s) + -d, --verbose log debug output +``` + +### SEE ALSO + +* [cosign bundle](cosign_bundle.md) - Interact with a Sigstore protobuf bundle + From 0fff093a644d66914d9de4dcb076767c7f2f7bf2 Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Wed, 23 Oct 2024 17:14:36 -0400 Subject: [PATCH 2/3] Check for empty envelope Signed-off-by: Zach Steindler --- cmd/cosign/cli/verify/verify_bundle.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/cosign/cli/verify/verify_bundle.go b/cmd/cosign/cli/verify/verify_bundle.go index 2245c2761d7..8bbf14714cb 100644 --- a/cmd/cosign/cli/verify/verify_bundle.go +++ b/cmd/cosign/cli/verify/verify_bundle.go @@ -277,7 +277,7 @@ func AssembleNewBundle(ctx context.Context, sigBytes, signedTimestamp []byte, en } var sigB64 string var payload []byte - if envelope != nil { + if envelope != nil && len(envelope.Signatures) > 0 { payload, err = json.Marshal(*envelope) if err != nil { return nil, err From 0d55987bd62f5615a81e7d46a2598fd802b4acb0 Mon Sep 17 00:00:00 2001 From: Zach Steindler Date: Fri, 1 Nov 2024 13:59:03 -0400 Subject: [PATCH 3/3] Fix bug with detached signature Also add test for Fulcio certificate and old bundle format Signed-off-by: Zach Steindler --- cmd/cosign/cli/bundle/bundle.go | 7 +++- cmd/cosign/cli/bundle/bundle_test.go | 51 ++++++++++++++++++++++++++ cmd/cosign/cli/verify/verify_bundle.go | 14 ++----- 3 files changed, 60 insertions(+), 12 deletions(-) diff --git a/cmd/cosign/cli/bundle/bundle.go b/cmd/cosign/cli/bundle/bundle.go index ee4eb7c7a80..54778e45e99 100644 --- a/cmd/cosign/cli/bundle/bundle.go +++ b/cmd/cosign/cli/bundle/bundle.go @@ -108,7 +108,12 @@ func (c *CreateCmd) Exec(ctx context.Context) (err error) { } if c.SignaturePath != "" { - sigBytes, err = os.ReadFile(c.SignaturePath) + signatureB64, err := os.ReadFile(c.SignaturePath) + if err != nil { + return err + } + + sigBytes, err = base64.StdEncoding.DecodeString(string(signatureB64)) if err != nil { return err } diff --git a/cmd/cosign/cli/bundle/bundle_test.go b/cmd/cosign/cli/bundle/bundle_test.go index 051e4f4374e..4b64a7aea9c 100644 --- a/cmd/cosign/cli/bundle/bundle_test.go +++ b/cmd/cosign/cli/bundle/bundle_test.go @@ -24,12 +24,16 @@ import ( "crypto/sha256" "crypto/x509" "encoding/base64" + "encoding/json" "encoding/pem" "os" "path/filepath" "testing" + "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/cosign/v2/test" sgBundle "github.com/sigstore/sigstore-go/pkg/bundle" + "github.com/sigstore/sigstore/pkg/cryptoutils" ) func TestCreateCmd(t *testing.T) { @@ -43,6 +47,7 @@ func TestCreateCmd(t *testing.T) { err := os.WriteFile(artifactPath, []byte(artifact), 0600) checkErr(t, err) + // Test signing with a key privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) checkErr(t, err) sigBytes, err := privateKey.Sign(rand.Reader, digest[:], crypto.SHA256) @@ -90,6 +95,52 @@ func TestCreateCmd(t *testing.T) { if b.Bundle.GetMessageSignature() == nil { t.Fatal("bundle does not have message signature") } + + // Test using an identity certificate in an old bundle format + rootCert, rootKey, _ := test.GenerateRootCa() + leafCert, privKey, _ := test.GenerateLeafCert("subject", "oidc-issuer", rootCert, rootKey) + + sigBytes, err = privKey.Sign(rand.Reader, digest[:], crypto.SHA256) + checkErr(t, err) + + signedPayload := cosign.LocalSignedPayload{} + signedPayload.Base64Signature = base64.StdEncoding.EncodeToString(sigBytes) + + certBytes, err := cryptoutils.MarshalCertificateToPEM(leafCert) + checkErr(t, err) + + signedPayload.Cert = base64.StdEncoding.EncodeToString(certBytes) + bundleContents, err := json.Marshal(signedPayload) + checkErr(t, err) + + bundlePath := filepath.Join(td, "old-bundle.json") + err = os.WriteFile(bundlePath, bundleContents, 0600) + checkErr(t, err) + + bundleCreate = CreateCmd{ + Artifact: artifactPath, + BundlePath: bundlePath, + IgnoreTlog: true, + Out: outPath, + } + + err = bundleCreate.Exec(ctx) + checkErr(t, err) + + b, err = sgBundle.LoadJSONFromPath(outPath) + checkErr(t, err) + + if b.Bundle.VerificationMaterial == nil { + t.Fatal("bundle does not have verification material") + } + + if b.Bundle.VerificationMaterial.GetCertificate() == nil { + t.Fatal("bundle verification material does not have certificate") + } + + if b.Bundle.GetMessageSignature() == nil { + t.Fatal("bundle does not have message signature") + } } func checkErr(t *testing.T, err error) { diff --git a/cmd/cosign/cli/verify/verify_bundle.go b/cmd/cosign/cli/verify/verify_bundle.go index 8bbf14714cb..05a50ebd801 100644 --- a/cmd/cosign/cli/verify/verify_bundle.go +++ b/cmd/cosign/cli/verify/verify_bundle.go @@ -31,7 +31,6 @@ import ( protodsse "github.com/sigstore/protobuf-specs/gen/pb-go/dsse" protorekor "github.com/sigstore/protobuf-specs/gen/pb-go/rekor/v1" "github.com/sigstore/rekor/pkg/generated/client" - "github.com/sigstore/rekor/pkg/generated/models" "github.com/sigstore/rekor/pkg/tle" sgbundle "github.com/sigstore/sigstore-go/pkg/bundle" "github.com/sigstore/sigstore-go/pkg/fulcio/certificate" @@ -294,18 +293,11 @@ func AssembleNewBundle(ctx context.Context, sigBytes, signedTimestamp []byte, en if len(tlogEntries) == 0 { return nil, fmt.Errorf("unable to find tlog entry") } - // Attempt to verify with the earliest integrated entry - var earliestLogEntry models.LogEntryAnon - var earliestLogEntryTime *time.Time - for _, e := range tlogEntries { - entryTime := time.Unix(*e.IntegratedTime, 0) - if earliestLogEntryTime == nil || entryTime.Before(*earliestLogEntryTime) { - earliestLogEntryTime = &entryTime - earliestLogEntry = e - } + if len(tlogEntries) > 1 { + return nil, fmt.Errorf("too many tlog entries; should only have 1") } - tlogEntry, err := tle.GenerateTransparencyLogEntry(earliestLogEntry) + tlogEntry, err := tle.GenerateTransparencyLogEntry(tlogEntries[0]) if err != nil { return nil, err }