Skip to content

Commit

Permalink
Add gitsign verify.
Browse files Browse the repository at this point in the history
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 <billy@chainguard.dev>
  • Loading branch information
wlynch committed Mar 22, 2023
1 parent 51ae2cc commit 92d8ebc
Show file tree
Hide file tree
Showing 12 changed files with 564 additions and 59 deletions.
1 change: 1 addition & 0 deletions .github/workflows/e2e.yaml
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions .gitignore
Expand Up @@ -2,7 +2,7 @@
*.swp
dist/*
.vscode/*
gitsign
gitsign-credential-cache
/gitsign
/gitsign-credential-cache
!/cmd/gitsign-credential-cache
vendor
1 change: 1 addition & 0 deletions docs/cli/gitsign.md
Expand Up @@ -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

38 changes: 38 additions & 0 deletions 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!

40 changes: 40 additions & 0 deletions 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
}
2 changes: 2 additions & 0 deletions internal/commands/root/root.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand Down
59 changes: 8 additions & 51 deletions internal/commands/root/verify.go
Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
151 changes: 151 additions & 0 deletions 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
}

0 comments on commit 92d8ebc

Please sign in to comment.