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

Check the hash of the binary in the verifier #124

Merged
merged 9 commits into from
Sep 14, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()

Choose a reason for hiding this comment

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

I would still add explicit checks here, so that if the provenance happens to be malformed (e.g. the digest empty), we will get a nice error message. But if it's already guaranteed to be covered elsewhere, feel free to ignore (though I would still be a bit worried that over time it may get out of sync).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Modified ParseProvenanceFile to return a ValidatedProvenance, to guarantee that the condition is already checked. Although, Golang is not very good at these things, since it is not easy to clone structs, or declare immutable variables.

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.

Choose a reason for hiding this comment

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

Why does the path need to be restored? Does it mean that it affects the execution of other test cases? That would seem problematic, since the execution of the tests would differ whether they are running concurrently or not. Is it possible to make the entire process not depend on the PWD and instead use absolute paths for everything? It would still be possible here to concat PWD with a relative path to obtain an absolute path and use that, but at least that would not mutate global state.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is basically because when provenances are loaded, they are also validated against the schema. The path of the schema is hardcoded inside some other file, and getting the path right requires changing the directory.

I have not been able to come up with a reliable and neat solution yet, but it certainly needs to be fixed in a separate PR. We have this bit of code in all tests, and it is causing a lot of clutter.

We plan to switch to the SLSA format, and retire the amber provenance format. So it probably is better to wait for that change. I believe this problem will automatically go away when that change is completed. Created #127.

Choose a reason for hiding this comment

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

+1 I think the basic functions (which are tested here, e.g. verifier.Verify) should deal with the actual content of the files (as strings), they should not assume they can read a file by its path. Then we can have helpers to just read the file at a path, and we can compose that by passing its content to the function in question.

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) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Really cool test! As idea: If we need the bad_command_provenance.json only in this test, shall we maybe hard code it as string in the test? Might also be faster to see the whole test then.

Similar with the invalidHashProvenancePath?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is a very good idea, and I agree. Let me do this in a separate PR since doing it nicely requires some refactoring in verifier.Verify(), which is the unit under test. It currently takes the path to a file as input. Created #126.

// 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