Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support oci image verification #147

Merged
merged 13 commits into from
Aug 17, 2022
123 changes: 98 additions & 25 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package main
import (
"context"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/hex"
"encoding/json"
Expand All @@ -12,13 +13,17 @@ import (
"log"
"os"

"github.com/google/go-containerregistry/pkg/name"
asraa marked this conversation as resolved.
Show resolved Hide resolved
"github.com/secure-systems-lab/go-securesystemslib/dsse"
"github.com/sigstore/cosign/cmd/cosign/cli/rekor"
"github.com/sigstore/cosign/pkg/cosign"
"github.com/slsa-framework/slsa-verifier/pkg"
)

var (
provenancePath string
artifactPath string
ociImageRef string
source string
branch string
tag string
Expand All @@ -28,8 +33,8 @@ var (

var defaultRekorAddr = "https://rekor.sigstore.dev"

func verify(ctx context.Context,
provenance []byte, artifactHash, source string, provenanceOpts *pkg.ProvenanceOpts) ([]byte, error) {
func verifyBlob(ctx context.Context,
provenance []byte, artifactHash, source string) (*envAndCert, error) {
rClient, err := rekor.NewClient(defaultRekorAddr)
if err != nil {
return nil, err
Expand All @@ -41,6 +46,10 @@ func verify(ctx context.Context,
return nil, err
}

return &envAndCert{env, cert}, nil
}

func verifyEnvAndCert(env *dsse.Envelope, cert *x509.Certificate, source string, pOpts *pkg.ProvenanceOpts) ([]byte, error) {
/* Verify properties of the signing identity. */
// Get the workflow info given the certificate information.
workflowInfo, err := pkg.GetWorkflowInfoFromCertificate(cert)
Expand All @@ -61,7 +70,7 @@ func verify(ctx context.Context,

/* Verify properties of the SLSA provenance. */
// Unpack and verify info in the provenance, including the Subject Digest.
if err := pkg.VerifyProvenance(env, provenanceOpts); err != nil {
if err := pkg.VerifyProvenance(env, pOpts); err != nil {
return nil, err
}

Expand All @@ -72,6 +81,7 @@ func verify(ctx context.Context,
func main() {
flag.StringVar(&provenancePath, "provenance", "", "path to a provenance file")
flag.StringVar(&artifactPath, "artifact-path", "", "path to an artifact to verify")
flag.StringVar(&ociImageRef, "oci-image", "", "reference to an OCI image to verify")
flag.StringVar(&source, "source", "",
"expected source repository that should have produced the binary, e.g. github.com/some/repo")
flag.StringVar(&branch, "branch", "main", "expected branch the binary was compiled from")
Expand All @@ -82,7 +92,13 @@ func main() {
"print the verified provenance to std out")
flag.Parse()

if provenancePath == "" || artifactPath == "" || source == "" {
if (provenancePath == "" || artifactPath == "") && ociImageRef == "" {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like we should have different commands for containers vs. artifacts

The combinations of how flags are used isn't really very user friendly and I feel like it will just get worse over time.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

...not that I expect that to be implemented in this PR. Just a general comment.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For v2, I'll try to write a one-pager on the SLSA verifier API. There are a few issues on this (#180) and I am worried without writing it up first I'll get it wrong.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to be clear, I was simply thinking something like slsa-verifier verify-artifact ... and slsa-verifier verify-image ... something like that. Right now it's down to the combination of what flags users use. Commands allow us to have different flags for different CLI commands.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, yes I agree too we should have sub-commands separating the two.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SGTM

fmt.Fprintf(os.Stderr, "either 'provenance' and 'artifact-path' or 'oci-image' must be specified\n")
flag.Usage()
os.Exit(1)
}

if source == "" {
flag.Usage()
os.Exit(1)
}
Expand All @@ -101,16 +117,17 @@ func main() {
os.Exit(1)
}

verifiedProvenance, err := runVerify(artifactPath, provenancePath, source, branch, ptag, pversiontag)
verifiedProvenance, err := runVerify(ociImageRef, artifactPath, provenancePath, source, branch, ptag, pversiontag)
if err != nil {
fmt.Fprintf(os.Stderr, "FAILED: SLSA verification failed: %v\n", err)
os.Exit(2)
}

fmt.Fprintf(os.Stderr, "PASSED: Verified SLSA provenance\n")

if printProvenance {
fmt.Fprintf(os.Stdout, "%s\n", string(verifiedProvenance))
for _, verified := range verifiedProvenance {
fmt.Fprintf(os.Stdout, "%s\n", string(verified))
}
}
}

Expand All @@ -124,23 +141,67 @@ func isFlagPassed(name string) bool {
return found
}

func runVerify(artifactPath, provenancePath, source, branch string, ptag, pversiontag *string) ([]byte, error) {
f, err := os.Open(artifactPath)
if err != nil {
log.Fatal(err)
}
defer f.Close()
type envAndCert struct {
env *dsse.Envelope
cert *x509.Certificate
}

provenance, err := os.ReadFile(provenancePath)
if err != nil {
return nil, err
}
func runVerify(ociImageRef, artifactPath, provenancePath, source, branch string, ptag, pversiontag *string) ([][]byte, error) {
// A list of verified envelope and certificates.
var envAndCerts []*envAndCert
var artifactHash string

h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
log.Panic(err)
ctx := context.Background()
if ociImageRef != "" {
ref, err := name.ParseReference(ociImageRef)
if err != nil {
return nil, err
}
// Run container verification.
atts, _, err := cosign.VerifyImageAttestations(ctx, ref, &cosign.CheckOpts{})
if err != nil {
return nil, err
}
for _, att := range atts {
pyld, err := att.Payload()
if err != nil {
return nil, err
}
env, err := pkg.EnvelopeFromBytes(pyld)
if err != nil {
return nil, err
}
cert, err := att.Cert()
if err != nil {
return nil, err
}
envAndCerts = append(envAndCerts, &envAndCert{env, cert})
}
} else {
// Run blob verification.
f, err := os.Open(artifactPath)
if err != nil {
log.Fatal(err)
}
defer f.Close()

provenance, err := os.ReadFile(provenancePath)
if err != nil {
return nil, err
}

h := sha256.New()
if _, err := io.Copy(h, f); err != nil {
log.Panic(err)
}
artifactHash = hex.EncodeToString(h.Sum(nil))

e, err := verifyBlob(ctx, provenance, artifactHash, source)
if err != nil {
return nil, err
}
envAndCerts = append(envAndCerts, e)
}
artifactHash := hex.EncodeToString(h.Sum(nil))

provenanceOpts := &pkg.ProvenanceOpts{
ExpectedBranch: branch,
Expand All @@ -149,8 +210,20 @@ func runVerify(artifactPath, provenancePath, source, branch string, ptag, pversi
ExpectedTag: ptag,
}

ctx := context.Background()
return verify(ctx, provenance,
artifactHash,
source, provenanceOpts)
verifiedAttestations := make([][]byte, 0)
var verifyErr error
for _, envAndCert := range envAndCerts {
verified, err := verifyEnvAndCert(envAndCert.env, envAndCert.cert, source, provenanceOpts)
if err != nil {
verifyErr = err
continue
}
verifiedAttestations = append(verifiedAttestations, verified)
}

if len(verifiedAttestations) == 0 {
return nil, verifyErr
}

return verifiedAttestations, nil
}
13 changes: 6 additions & 7 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,10 @@ func pString(s string) *string {
}

// Versions of the builders to test.
// TODO: Test v0.0.2.
var generatorVersions = []string{"v0.0.2", "v1.1.1"}
var golangGeneratorVersions = []string{"v0.0.2", "v1.1.1"}

func Test_runVerify(t *testing.T) {
t.Parallel()
func Test_runVerifyGo(t *testing.T) {
// t.Parallel()
tests := []struct {
name string
artifact string
Expand Down Expand Up @@ -340,7 +339,7 @@ func Test_runVerify(t *testing.T) {
t.Run(tt.name, func(t *testing.T) {
// t.Parallel()

checkVersions := generatorVersions
checkVersions := golangGeneratorVersions
if tt.noversion {
checkVersions = []string{""}
}
Expand All @@ -351,10 +350,10 @@ func Test_runVerify(t *testing.T) {
branch = "main"
}

artifactPath = filepath.Clean(fmt.Sprintf("./testdata/%v/%s", v, tt.artifact))
artifactPath = filepath.Clean(fmt.Sprintf("./testdata/go/%v/%s", v, tt.artifact))
provenancePath = fmt.Sprintf("%s.intoto.jsonl", artifactPath)

_, err := runVerify(artifactPath,
_, err := runVerify("", artifactPath,
provenancePath,
tt.source, branch,
tt.ptag, tt.pversiontag)
Expand Down
5 changes: 3 additions & 2 deletions pkg/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,9 @@ var (
)

var trustedReusableWorkflows = map[string]bool{
trustedBuilderRepository + "/.github/workflows/generator_generic_slsa3.yml": true,
trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml": true,
trustedBuilderRepository + "/.github/workflows/generator_generic_slsa3.yml": true,
trustedBuilderRepository + "/.github/workflows/builder_go_slsa3.yml": true,
trustedBuilderRepository + "/.github/workflows/generator_container_slsa3.yml": true,
}

// VerifyWorkflowIdentity verifies the signing certificate information
Expand Down