generated from sigstore/sigstore-project-template
-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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
Showing
9 changed files
with
493 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
] | ||
} | ||
} |
Oops, something went wrong.