From 92d8ebcec28a67eacb418940b9977322a2112c4a Mon Sep 17 00:00:00 2001 From: Billy Lynch Date: Wed, 22 Mar 2023 17:10:08 -0400 Subject: [PATCH] Add gitsign verify. This adds a new subcommand that mirrors cosign verify for certificate claim verification. Previously we relied on `git verify-commit` for commit verification. While this did check that the signature was valid and it exists in rekor, it did not check whether the identity was what was expected, because Git does not give controls over this via the commit signature interface. This command provides this functionality. Also adds a warning to the output of the `git verify-commit` output warning users that that verification mechanism may not be complete. Signed-off-by: Billy Lynch --- .github/workflows/e2e.yaml | 1 + .gitignore | 4 +- docs/cli/gitsign.md | 1 + docs/cli/gitsign_verify.md | 38 +++++++ internal/cert/verify.go | 40 +++++++ internal/commands/root/root.go | 2 + internal/commands/root/verify.go | 59 ++-------- internal/commands/verify/verify.go | 151 +++++++++++++++++++++++++ internal/gitsign/gitsign.go | 136 +++++++++++++++++++++++ internal/gitsign/gitsign_test.go | 172 +++++++++++++++++++++++++++++ pkg/git/verify.go | 1 + pkg/rekor/rekor.go | 18 ++- 12 files changed, 564 insertions(+), 59 deletions(-) create mode 100644 docs/cli/gitsign_verify.md create mode 100644 internal/cert/verify.go create mode 100644 internal/commands/verify/verify.go create mode 100644 internal/gitsign/gitsign.go create mode 100644 internal/gitsign/gitsign_test.go diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml index 51f2b712..41fbd6b1 100644 --- a/.github/workflows/e2e.yaml +++ b/.github/workflows/e2e.yaml @@ -81,6 +81,7 @@ jobs: # Verify commit git verify-commit HEAD + gitsign verify --certificate-github-workflow-repository=${{ github.repository }} --certificate-github-workflow-sha=${{ github.sha }} --certificate-oidc-issuer="https://token.actions.githubusercontent.com" # Extra debug info git cat-file commit HEAD | sed -n '/BEGIN/, /END/p' | sed 's/^ //g' | sed 's/gpgsig //g' | sed 's/SIGNED MESSAGE/PKCS7/g' | openssl pkcs7 -print -print_certs -text diff --git a/.gitignore b/.gitignore index ca621017..dd907881 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,7 @@ *.swp dist/* .vscode/* -gitsign -gitsign-credential-cache +/gitsign +/gitsign-credential-cache !/cmd/gitsign-credential-cache vendor diff --git a/docs/cli/gitsign.md b/docs/cli/gitsign.md index 77d3a201..5a6ac329 100644 --- a/docs/cli/gitsign.md +++ b/docs/cli/gitsign.md @@ -24,5 +24,6 @@ gitsign [flags] * [gitsign attest](gitsign_attest.md) - add attestations to Git objects * [gitsign show](gitsign_show.md) - Show source predicate information +* [gitsign verify](gitsign_verify.md) - Show source predicate information * [gitsign version](gitsign_version.md) - print Gitsign version diff --git a/docs/cli/gitsign_verify.md b/docs/cli/gitsign_verify.md new file mode 100644 index 00000000..9bf6e541 --- /dev/null +++ b/docs/cli/gitsign_verify.md @@ -0,0 +1,38 @@ +## gitsign verify + +Show source predicate information + +### Synopsis + +Show source predicate information + +Prints an in-toto style predicate for the specified revision. +If no revision is specified, HEAD is used. + +This command is experimental, and its CLI surface may change. + +``` +gitsign verify [commit] [flags] +``` + +### Options + +``` + --certificate-github-workflow-name string contains the workflow claim from the GitHub OIDC Identity token that contains the name of the executed workflow. + --certificate-github-workflow-ref string contains the ref claim from the GitHub OIDC Identity token that contains the git ref that the workflow run was based upon. + --certificate-github-workflow-repository string contains the repository claim from the GitHub OIDC Identity token that contains the repository that the workflow run was based upon + --certificate-github-workflow-sha string contains the sha claim from the GitHub OIDC Identity token that contains the commit SHA that the workflow run was based upon. + --certificate-github-workflow-trigger string contains the event_name claim from the GitHub OIDC Identity token that contains the name of the event that triggered the workflow run + --certificate-identity string 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. + --certificate-identity-regexp string A regular expression alternative to --certificate-identity. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --certificate-identity or --certificate-identity-regexp must be set for keyless flows. + --certificate-oidc-issuer string The OIDC issuer expected in a valid Fulcio certificate, e.g. https://token.actions.githubusercontent.com or https://oauth2.sigstore.dev/auth. Either --certificate-oidc-issuer or --certificate-oidc-issuer-regexp must be set for keyless flows. + --certificate-oidc-issuer-regexp string A regular expression alternative to --certificate-oidc-issuer. Accepts the Go regular expression syntax described at https://golang.org/s/re2syntax. Either --certificate-oidc-issuer or --certificate-oidc-issuer-regexp must be set for keyless flows. + -h, --help help for verify + --insecure-ignore-sct when set, verification will not check that a certificate contains an embedded SCT, a proof of inclusion in a certificate transparency log + --sct string 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. +``` + +### SEE ALSO + +* [gitsign](gitsign.md) - Keyless Git signing with Sigstore! + diff --git a/internal/cert/verify.go b/internal/cert/verify.go new file mode 100644 index 00000000..daf079ba --- /dev/null +++ b/internal/cert/verify.go @@ -0,0 +1,40 @@ +// Copyright 2023 The Sigstore Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package cert + +import ( + "crypto/x509" + + "github.com/sigstore/cosign/v2/pkg/cosign" +) + +// Verifier verifies a given cert for a set of claims. +type Verifier interface { + Verify(cert *x509.Certificate) error +} + +// CosignVerifier borrows its certificate verification logic from cosign. +type CosignVerifier struct { + opts *cosign.CheckOpts +} + +func NewCosignVerifier(opts *cosign.CheckOpts) *CosignVerifier { + return &CosignVerifier{opts: opts} +} + +func (v *CosignVerifier) Verify(cert *x509.Certificate) error { + _, err := cosign.ValidateAndUnpackCert(cert, v.opts) + return err +} diff --git a/internal/commands/root/root.go b/internal/commands/root/root.go index 0ddf1114..c59be2d0 100644 --- a/internal/commands/root/root.go +++ b/internal/commands/root/root.go @@ -20,6 +20,7 @@ import ( "github.com/sigstore/gitsign/internal/commands/attest" "github.com/sigstore/gitsign/internal/commands/show" + "github.com/sigstore/gitsign/internal/commands/verify" "github.com/sigstore/gitsign/internal/commands/version" "github.com/sigstore/gitsign/internal/config" "github.com/sigstore/gitsign/internal/io" @@ -88,6 +89,7 @@ func New(cfg *config.Config) *cobra.Command { rootCmd.AddCommand(version.New(cfg)) rootCmd.AddCommand(show.New(cfg)) rootCmd.AddCommand(attest.New(cfg)) + rootCmd.AddCommand(verify.New(cfg)) o.AddFlags(rootCmd) return rootCmd diff --git a/internal/commands/root/verify.go b/internal/commands/root/verify.go index 84af2f38..1a1d018a 100644 --- a/internal/commands/root/verify.go +++ b/internal/commands/root/verify.go @@ -18,19 +18,15 @@ package root import ( "bytes" "context" - "crypto/x509" "errors" "fmt" "io" "os" - "github.com/sigstore/gitsign/internal" - "github.com/sigstore/gitsign/internal/fulcio/fulcioroots" + "github.com/sigstore/gitsign/internal/commands/verify" + "github.com/sigstore/gitsign/internal/gitsign" "github.com/sigstore/gitsign/internal/gpg" gsio "github.com/sigstore/gitsign/internal/io" - "github.com/sigstore/gitsign/internal/rekor" - "github.com/sigstore/gitsign/pkg/git" - "github.com/sigstore/sigstore/pkg/cryptoutils" ) // commandSign implements gitsign commit verification. @@ -70,44 +66,15 @@ func commandVerify(o *options, s *gsio.Streams, args ...string) error { return fmt.Errorf("failed to read signature data (detached: %T): %w", detached, err) } - root, intermediate, err := fulcioroots.NewFromConfig(ctx, o.Config) + v, err := gitsign.NewVerifierWithCosignOpts(ctx, o.Config, nil) if err != nil { - return fmt.Errorf("error getting certificate root: %w", err) + return err } - - tsa, err := x509.SystemCertPool() - if err != nil { - return fmt.Errorf("error getting system root pool: %w", err) - } - if path := o.Config.TimestampCert; path != "" { - f, err := os.Open(path) - if err != nil { - return err - } - cert, err := cryptoutils.LoadCertificatesFromPEM(f) - if err != nil { - return fmt.Errorf("error loading certs from %s: %w", path, err) - } - for _, c := range cert { - tsa.AddCert(c) - } - } - - cv, err := git.NewCertVerifier( - git.WithRootPool(root), - git.WithIntermediatePool(intermediate), - git.WithTimestampCertPool(tsa), - ) - if err != nil { - return fmt.Errorf("error creating git cert verifier: %w", err) - } - - rekor, err := rekor.NewClient(o.Config.Rekor) + summary, err := v.Verify(ctx, data, sig, true) if err != nil { - return fmt.Errorf("failed to create rekor client: %w", err) + return err } - summary, err := git.Verify(ctx, cv, rekor, data, sig, detached) if err != nil { if summary != nil && summary.Cert != nil { gpgout.EmitBadSig(summary.Cert) @@ -118,20 +85,10 @@ func commandVerify(o *options, s *gsio.Streams, args ...string) error { return fmt.Errorf("failed to verify signature: %w", err) } - fpr := internal.CertHexFingerprint(summary.Cert) + verify.PrintSummary(s.Err, summary) + fmt.Fprintln(s.Err, "WARNING: git verify-commit does not verify cert claims. Prefer using `gitsign verify` instead.") - fmt.Fprintln(s.Err, "tlog index:", *summary.LogEntry.LogIndex) - fmt.Fprintf(s.Err, "gitsign: Signature made using certificate ID 0x%s | %v\n", fpr, summary.Cert.Issuer) gpgout.EmitGoodSig(summary.Cert) - - // TODO: Maybe split up signature checking and certificate checking so we can - // output something more meaningful. - fmt.Fprintf(s.Err, "gitsign: Good signature from %v\n", summary.Cert.EmailAddresses) - - for _, c := range summary.Claims { - fmt.Fprintf(s.Err, "%s: %t\n", string(c.Key), c.Value) - } - gpgout.EmitTrustFully() return nil diff --git a/internal/commands/verify/verify.go b/internal/commands/verify/verify.go new file mode 100644 index 00000000..a485bb33 --- /dev/null +++ b/internal/commands/verify/verify.go @@ -0,0 +1,151 @@ +// Copyright 2023 The Sigstore Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package verify + +import ( + "context" + "encoding/pem" + "fmt" + "io" + "os" + + gogit "github.com/go-git/go-git/v5" + "github.com/go-git/go-git/v5/plumbing" + cosignopts "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/gitsign/internal" + "github.com/sigstore/gitsign/internal/config" + "github.com/sigstore/gitsign/internal/gitsign" + "github.com/sigstore/gitsign/pkg/git" + "github.com/sigstore/sigstore/pkg/cryptoutils" + "github.com/spf13/cobra" +) + +type options struct { + Config *config.Config + cosignopts.CertVerifyOptions +} + +func (o *options) AddFlags(cmd *cobra.Command) { + o.CertVerifyOptions.AddFlags(cmd) +} + +func (o *options) Run(w io.Writer, args []string) error { + ctx := context.Background() + repo, err := gogit.PlainOpenWithOptions(".", &gogit.PlainOpenOptions{ + DetectDotGit: true, + }) + if err != nil { + return err + } + + revision := "HEAD" + if len(args) > 0 { + revision = args[0] + } + + h, err := repo.ResolveRevision(plumbing.Revision(revision)) + if err != nil { + return fmt.Errorf("error resolving commit object: %w", err) + } + c, err := repo.CommitObject(*h) + if err != nil { + return fmt.Errorf("error reading commit object: %w", err) + } + + sig := []byte(c.PGPSignature) + p, _ := pem.Decode(sig) + if p == nil || p.Type != "SIGNED MESSAGE" { + return fmt.Errorf("unsupported signature type") + } + + c2 := new(plumbing.MemoryObject) + if err := c.EncodeWithoutSignature(c2); err != nil { + return err + } + r, err := c2.Reader() + if err != nil { + return err + } + defer r.Close() + data, err := io.ReadAll(r) + if err != nil { + return err + } + + v, err := gitsign.NewVerifierWithCosignOpts(ctx, o.Config, &o.CertVerifyOptions) + if err != nil { + return err + } + summary, err := v.Verify(ctx, data, sig, true) + if err != nil { + return err + } + + PrintSummary(os.Stdout, summary) + + return nil +} + +func PrintSummary(w io.Writer, summary *git.VerificationSummary) { + fpr := internal.CertHexFingerprint(summary.Cert) + + fmt.Fprintln(w, "tlog index:", *summary.LogEntry.LogIndex) + fmt.Fprintf(w, "gitsign: Signature made using certificate ID 0x%s | %v\n", fpr, summary.Cert.Issuer) + + ce := cosign.CertExtensions{Cert: summary.Cert} + fmt.Fprintf(w, "gitsign: Good signature from %v(%s)\n", cryptoutils.GetSubjectAlternateNames(summary.Cert), ce.GetIssuer()) + + for _, c := range summary.Claims { + fmt.Fprintf(w, "%s: %t\n", string(c.Key), c.Value) + } +} + +func New(cfg *config.Config) *cobra.Command { + o := &options{Config: cfg} + + cmd := &cobra.Command{ + Use: "verify [commit]", + SilenceUsage: true, + Short: "Show source predicate information", + Long: `Show source predicate information + +Prints an in-toto style predicate for the specified revision. +If no revision is specified, HEAD is used. + +This command is experimental, and its CLI surface may change.`, + RunE: func(cmd *cobra.Command, args []string) error { + // Simulate unknown flag errors. + if o.Cert != "" { + return fmt.Errorf("unknown flag: --certificate") + } + if o.CertChain != "" { + return fmt.Errorf("unknown flag: --certificate-chain") + } + + return o.Run(os.Stdout, args) + }, + } + o.AddFlags(cmd) + + // Hide flags we don't implement. + // --certificate: The cert should always come from the commit. + _ = cmd.Flags().MarkHidden("certificate") + // --certificate-chain: We only support reading from a TUF root at the moment. + // TODO: add support for this. + _ = cmd.Flags().MarkHidden("certificate-chain") + + return cmd +} diff --git a/internal/gitsign/gitsign.go b/internal/gitsign/gitsign.go new file mode 100644 index 00000000..88fe5467 --- /dev/null +++ b/internal/gitsign/gitsign.go @@ -0,0 +1,136 @@ +// Copyright 2023 The Sigstore Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gitsign + +import ( + "context" + "crypto/x509" + "fmt" + "os" + + cosignopts "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" + "github.com/sigstore/cosign/v2/pkg/cosign" + "github.com/sigstore/gitsign/internal/cert" + "github.com/sigstore/gitsign/internal/config" + "github.com/sigstore/gitsign/internal/fulcio/fulcioroots" + rekorinternal "github.com/sigstore/gitsign/internal/rekor" + "github.com/sigstore/gitsign/pkg/git" + "github.com/sigstore/gitsign/pkg/rekor" + "github.com/sigstore/sigstore/pkg/cryptoutils" +) + +type Verifier struct { + git git.Verifier + cert cert.Verifier + rekor rekor.Verifier +} + +// NewVerifierWithCosignOpts implements a Gitsign verifier using Cosign CertVerifyOptions. +// Note: not all options are supported. +// - cert: This is always taken from the commit. +func NewVerifierWithCosignOpts(ctx context.Context, cfg *config.Config, opts *cosignopts.CertVerifyOptions) (*Verifier, error) { + root, intermediate, err := fulcioroots.NewFromConfig(ctx, cfg) + if err != nil { + return nil, fmt.Errorf("error getting certificate root: %w", err) + } + + tsa, err := x509.SystemCertPool() + if err != nil { + return nil, fmt.Errorf("error getting system root pool: %w", err) + } + if path := cfg.TimestampCert; path != "" { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + cert, err := cryptoutils.LoadCertificatesFromPEM(f) + if err != nil { + return nil, fmt.Errorf("error loading certs from %s: %w", path, err) + } + for _, c := range cert { + tsa.AddCert(c) + } + } + + gitverifier, err := git.NewCertVerifier( + git.WithRootPool(root), + git.WithIntermediatePool(intermediate), + git.WithTimestampCertPool(tsa), + ) + if err != nil { + return nil, fmt.Errorf("error creating Git verifier: %w", err) + } + + rekor, err := rekorinternal.NewClient(cfg.Rekor) + if err != nil { + return nil, fmt.Errorf("failed to create rekor client: %w", err) + } + + // Optionally include cert.Verifier. + // This needs to be optional because when verifying with + // `git verify-commit` we don't have access to issuer / identity details. + // In these cases, clients should look for the certificate validated claim + // and warn if missing. + var certverifier cert.Verifier + if opts != nil { + ctpub, err := cosign.GetCTLogPubs(ctx) + if err != nil { + return nil, fmt.Errorf("error getting CT log public key: %w", err) + } + identities, err := opts.Identities() + if err != nil { + return nil, fmt.Errorf("error parsing identities: %w", err) + } + certverifier = cert.NewCosignVerifier(&cosign.CheckOpts{ + RekorClient: rekor.Rekor, + RootCerts: root, + IntermediateCerts: intermediate, + CTLogPubKeys: ctpub, + RekorPubKeys: rekor.PublicKeys(), + CertGithubWorkflowTrigger: opts.CertGithubWorkflowTrigger, + CertGithubWorkflowSha: opts.CertGithubWorkflowSha, + CertGithubWorkflowName: opts.CertGithubWorkflowName, + CertGithubWorkflowRepository: opts.CertGithubWorkflowRepository, + CertGithubWorkflowRef: opts.CertGithubWorkflowRef, + Identities: identities, + IgnoreSCT: opts.IgnoreSCT, + }) + } + + return &Verifier{ + git: gitverifier, + cert: certverifier, + rekor: rekor, + }, nil +} + +func (v *Verifier) Verify(ctx context.Context, data []byte, sig []byte, detached bool) (*git.VerificationSummary, error) { + // TODO: we probably want to deprecate git.Verify in favor of this struct. + summary, err := git.Verify(ctx, v.git, v.rekor, data, sig, detached) + if err != nil { + return summary, err + } + + if v.cert != nil { + if err := v.cert.Verify(summary.Cert); err != nil { + summary.Claims = append(summary.Claims, git.NewClaim(git.ClaimValidatedCerificate, false)) + return summary, err + } + summary.Claims = append(summary.Claims, git.NewClaim(git.ClaimValidatedCerificate, true)) + } + + return summary, nil +} diff --git a/internal/gitsign/gitsign_test.go b/internal/gitsign/gitsign_test.go new file mode 100644 index 00000000..6d1c02e2 --- /dev/null +++ b/internal/gitsign/gitsign_test.go @@ -0,0 +1,172 @@ +// Copyright 2023 The Sigstore Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package gitsign + +import ( + "context" + "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/asn1" + "io" + "math/big" + "testing" + "time" + + "github.com/go-git/go-git/v5/plumbing/object" + "github.com/go-git/go-git/v5/storage/memory" + "github.com/sigstore/cosign/v2/pkg/cosign" + certverifier "github.com/sigstore/gitsign/internal/cert" + "github.com/sigstore/gitsign/internal/signature" + "github.com/sigstore/gitsign/pkg/git" + "github.com/sigstore/rekor/pkg/generated/models" +) + +func TestVerify(t *testing.T) { + ctx := context.Background() + + // Generate cert + cert, priv := generateCert(t, &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{ + CommonName: "tacocat", + }, + EmailAddresses: []string{"tacocat@example.com"}, + ExtraExtensions: []pkix.Extension{{ + Id: asn1.ObjectIdentifier{1, 3, 6, 1, 4, 1, 57264, 1, 1}, + Value: []byte("example.com"), + }}, + NotBefore: time.Now(), + NotAfter: time.Now().Add(5 * time.Minute), + }) + + // Git verifier + roots := x509.NewCertPool() + roots.AddCert(cert) + gv, err := git.NewCertVerifier(git.WithRootPool(roots)) + if err != nil { + t.Fatalf("error creating git verifer: %v", err) + } + + // Cert verifier + cv := certverifier.NewCosignVerifier(&cosign.CheckOpts{ + RootCerts: roots, + Identities: []cosign.Identity{{ + Issuer: "example.com", + Subject: "tacocat@example.com", + }}, + IgnoreSCT: true, + IgnoreTlog: true, + }) + + // Rekor verifier - we don't have a good way to test this right now so mock it out. + rekor := fakeRekor{} + + v := Verifier{ + git: gv, + cert: cv, + rekor: rekor, + } + + data, sig := generateData(t, cert, priv) + if _, err := v.Verify(ctx, data, sig, true); err != nil { + t.Fatal(err) + } +} + +func generateCert(t *testing.T, tmpl *x509.Certificate) (*x509.Certificate, *ecdsa.PrivateKey) { + t.Helper() + + priv, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + if err != nil { + t.Fatalf("error generating private key: %v", err) + } + pub := &priv.PublicKey + raw, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, pub, priv) + if err != nil { + t.Fatalf("error generating certificate: %v", err) + } + cert, err := x509.ParseCertificate(raw) + if err != nil { + t.Fatalf("ParseCertificate: %v", err) + } + return cert, priv +} + +func generateData(t *testing.T, cert *x509.Certificate, priv crypto.Signer) ([]byte, []byte) { + t.Helper() + + // Generate commit data + commit := object.Commit{ + Message: "hello world!", + } + obj := memory.NewStorage().NewEncodedObject() + if err := commit.Encode(obj); err != nil { + t.Fatal(err) + } + reader, err := obj.Reader() + if err != nil { + t.Fatal(err) + } + data, err := io.ReadAll(reader) + if err != nil { + t.Fatalf("error reading git data: %v", err) + } + + id := &identity{ + cert: cert, + priv: priv, + } + sig, _, err := signature.Sign(id, data, signature.SignOptions{ + Detached: true, + Armor: true, + // Fake CA outputs self-signed certs, so we need to use -1 to make sure + // the self-signed cert itself is included in the chain, otherwise + // Verify cannot find a cert to use for verification. + IncludeCerts: 0, + }) + if err != nil { + t.Fatalf("Sign() = %v", err) + } + + return data, sig +} + +type fakeRekor struct{} + +func (fakeRekor) Verify(ctx context.Context, commitSHA string, cert *x509.Certificate) (*models.LogEntryAnon, error) { + return nil, nil +} + +type identity struct { + signature.Identity + cert *x509.Certificate + priv crypto.Signer +} + +func (i *identity) Certificate() (*x509.Certificate, error) { + return i.cert, nil +} + +func (i *identity) CertificateChain() ([]*x509.Certificate, error) { + return []*x509.Certificate{i.cert}, nil +} + +func (i *identity) Signer() (crypto.Signer, error) { + return i.priv, nil +} diff --git a/pkg/git/verify.go b/pkg/git/verify.go index a1781fc5..1e7d81aa 100644 --- a/pkg/git/verify.go +++ b/pkg/git/verify.go @@ -52,6 +52,7 @@ type ClaimCondition string const ( ClaimValidatedSignature ClaimCondition = "Validated Git signature" ClaimValidatedRekorEntry ClaimCondition = "Validated Rekor entry" + ClaimValidatedCerificate ClaimCondition = "Validated Certificate claims" ) func NewClaim(c ClaimCondition, ok bool) Claim { diff --git a/pkg/rekor/rekor.go b/pkg/rekor/rekor.go index f5e079c4..b8c0d59b 100644 --- a/pkg/rekor/rekor.go +++ b/pkg/rekor/rekor.go @@ -56,6 +56,7 @@ type Writer interface { // Client implements a basic rekor implementation for writing and verifying Rekor data. type Client struct { *client.Rekor + publicKeys *cosign.TrustedTransparencyLogPubKeys } func New(url string, opts ...rekor.Option) (*Client, error) { @@ -63,8 +64,13 @@ func New(url string, opts ...rekor.Option) (*Client, error) { if err != nil { return nil, err } + pubs, err := rekorPubsFromClient(c) + if err != nil { + return nil, err + } return &Client{ - Rekor: c, + Rekor: c, + publicKeys: pubs, }, nil } @@ -162,11 +168,7 @@ func (c *Client) Verify(ctx context.Context, commitSHA string, cert *x509.Certif if err != nil { return nil, err } - rekorPubsFromAPI, err := rekorPubsFromClient(c.Rekor) - if err != nil { - return nil, err - } - return e, cosign.VerifyTLogEntryOffline(ctx, e, rekorPubsFromAPI) + return e, cosign.VerifyTLogEntryOffline(ctx, e, c.publicKeys) } // extractCerts is taken from cosign's cmd/cosign/cli/verify/verify_blob.go. @@ -219,3 +221,7 @@ func extractCerts(e *models.LogEntryAnon) ([]*x509.Certificate, error) { return certs, err } + +func (c *Client) PublicKeys() *cosign.TrustedTransparencyLogPubKeys { + return c.publicKeys +}