Skip to content

Commit

Permalink
Add gitsign show subcommand. (#191)
Browse files Browse the repository at this point in the history
* Add `gitsign show` subcommand.

Adds a new subcommand that outputs an in-toto Statement containing the
source provenance of the given commit.

Also moves io Stream creation into the root RunE func so that streams
aren't created for subcommands that don't need them.

Signed-off-by: Billy Lynch <billy@chainguard.dev>

* Remove extra remote defaulting.

Signed-off-by: Billy Lynch <billy@chainguard.dev>

* Remove cobra.NoArgs.

This was accidentally left in place while trying to debug a different
issuer.

Signed-off-by: Billy Lynch <billy@chainguard.dev>

Signed-off-by: Billy Lynch <billy@chainguard.dev>
  • Loading branch information
wlynch committed Nov 17, 2022
1 parent 828d9ae commit a8a5f24
Show file tree
Hide file tree
Showing 9 changed files with 493 additions and 1 deletion.
2 changes: 1 addition & 1 deletion internal/commands/root/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,13 @@ func (o *options) AddFlags(cmd *cobra.Command) {

func New(cfg *config.Config) *cobra.Command {
o := &options{Config: cfg}
s := io.New(o.Config.LogPath)

rootCmd := &cobra.Command{
Use: "gitsign",
Short: "Keyless Git signing with Sigstore!",
Args: cobra.ArbitraryArgs,
RunE: func(cmd *cobra.Command, args []string) error {
s := io.New(o.Config.LogPath)
return s.Wrap(func() error {
switch {
case o.FlagVersion:
Expand Down
213 changes: 213 additions & 0 deletions internal/commands/show/show.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
// Copyright 2022 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 show

import (
"encoding/base64"
"encoding/json"
"encoding/pem"
"errors"
"io"
"os"

"github.com/github/smimesign/ietf-cms/protocol"
"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/plumbing"
"github.com/in-toto/in-toto-golang/in_toto"
v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
"github.com/sigstore/gitsign/internal/config"
"github.com/sigstore/gitsign/pkg/predicate"
"github.com/sigstore/sigstore/pkg/cryptoutils"
"github.com/spf13/cobra"
)

const (
predicateType = "gitsign.sigstore.dev/predicate/git/v0.1"
)

type options struct {
FlagRemote string
}

func (o *options) AddFlags(cmd *cobra.Command) {
cmd.Flags().StringVarP(&o.FlagRemote, "remote", "r", "origin", "git remote (used to populate subject)")
}

func (o *options) Run(w io.Writer, args []string) error {
repo, err := git.PlainOpenWithOptions(".", &git.PlainOpenOptions{
DetectDotGit: true,
})
if err != nil {
return err
}
revision := "HEAD"
if len(args) > 0 {
revision = args[0]
}

out, err := statement(repo, o.FlagRemote, revision)
if err != nil {
return err
}

enc := json.NewEncoder(w)
enc.SetIndent("", " ")
if err := enc.Encode(out); err != nil {
return err
}

return nil
}

func statement(repo *git.Repository, remote, revision string) (*in_toto.Statement, error) {
hash, err := repo.ResolveRevision(plumbing.Revision(revision))
if err != nil {
return nil, err
}
commit, err := repo.CommitObject(*hash)
if err != nil {
return nil, err
}

// Extract parent hashes
parents := make([]string, 0, len(commit.ParentHashes))
for _, p := range commit.ParentHashes {
if !p.IsZero() {
parents = append(parents, p.String())
}
}

// Build initial predicate from the commit.
predicate := &predicate.GitCommit{
Commit: &predicate.Commit{
Tree: commit.TreeHash.String(),
Parents: parents,
Author: &predicate.Author{
Name: commit.Author.Name,
Email: commit.Author.Email,
Date: commit.Author.When,
},
Committer: &predicate.Author{
Name: commit.Committer.Name,
Email: commit.Committer.Email,
Date: commit.Committer.When,
},
Message: commit.Message,
},
Signature: commit.PGPSignature,
}

// We have a PEM encoded signature, try and extract certificate details.
pem, _ := pem.Decode([]byte(commit.PGPSignature))
if pem != nil {
sigs, err := parseSignature(pem.Bytes)
if err != nil {
return nil, err
}
predicate.SignerInfo = sigs
}

// Try and resolve the remote name to use as the subject name.
// If the repo does not have a remote configured then this will be left
// blank.
resolvedRemote, err := repo.Remote(remote)
if err != nil && !errors.Is(err, git.ErrRemoteNotFound) {
return nil, err
}
remoteName := ""
if resolvedRemote != nil && resolvedRemote.Config() != nil && len(resolvedRemote.Config().URLs) > 0 {
remoteName = resolvedRemote.Config().URLs[0]
}

// Wrap predicate in in-toto Statement.
return &in_toto.Statement{
StatementHeader: in_toto.StatementHeader{
Type: in_toto.StatementInTotoV01,
Subject: []in_toto.Subject{{
Name: remoteName,
Digest: v02.DigestSet{
// TODO?: Figure out if/how to support git sha256 - this
// will likely depend on upstream support in go-git.
// See https://github.com/go-git/go-git/issues/229.
"sha1": hash.String(),
},
}},
PredicateType: predicateType,
},
Predicate: predicate,
}, nil
}

func parseSignature(raw []byte) ([]*predicate.SignerInfo, error) {
ci, err := protocol.ParseContentInfo(raw)
if err != nil {
return nil, err
}

sd, err := ci.SignedDataContent()
if err != nil {
return nil, err
}

certs, err := sd.X509Certificates()
if err != nil {
return nil, err
}

// A signature may have multiple signers associated to it -
// extract each SignerInfo separately.
out := make([]*predicate.SignerInfo, 0, len(sd.SignerInfos))
for _, si := range sd.SignerInfos {
cert, err := si.FindCertificate(certs)
if err != nil {
continue
}
b, err := cryptoutils.MarshalCertificateToPEM(cert)
if err != nil {
return nil, err
}
sa, err := si.SignedAttrs.MarshaledForVerification()
if err != nil {
return nil, err
}
out = append(out, &predicate.SignerInfo{
Certificate: string(b),
Attributes: base64.StdEncoding.EncodeToString(sa),
})
}

return out, nil
}

func New(cfg *config.Config) *cobra.Command {
o := &options{}

cmd := &cobra.Command{
Use: "show [revision]",
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 {
return o.Run(os.Stdout, args)
},
}
o.AddFlags(cmd)

return cmd
}
97 changes: 97 additions & 0 deletions internal/commands/show/show_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Copyright 2022 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 show

import (
"encoding/json"
"fmt"
"os"
"testing"

"github.com/go-git/go-git/v5"
"github.com/go-git/go-git/v5/config"
"github.com/go-git/go-git/v5/plumbing"
"github.com/go-git/go-git/v5/storage/memory"
"github.com/google/go-cmp/cmp"
"github.com/in-toto/in-toto-golang/in_toto"
"github.com/sigstore/gitsign/pkg/predicate"
)

func TestShow(t *testing.T) {
storage := memory.NewStorage()
repo := &git.Repository{
Storer: storage,
}
if err := repo.SetConfig(&config.Config{
Remotes: map[string]*config.RemoteConfig{
"origin": {
Name: "origin",
URLs: []string{"git@github.com:wlynch/gitsign.git"},
},
},
}); err != nil {
t.Fatalf("error setting git config: %v", err)
}

// Expect files in testdata directory:
// foo.in.txt -> foo.out.json
// IMPORTANT: When generating new test files, use a command like `git cat-file commit main > foo.in.txt`.
// If you try and copy/paste the content, you may get burned by file encodings and missing \r characters.
for _, tc := range []string{
"fulcio-cert",
"gpg",
} {
t.Run(tc, func(t *testing.T) {
raw, err := os.ReadFile(fmt.Sprintf("testdata/%s.in.txt", tc))
if err != nil {
t.Fatalf("error reading input: %v", err)
}
obj := storage.NewEncodedObject()
obj.SetType(plumbing.CommitObject)
w, err := obj.Writer()
if err != nil {
t.Fatalf("error getting git object writer: %v", err)
}
_, err = w.Write(raw)
if err != nil {
t.Fatalf("error writing git commit: %v", err)
}
h, err := storage.SetEncodedObject(obj)
if err != nil {
t.Fatalf("error storing git commit: %v", err)
}

got, err := statement(repo, "origin", h.String())
if err != nil {
t.Fatalf("statement(): %v", err)
}

wantRaw, err := os.ReadFile(fmt.Sprintf("testdata/%s.out.json", tc))
if err != nil {
t.Fatalf("error reading want json: %v", err)
}
want := &in_toto.Statement{
Predicate: &predicate.GitCommit{},
}
if err := json.Unmarshal(wantRaw, want); err != nil {
t.Fatalf("error decoding want json: %v", err)
}

if diff := cmp.Diff(want, got); diff != "" {
t.Error(diff)
}
})
}
}
30 changes: 30 additions & 0 deletions internal/commands/show/testdata/fulcio-cert.in.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
tree 194fca354a2439028e347ce5e19e4db45bd708a6
parent 2eaf8fc6d66505baa90640d018e1131cd8e99334
author Billy Lynch <billy@chainguard.dev> 1668460399 -0500
committer Billy Lynch <billy@chainguard.dev> 1668460399 -0500
gpgsig -----BEGIN SIGNED MESSAGE-----
MIIEAwYJKoZIhvcNAQcCoIID9DCCA/ACAQExDTALBglghkgBZQMEAgEwCwYJKoZI
hvcNAQcBoIICpDCCAqAwggImoAMCAQICFFTzLmXKAlKX5xTUaYoUE5giCxZvMAoG
CCqGSM49BAMDMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2ln
c3RvcmUtaW50ZXJtZWRpYXRlMB4XDTIyMTExNDIxMTMyM1oXDTIyMTExNDIxMjMy
M1owADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABH++UCDlF9MaQCSgDKQ0bWhD
eOmTrk1sEHw9Oel1eCyrr3SFhDAghcO3VwO7baYmL16fUwRYwMhj5urowsLVrjKj
ggFFMIIBQTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHQYD
VR0OBBYEFHMPDOs6IDY/iRnVqacIj/yvJbNpMB8GA1UdIwQYMBaAFN/T6c9WJBGW
+ajY6ShVosYuGGQ/MCIGA1UdEQEB/wQYMBaBFGJpbGx5QGNoYWluZ3VhcmQuZGV2
MCkGCisGAQQBg78wAQEEG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTCBigYK
KwYBBAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAv
Ke6OAAABhHf9WZAAAAQDAEcwRQIgV8anMDEjbHI/WvGxpJmm44DgBTYf5bkfBJIP
6FJtqXYCIQD/noLzthDKgjrXoiep/BqqnygoTRM9HKim+DRMbwHteDAKBggqhkjO
PQQDAwNoADBlAjEAvHvqOAKT34QQx9PSuOswQfquByALdzA1ES0nx4M5i47kqNeE
Bl612/hYTD1ydpLIAjBTWiHDtdxM9rriTIyGGJubC0+vNcccsURDTJ+A3XnMAER3
ikl/cJ2wG9c8ZN7AUS8xggElMIIBIQIBATBPMDcxFTATBgNVBAoTDHNpZ3N0b3Jl
LmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlAhRU8y5lygJSl+cU
1GmKFBOYIgsWbzALBglghkgBZQMEAgGgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcN
AQcBMBwGCSqGSIb3DQEJBTEPFw0yMjExMTQyMTEzMjNaMC8GCSqGSIb3DQEJBDEi
BCC9Yk93XCRKy6FPCb8dAqjdWpjb1NIbFtTo9CP6yYOZQjAKBggqhkjOPQQDAgRH
MEUCIQCq+2Zs0bBcAAciePeeRpzmfVJ2gEu7sGngy+TcYpS0ugIgL9Qix3V8taBV
+Tb6rMZmt80sfGsYhUqE8KsIF1AEc+8=
-----END SIGNED MESSAGE-----

add sample
38 changes: 38 additions & 0 deletions internal/commands/show/testdata/fulcio-cert.out.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"_type": "https://in-toto.io/Statement/v0.1",
"predicateType": "gitsign.sigstore.dev/predicate/git/v0.1",
"subject": [
{
"name": "git@github.com:wlynch/gitsign.git",
"digest": {
"sha1": "10a3086104c5331623be85a5e30d709f457b536b"
}
}
],
"predicate": {
"source": {
"tree": "194fca354a2439028e347ce5e19e4db45bd708a6",
"parents": [
"2eaf8fc6d66505baa90640d018e1131cd8e99334"
],
"author": {
"name": "Billy Lynch",
"email": "billy@chainguard.dev",
"date": "2022-11-14T16:13:19-05:00"
},
"committer": {
"name": "Billy Lynch",
"email": "billy@chainguard.dev",
"date": "2022-11-14T16:13:19-05:00"
},
"message": "add sample\n"
},
"signature": "-----BEGIN SIGNED MESSAGE-----\nMIIEAwYJKoZIhvcNAQcCoIID9DCCA/ACAQExDTALBglghkgBZQMEAgEwCwYJKoZI\nhvcNAQcBoIICpDCCAqAwggImoAMCAQICFFTzLmXKAlKX5xTUaYoUE5giCxZvMAoG\nCCqGSM49BAMDMDcxFTATBgNVBAoTDHNpZ3N0b3JlLmRldjEeMBwGA1UEAxMVc2ln\nc3RvcmUtaW50ZXJtZWRpYXRlMB4XDTIyMTExNDIxMTMyM1oXDTIyMTExNDIxMjMy\nM1owADBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABH++UCDlF9MaQCSgDKQ0bWhD\neOmTrk1sEHw9Oel1eCyrr3SFhDAghcO3VwO7baYmL16fUwRYwMhj5urowsLVrjKj\nggFFMIIBQTAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0lBAwwCgYIKwYBBQUHAwMwHQYD\nVR0OBBYEFHMPDOs6IDY/iRnVqacIj/yvJbNpMB8GA1UdIwQYMBaAFN/T6c9WJBGW\n+ajY6ShVosYuGGQ/MCIGA1UdEQEB/wQYMBaBFGJpbGx5QGNoYWluZ3VhcmQuZGV2\nMCkGCisGAQQBg78wAQEEG2h0dHBzOi8vYWNjb3VudHMuZ29vZ2xlLmNvbTCBigYK\nKwYBBAHWeQIEAgR8BHoAeAB2AN09MGrGxxEyYxkeHJlnNwKiSl643jyt/4eKcoAv\nKe6OAAABhHf9WZAAAAQDAEcwRQIgV8anMDEjbHI/WvGxpJmm44DgBTYf5bkfBJIP\n6FJtqXYCIQD/noLzthDKgjrXoiep/BqqnygoTRM9HKim+DRMbwHteDAKBggqhkjO\nPQQDAwNoADBlAjEAvHvqOAKT34QQx9PSuOswQfquByALdzA1ES0nx4M5i47kqNeE\nBl612/hYTD1ydpLIAjBTWiHDtdxM9rriTIyGGJubC0+vNcccsURDTJ+A3XnMAER3\nikl/cJ2wG9c8ZN7AUS8xggElMIIBIQIBATBPMDcxFTATBgNVBAoTDHNpZ3N0b3Jl\nLmRldjEeMBwGA1UEAxMVc2lnc3RvcmUtaW50ZXJtZWRpYXRlAhRU8y5lygJSl+cU\n1GmKFBOYIgsWbzALBglghkgBZQMEAgGgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcN\nAQcBMBwGCSqGSIb3DQEJBTEPFw0yMjExMTQyMTEzMjNaMC8GCSqGSIb3DQEJBDEi\nBCC9Yk93XCRKy6FPCb8dAqjdWpjb1NIbFtTo9CP6yYOZQjAKBggqhkjOPQQDAgRH\nMEUCIQCq+2Zs0bBcAAciePeeRpzmfVJ2gEu7sGngy+TcYpS0ugIgL9Qix3V8taBV\n+Tb6rMZmt80sfGsYhUqE8KsIF1AEc+8=\n-----END SIGNED MESSAGE-----\n",
"signer_info": [
{
"attributes": "MWkwGAYJKoZIhvcNAQkDMQsGCSqGSIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjIxMTE0MjExMzIzWjAvBgkqhkiG9w0BCQQxIgQgvWJPd1wkSsuhTwm/HQKo3VqY29TSGxbU6PQj+smDmUI=",
"certificate": "-----BEGIN CERTIFICATE-----\nMIICoDCCAiagAwIBAgIUVPMuZcoCUpfnFNRpihQTmCILFm8wCgYIKoZIzj0EAwMw\nNzEVMBMGA1UEChMMc2lnc3RvcmUuZGV2MR4wHAYDVQQDExVzaWdzdG9yZS1pbnRl\ncm1lZGlhdGUwHhcNMjIxMTE0MjExMzIzWhcNMjIxMTE0MjEyMzIzWjAAMFkwEwYH\nKoZIzj0CAQYIKoZIzj0DAQcDQgAEf75QIOUX0xpAJKAMpDRtaEN46ZOuTWwQfD05\n6XV4LKuvdIWEMCCFw7dXA7ttpiYvXp9TBFjAyGPm6ujCwtWuMqOCAUUwggFBMA4G\nA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzAdBgNVHQ4EFgQUcw8M\n6zogNj+JGdWppwiP/K8ls2kwHwYDVR0jBBgwFoAU39Ppz1YkEZb5qNjpKFWixi4Y\nZD8wIgYDVR0RAQH/BBgwFoEUYmlsbHlAY2hhaW5ndWFyZC5kZXYwKQYKKwYBBAGD\nvzABAQQbaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tMIGKBgorBgEEAdZ5AgQC\nBHwEegB4AHYA3T0wasbHETJjGR4cmWc3AqJKXrjePK3/h4pygC8p7o4AAAGEd/1Z\nkAAABAMARzBFAiBXxqcwMSNscj9a8bGkmabjgOAFNh/luR8Ekg/oUm2pdgIhAP+e\ngvO2EMqCOteiJ6n8GqqfKChNEz0cqKb4NExvAe14MAoGCCqGSM49BAMDA2gAMGUC\nMQC8e+o4ApPfhBDH09K46zBB+q4HIAt3MDURLSfHgzmLjuSo14QGXrXb+FhMPXJ2\nksgCMFNaIcO13Ez2uuJMjIYYm5sLT681xxyxRENMn4DdecwARHeKSX9wnbAb1zxk\n3sBRLw==\n-----END CERTIFICATE-----\n"
}
]
}
}
Loading

0 comments on commit a8a5f24

Please sign in to comment.