Skip to content
This repository has been archived by the owner on Apr 9, 2024. It is now read-only.

Commit

Permalink
Check the hash of the binary in the verifier (#124)
Browse files Browse the repository at this point in the history
* Checks the hash of the binary in the verifier
* Uses a simpler provenance file for provenance verification
* Replaces oak with a simple example in testdata/build.toml
* Adds coverage report to the CI
* Adds negative tests for verifier
* Modifies ParseProvenanceFile to return ValidatedProvenance
  • Loading branch information
rbehjati authored Sep 14, 2022
1 parent 9afcf09 commit d98c4b9
Show file tree
Hide file tree
Showing 20 changed files with 183 additions and 51 deletions.
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,26 @@ jobs:
run: |
"${GITHUB_WORKSPACE}/bin/bazel" test --test_output=streamed //...
# TODO(#113): Find a better, and easier-to-navigate way to report coverage.
# Generate coverage report for ./pkg/... and ./internal/... and upload the report as an html file.
# See `go help testflag` and `go tool cover -help` for more options.
test-coverage:
runs-on: ubuntu-20.04
steps:
- uses: actions/checkout@v3
- uses: actions/setup-go@v3
with:
go-version: "=1.18.4"
- name: Run go test
run: |
go test -coverpkg=./... -coverprofile coverage.out ./pkg/... ./internal/...
go tool cover -func=coverage.out
go tool cover -html=coverage.out -o coverage.html
- uses: actions/upload-artifact@v3
with:
name: coverage.html
path: coverage.html

check-license-lines:
runs-on: ubuntu-20.04
steps:
Expand Down
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,17 +25,17 @@ in the [`common`](/internal/common/) package.

The [`cmd/builder`](/cmd/builder/) command line tool described above can be used for building the binaries, and at the same time for generating a corresponding provenance file. To use this tool, the developers need to provide a toml file similar to the one in [`testdata/build.toml`](/testdata/build.toml). See the definition of `BuildConfig` in package [`common`](/internal/common/) for thedescription of each field.

To build a binary from the Git repository specified in [`testdata/build.toml`](../testdata/build.toml) and generate its provenance file, run either:
To build a binary from the Git repository specified in [`testdata/oak_build.toml`](../testdata/oak_build.toml) and generate its provenance file, run either:

```bash
$ bazel run //cmd/builder:main -- \
-build_config_path <absolute-path-to-transparent-release-repo>/testdata/build.toml \
-build_config_path <absolute-path-to-transparent-release-repo>/testdata/oak_build.toml \
```

or, alternatively:

```bash
$ go run cmd/builder/main.go -build_config_path testdata/build.toml
$ go run cmd/builder/main.go -build_config_path testdata/oak_build.toml
```

You should see the following output on the console:
Expand All @@ -51,7 +51,7 @@ To build from a local repository you can specify `-git_root_dir`. In this case,

```bash
$ bazel run //cmd/builder:main -- \
-build_config_path <absolute-path-to-transparent-release>/testdata/build.toml \
-build_config_path <absolute-path-to-transparent-release>/testdata/oak_build.toml \
-git_root_dir <path-to-git-repo-root>
```

Expand Down
6 changes: 3 additions & 3 deletions experimental/auth-logic/wrappers/provenance_build_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ type simplifiedProvenance struct {
// by emitting the authorization logic statement.
func (pbw ProvenanceBuildWrapper) EmitStatement() (UnattributedStatement, error) {
// Unmarshal a provenance struct from the JSON file.
provenance, err := amber.ParseProvenanceFile(pbw.ProvenanceFilePath)
validatedProvenance, err := amber.ParseProvenanceFile(pbw.ProvenanceFilePath)
if err != nil {
return UnattributedStatement{}, fmt.Errorf("provenance build wrapper couldn't parse provenance file: %v", err)
}
Expand All @@ -52,8 +52,8 @@ func (pbw ProvenanceBuildWrapper) EmitStatement() (UnattributedStatement, error)
}

simpleProv := simplifiedProvenance{
AppName: SanitizeName(provenance.Subject[0].Name),
MeasuredSha256: provenance.Subject[0].Digest["sha256"],
AppName: SanitizeName(validatedProvenance.GetBinaryName()),
MeasuredSha256: validatedProvenance.GetBinarySHA256Hash(),
}

policyTemplate, err := template.ParseFiles(provenanceBuilderTemplate)
Expand Down
27 changes: 8 additions & 19 deletions experimental/auth-logic/wrappers/provenance_wrapper.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,22 +32,15 @@ type ProvenanceWrapper struct{ FilePath string }
// EmitStatement implements the wrapper interface for ProvenanceWrapper
// by emitting the authorization logic statement.
func (p ProvenanceWrapper) EmitStatement() (UnattributedStatement, error) {
provenance, err := amber.ParseProvenanceFile(p.FilePath)
validatedProvenance, err := amber.ParseProvenanceFile(p.FilePath)
if err != nil {
return UnattributedStatement{}, fmt.Errorf("provenance wrapper couldn't prase provenance file: %v", err)
}

if len(provenance.Subject) != 1 {
return UnattributedStatement{}, fmt.Errorf("provenance file missing subject")
}

sanitizedAppName := SanitizeName(provenance.Subject[0].Name)
expectedHash, hashOk := provenance.Subject[0].Digest["sha256"]

if !hashOk {
return UnattributedStatement{}, fmt.Errorf("provenance file did not give an expected hash")
}
sanitizedAppName := SanitizeName(validatedProvenance.GetBinaryName())
expectedHash := validatedProvenance.GetBinarySHA256Hash()

provenance := validatedProvenance.GetProvenance()
predicate := provenance.Predicate.(slsa.ProvenancePredicate)
builderName := predicate.Builder.ID

Expand All @@ -64,14 +57,10 @@ func (p ProvenanceWrapper) EmitStatement() (UnattributedStatement, error) {
// application it is about. This is useful for generating principal names,
// for example.
func GetAppNameFromProvenance(provenanceFilePath string) (string, error) {
provenance, provenanceErr := amber.ParseProvenanceFile(provenanceFilePath)
if provenanceErr != nil {
return "", provenanceErr
}

if len(provenance.Subject) != 1 {
return "", fmt.Errorf("provenance file missing subject")
validatedProvenance, err := amber.ParseProvenanceFile(provenanceFilePath)
if err != nil {
return "", fmt.Errorf("provenance wrapper couldn't prase provenance file: %v", err)
}

return provenance.Subject[0].Name, nil
return validatedProvenance.GetBinaryName(), nil
}
1 change: 0 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ require (
github.com/cyberphone/json-canonicalization v0.0.0-20210303052042-6bc126869bf4
github.com/go-openapi/runtime v0.23.3
github.com/go-openapi/strfmt v0.21.2
github.com/google/go-cmp v0.5.7
github.com/in-toto/in-toto-golang v0.3.4-0.20211211042327-af1f9fb822bf
github.com/pelletier/go-toml v1.9.4
github.com/sigstore/rekor v0.6.0
Expand Down
1 change: 0 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1947,7 +1947,6 @@ golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.0.0-20160322025152-9bf6e6e569ff/go.mod h1:4mhQ8q/RsB7i+udVvVy5NUi08OU8ZlA0gRVgrF7VFY0=
google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk=
Expand Down
1 change: 1 addition & 0 deletions internal/common/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ go_test(
"//schema/amber-slsa-buildtype/v1:example.json",
"//schema/amber-slsa-buildtype/v1:provenance.json",
"//testdata:build.toml",
"//testdata:provenance.json",
"//testdata:static.txt",
],
embed = [":common"],
Expand Down
24 changes: 19 additions & 5 deletions internal/common/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,11 +90,8 @@ func LoadBuildConfigFromFile(path string) (*BuildConfig, error) {
}

// LoadBuildConfigFromProvenance loads build configuration from a SLSA Provenance object.
func LoadBuildConfigFromProvenance(statement *intoto.Statement) (*BuildConfig, error) {
if len(statement.Subject) != 1 {
return nil, fmt.Errorf("the provenance statement must have exactly one Subject, got %d", len(statement.Subject))
}

func LoadBuildConfigFromProvenance(provenance *amber.ValidatedProvenance) (*BuildConfig, error) {
statement := provenance.GetProvenance()
predicate := statement.Predicate.(slsa.ProvenancePredicate)
if len(predicate.Materials) != 2 {
return nil, fmt.Errorf("the provenance must have exactly two Materials, got %d", len(predicate.Materials))
Expand Down Expand Up @@ -318,6 +315,23 @@ func (b *BuildConfig) ChangeDirToGitRoot(gitRootDir string) (*RepoCheckoutInfo,
return info, nil
}

// VerifyBinarySha256Hash computes the SHA256 hash of the binary built by this
// BuildConfig, and checks that this hash is equal to the given `expectedSha256Hash`.
// Returns an error if the hashes are not equal.
func (b *BuildConfig) VerifyBinarySha256Hash(expectedBinarySha256Hash string) error {
binarySha256Hash, err := b.ComputeBinarySha256Hash()
if err != nil {
return fmt.Errorf("couldn't get the hash of the binary: %v", err)
}

if binarySha256Hash != expectedBinarySha256Hash {
return fmt.Errorf("the hash of the generated binary does not match the expected SHA256 hash; got %s, want %s",
binarySha256Hash, expectedBinarySha256Hash)
}

return nil
}

func parseBuilderImageURI(imageURI string) (string, string, error) {
// We expect the URI of the builder image to be of the form NAME@DIGEST
URIParts := strings.Split(imageURI, "@")
Expand Down
8 changes: 4 additions & 4 deletions internal/common/common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import (

const (
testdataPath = "../../testdata/"
provenanceExamplePath = "schema/amber-slsa-buildtype/v1/example.json"
provenanceExamplePath = "testdata/provenance.json"
wantTOMLHash = "322527c0260e25f0e9a2595bd0d71a52294fe2397a7af76165190fd98de8920d"
wantBuilderImageID = "6e5beabe4ace0e3aaa01ce497f5f1ef30fed7c18c596f35621751176b1ab583d"
wantSHA1HexDigitLength = 40
Expand Down Expand Up @@ -131,12 +131,12 @@ func TestGenerateProvenanceStatement(t *testing.T) {
buildConfig := predicate.BuildConfig.(amber.BuildConfig)

// Check that the provenance is generated correctly
testutil.AssertEq(t, "repoURL", predicate.Materials[1].URI, "https://github.com/project-oak/oak")
testutil.AssertEq(t, "repoURL", predicate.Materials[1].URI, "https://github.com/project-oak/transparent-release")
testutil.AssertNonEmpty(t, "subjectName", prov.Subject[0].Name)
testutil.AssertEq(t, "subjectDigest", len(prov.Subject[0].Digest["sha256"]), wantSHA256HexDigitLength)
testutil.AssertEq(t, "commitHash length", len(predicate.Materials[1].Digest["sha1"]), wantSHA1HexDigitLength)
testutil.AssertEq(t, "builderImageID length", len(predicate.Materials[0].Digest["sha256"]), wantSHA256HexDigitLength)
testutil.AssertEq(t, "builderImageURI", predicate.Materials[0].URI, fmt.Sprintf("gcr.io/oak-ci/oak@sha256:%s", predicate.Materials[0].Digest["sha256"]))
testutil.AssertEq(t, "builderImageURI", predicate.Materials[0].URI, fmt.Sprintf("bash@sha256:%s", predicate.Materials[0].Digest["sha256"]))
testutil.AssertNonEmpty(t, "command[0]", buildConfig.Command[0])
testutil.AssertNonEmpty(t, "command[1]", buildConfig.Command[1])
}
Expand All @@ -147,7 +147,7 @@ func checkBuildConfig(got *BuildConfig, t *testing.T) {
t.Fatalf("couldn't parse imageURI (%q): %v", got.BuilderImage, err)
}
// Check that the provenance is generated correctly
testutil.AssertEq(t, "repoURL", got.Repo, "https://github.com/project-oak/oak")
testutil.AssertEq(t, "repoURL", got.Repo, "https://github.com/project-oak/transparent-release")
testutil.AssertEq(t, "commitHash length", len(got.CommitHash), wantSHA1HexDigitLength)
testutil.AssertEq(t, "builderImageID length", len(digest), wantSHA256HexDigitLength)
testutil.AssertEq(t, "builderImageID digest algorithm", alg, "sha256")
Expand Down
3 changes: 3 additions & 0 deletions internal/verifier/BUILD
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ go_test(
data = [
"//schema/amber-slsa-buildtype/v1:example.json",
"//schema/amber-slsa-buildtype/v1:provenance.json",
"//testdata:provenance.json",
"//testdata:bad_command_provenance.json",
"//testdata:invalid_hash_provenance.json",
],
embed = [":verifier"],
deps = ["//internal/testutil"],
Expand Down
11 changes: 10 additions & 1 deletion internal/verifier/verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type ReproducibleProvenanceVerifier struct {
// it and verifying that the resulting binary has a hash equal to the one
// specified in the subject of the given provenance file. If the hashes are
// different returns an error, otherwise returns nil.
// TODO(#126): Refactor and separate verification logic from the logic for reading the file.
func (verifier *ReproducibleProvenanceVerifier) Verify(provenanceFilePath string) error {
provenance, err := amber.ParseProvenanceFile(provenanceFilePath)
if err != nil {
Expand All @@ -67,6 +68,13 @@ func (verifier *ReproducibleProvenanceVerifier) Verify(provenanceFilePath string
return fmt.Errorf("couldn't build the binary: %v", err)
}

// The provenance is valid, therefore `expectedBinaryHash` is guaranteed to be non-empty.
expectedBinaryHash := provenance.GetBinarySHA256Hash()

if err := buildConfig.VerifyBinarySha256Hash(expectedBinaryHash); err != nil {
return fmt.Errorf("failed to verify the hash of the built binary: %v", err)
}

return nil
}

Expand All @@ -81,13 +89,14 @@ type AmberProvenanceMetadataVerifier struct {
// AmberProvenanceMetadataVerifier instance. Returns an error if any of the
// values is not as expected. Otherwise returns nil, indicating success.
// TODO(#69): Check metadata against the expected values.
// TODO(#126): Refactor and separate verification logic from the logic for reading the file.
func (verifier *AmberProvenanceMetadataVerifier) Verify(provenanceFilePath string) error {
provenance, err := amber.ParseProvenanceFile(provenanceFilePath)
if err != nil {
return fmt.Errorf("couldn't load the provenance file from %s: %v", provenanceFilePath, err)
}

predicate := provenance.Predicate.(slsa.ProvenancePredicate)
predicate := provenance.GetProvenance().Predicate.(slsa.ProvenancePredicate)

if predicate.BuildType != amber.AmberBuildTypeV1 {
return fmt.Errorf("incorrect BuildType: got %s, want %v", predicate.BuildType, amber.AmberBuildTypeV1)
Expand Down
49 changes: 45 additions & 4 deletions internal/verifier/verifier_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@ package verify

import (
"os"
"strings"
"testing"

"github.com/project-oak/transparent-release/internal/testutil"
)

const schemaExamplePath = "schema/amber-slsa-buildtype/v1/example.json"
const validProvenancePath = "testdata/provenance.json"
const invalidHashProvenancePath = "testdata/invalid_hash_provenance.json"
const badCommandProvenancePath = "testdata/bad_command_provenance.json"

func TestReproducibleProvenanceVerifier(t *testing.T) {
func TestReproducibleProvenanceVerifier_validProvenance(t *testing.T) {
// The path to provenance is specified relative to the root of the repo, so we need to go one level up.
// Get the current directory before that to restore the path at the end of the test.
currentDir, err := os.Getwd()
Expand All @@ -34,11 +37,49 @@ func TestReproducibleProvenanceVerifier(t *testing.T) {
testutil.Chdir(t, "../../")
verifier := ReproducibleProvenanceVerifier{}

if err := verifier.Verify(schemaExamplePath); err != nil {
if err := verifier.Verify(validProvenancePath); err != nil {
t.Fatalf("couldn't verify the provenance file: %v", err)
}
}

// TODO(#126): Update the test once Verify is refactored.
func TestReproducibleProvenanceVerifier_invalidHash(t *testing.T) {
// The path to provenance is specified relative to the root of the repo, so we need to go one level up.
// Get the current directory before that to restore the path at the end of the test.
currentDir, err := os.Getwd()
if err != nil {
t.Fatalf("couldn't get current directory: %v", err)
}
defer testutil.Chdir(t, currentDir)
testutil.Chdir(t, "../../")
verifier := ReproducibleProvenanceVerifier{}

want := "failed to verify the hash of the built binary"

if got := verifier.Verify(invalidHashProvenancePath); !strings.Contains(got.Error(), want) {
t.Fatalf("got %v, want error message containing %q,", got, want)
}
}

// TODO(#126): Update the test once Verify is refactored.
func TestReproducibleProvenanceVerifier_badCommand(t *testing.T) {
// The path to provenance is specified relative to the root of the repo, so we need to go one level up.
// Get the current directory before that to restore the path at the end of the test.
currentDir, err := os.Getwd()
if err != nil {
t.Fatalf("couldn't get current directory: %v", err)
}
defer testutil.Chdir(t, currentDir)
testutil.Chdir(t, "../../")
verifier := ReproducibleProvenanceVerifier{}

want := "couldn't build the binary"

if got := verifier.Verify(badCommandProvenancePath); !strings.Contains(got.Error(), want) {
t.Fatalf("got %v, want error message containing %q,", got, want)
}
}

func TestAmberProvenanceMetadataVerifier(t *testing.T) {
// The path to provenance is specified relative to the root of the repo, so we need to go one level up.
// Get the current directory before that to restore the path at the end of the test.
Expand All @@ -50,7 +91,7 @@ func TestAmberProvenanceMetadataVerifier(t *testing.T) {
testutil.Chdir(t, "../../")
verifier := AmberProvenanceMetadataVerifier{}

if err := verifier.Verify(schemaExamplePath); err != nil {
if err := verifier.Verify(validProvenancePath); err != nil {
t.Fatalf("couldn't verify the provenance file: %v", err)
}
}
Loading

0 comments on commit d98c4b9

Please sign in to comment.