From 70efbcfe1ae4a9eec460d158ad34cc65450e49ac Mon Sep 17 00:00:00 2001 From: Tom Meadows Date: Mon, 11 Dec 2023 17:54:32 +0000 Subject: [PATCH 1/2] Improving `--signer-fulcio-token` flag to accept both path and raw token string (#82) * modified `--signer-fulcio-token` flag to accept either a path to a token or a raw token string * modified `--signer-fulcio-token` flag to accept either a path to a token or a raw token string Signed-off-by: chaosinthecrd * adding an unhappy path to the tests Signed-off-by: chaosinthecrd * updated function and description * updated function and description Signed-off-by: chaosinthecrd * removing ineffectual assignment in test Signed-off-by: chaosinthecrd * updated to add token path flag and remove idToken function magic Signed-off-by: chaosinthecrd * removing ineffectual assignments Signed-off-by: chaosinthecrd * removing whitespace Signed-off-by: chaosinthecrd * fixing small issue and adding test to makefile to speed things up Signed-off-by: chaosinthecrd --------- Signed-off-by: chaosinthecrd --- Makefile | 6 ++++++ hack/test.token | 1 + signer/fulcio/fulcio.go | 39 +++++++++++++++++++++++++++++++----- signer/fulcio/fulcio_test.go | 21 +++++++++++++++++++ 4 files changed, 62 insertions(+), 5 deletions(-) create mode 100644 hack/test.token diff --git a/Makefile b/Makefile index f717d8a7..2738f0f0 100644 --- a/Makefile +++ b/Makefile @@ -18,3 +18,9 @@ controller-gen: $(CONTROLLER_GEN) ## Download controller-gen locally if necessar $(CONTROLLER_GEN): $(LOCALBIN) test -s $(LOCALBIN)/controller-gen && $(LOCALBIN)/controller-gen --version | grep -q $(CONTROLLER_TOOLS_VERSION) || \ GOBIN=$(LOCALBIN) go install sigs.k8s.io/controller-tools/cmd/controller-gen@$(CONTROLLER_TOOLS_VERSION) + +test: ## Run the go unit tests + go test -v -coverprofile=profile.cov -covermode=atomic ./... + +help: ## Display this help screen + @grep -h -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/hack/test.token b/hack/test.token new file mode 100644 index 00000000..fbcb3807 --- /dev/null +++ b/hack/test.token @@ -0,0 +1 @@ +eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmb29iYXIiLCJuYW1lIjoiSm9obiBEb2UiLCJpYXQiOjE1MTYyMzkwMjIsIkVtYWlsIjoidGVzdEBpbi10b3RvLmlvIn0.IswtNc6aJL3zAf-lSGvuz7Okf2tBr-I3ulJ_SRUMt0k diff --git a/signer/fulcio/fulcio.go b/signer/fulcio/fulcio.go index 8983f8c0..449e7553 100644 --- a/signer/fulcio/fulcio.go +++ b/signer/fulcio/fulcio.go @@ -93,7 +93,7 @@ func init() { ), registry.StringConfigOption( "token", - "Raw token to use for authentication", + "Raw token string to use for authentication to fulcio (cannot be used in conjunction with --fulcio-token-path)", "", func(sp signer.SignerProvider, token string) (signer.SignerProvider, error) { fsp, ok := sp.(FulcioSignerProvider) @@ -105,6 +105,20 @@ func init() { return fsp, nil }, ), + registry.StringConfigOption( + "token-path", + "Path to the file containing a raw token to use for authentication to fulcio (cannot be used in conjunction with --fulcio-token)", + "", + func(sp signer.SignerProvider, tokenPath string) (signer.SignerProvider, error) { + fsp, ok := sp.(FulcioSignerProvider) + if !ok { + return sp, fmt.Errorf("provided signer provider is not a fulcio signer provider") + } + + WithTokenPath(tokenPath)(&fsp) + return fsp, nil + }, + ), ) } @@ -113,6 +127,7 @@ type FulcioSignerProvider struct { OidcIssuer string OidcClientID string Token string + TokenPath string } type Option func(*FulcioSignerProvider) @@ -141,6 +156,12 @@ func WithToken(tokenOption string) Option { } } +func WithTokenPath(tokenPathOption string) Option { + return func(fsp *FulcioSignerProvider) { + fsp.TokenPath = tokenPathOption + } +} + func New(opts ...Option) FulcioSignerProvider { fsp := FulcioSignerProvider{} for _, opt := range opts { @@ -194,7 +215,7 @@ func (fsp FulcioSignerProvider) Signer(ctx context.Context) (cryptoutil.Signer, var raw string switch { - case fsp.Token == "" && os.Getenv("GITHUB_ACTIONS") == "true": + case fsp.Token == "" && fsp.TokenPath == "" && os.Getenv("GITHUB_ACTIONS") == "true": tokenURL := os.Getenv("ACTIONS_ID_TOKEN_REQUEST_URL") if tokenURL == "" { return nil, errors.New("ACTIONS_ID_TOKEN_REQUEST_URL is not set") @@ -209,10 +230,18 @@ func (fsp FulcioSignerProvider) Signer(ctx context.Context) (cryptoutil.Signer, if err != nil { return nil, err } - - case fsp.Token != "": + // we want to fail if both flags used (they're mutually exclusive) + case fsp.TokenPath != "" && fsp.Token != "": + return nil, errors.New("only one of --fulcio-token-path or --fulcio-raw-token can be used") + case fsp.Token != "" && fsp.TokenPath == "": raw = fsp.Token + case fsp.TokenPath != "" && fsp.Token == "": + f, err := os.ReadFile(fsp.TokenPath) + if err != nil { + return nil, fmt.Errorf("failed to read fulcio token from filepath %s: %w", fsp.TokenPath, err) + } + raw = string(f) case fsp.Token == "" && isatty.IsTerminal(os.Stdin.Fd()): tok, err := oauthflow.OIDConnect(fsp.OidcIssuer, fsp.OidcClientID, "", "", oauthflow.DefaultIDTokenGetter) if err != nil { @@ -281,7 +310,7 @@ func (fsp FulcioSignerProvider) Signer(ctx context.Context) (cryptoutil.Signer, func getCert(ctx context.Context, key *rsa.PrivateKey, fc fulciopb.CAClient, token string) (*fulciopb.SigningCertificate, error) { t, err := jwt.ParseSigned(token) if err != nil { - return nil, err + return nil, fmt.Errorf("Failed to parse jwt token for fulcio: %w", err) } var claims struct { diff --git a/signer/fulcio/fulcio_test.go b/signer/fulcio/fulcio_test.go index 1a062821..8f5a3dae 100644 --- a/signer/fulcio/fulcio_test.go +++ b/signer/fulcio/fulcio_test.go @@ -24,6 +24,7 @@ import ( "fmt" "log" "net" + "os" "strings" "testing" "time" @@ -33,6 +34,7 @@ import ( fulciopb "github.com/sigstore/fulcio/pkg/generated/protobuf" "github.com/stretchr/testify/require" "go.step.sm/crypto/jose" + "path/filepath" "google.golang.org/grpc" "gopkg.in/square/go-jose.v2/jwt" @@ -198,6 +200,25 @@ func TestSigner(t *testing.T) { _, err = provider.Signer(ctx) //this should be a tranport err since we cant actually test on 443 which is the default require.ErrorContains(t, err, "lookup test") + + // Test signer with token read from file + // NOTE: this function could be refactored to accept a fileSystem or io.Reader so reading the file can be mocked, + // but unsure if this is the way we want to go for now + wd, err := os.Getwd() + if err != nil { + t.Fatalf("failed to get working directory: %v", err) + } + rootDir := filepath.Dir(filepath.Dir(wd)) + tp := filepath.Join(rootDir, "hack", "test.token") + + provider = New(WithFulcioURL(fmt.Sprintf("http://%v:%v", hostname, port)), WithTokenPath(tp)) + _, err = provider.Signer(ctx) + require.NoError(t, err) + + // Test signer with both token read from file and raw token + provider = New(WithFulcioURL(fmt.Sprintf("http://%v:%v", hostname, port)), WithTokenPath(tp), WithToken(token)) + _, err = provider.Signer(ctx) + require.ErrorContains(t, err, "only one of --fulcio-token-path or --fulcio-raw-token can be used") } func generateCertChain(t *testing.T) []string { From 737eed88d5d9c67a6a695ffadea23dd9de189fa7 Mon Sep 17 00:00:00 2001 From: Tom Meadows Date: Mon, 11 Dec 2023 18:17:53 +0000 Subject: [PATCH 2/2] Updating README (#97) * Updating README with more relevant information for the `go-witness` library. --------- Signed-off-by: Tom Meadows --- README.md | 326 +++++------------------------------------------------- 1 file changed, 28 insertions(+), 298 deletions(-) diff --git a/README.md b/README.md index 53428b47..9579bb17 100644 --- a/README.md +++ b/README.md @@ -1,302 +1,32 @@ +# go-witness +A client library for [Witness](https://github.com/in-toto/witness), written in Go. + +[![Go Reference](https://pkg.go.dev/badge/github.com/in-toto/go-witness.svg)](https://pkg.go.dev/github.com/in-toto/go-witness) +[![Go Report Card](https://goreportcard.com/badge/github.com/in-toto/go-witness)](https://goreportcard.com/report/github.com/in-toto/go-witness) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/8164/badge)](https://www.bestpractices.dev/projects/8164) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/in-toto/go-witness/badge)](https://securityscorecards.dev/viewer/?uri=github.com/in-toto/go-witness) [![FOSSA Status](https://app.fossa.com/api/projects/custom%2B41709%2Fgithub.com%2Fin-toto%2Fgo-witness.svg?type=shield&issueType=license)](https://app.fossa.com/projects/custom%2B41709%2Fgithub.com%2Fin-toto%2Fgo-witness?ref=badge_shield&issueType=license) -[![asciicast](https://asciinema.org/a/2DZRRh8uzrzHcUVL8md86Zj4D.svg)](https://asciinema.org/a/2DZRRh8uzrzHcUVL8md86Zj4D) - -## Witness is a pluggable framework for supply chain security - -Witness prevents tampering of build materials and verifies the integrity of the build process from source to target. It works by wrapping commands executed in a continuous integration process. Its attestation system is pluggable and offers support out of the box for most major CI and infrastructure providers. Verification of Witness metadata and a secure PKI distribution system will mitigate against many software supply chain attack vectors and can be used as a framework for automated governance. - -Witness is an implementation of the in-toto spec including [ITE-5](https://github.com/in-toto/ITE/tree/master/ITE/5), [ITE-6](https://github.com/in-toto/ITE/tree/master/ITE/6), [ITE-7](https://github.com/in-toto/ITE/tree/master/ITE/7) with an [embedded rego policy engine](https://www.openpolicyagent.org/docs/latest/integration/#integrating-with-the-go-api). - -- Does **NOT** require elevated privileges. -- Can run in a containerized or non-containerized environment -- Records secure hashes of materials, artifacts, and events occurring during the CI process -- Integrations with cloud identity services -- Keyless signing with [SPIFFE/SPIRE](https://spiffe.io/) -- Support for uploading attestation evidence to rekor server (sigstore) -- Build policy enforcement with Open Policy Agent. -- Alpha support for tracing and process tampering prevention -- Verifies file integrity between CI steps, and across air gap. -- Experimental Windows and ARM Support - -## Usage - -- [Run](docs/witness_run.md) - Runs the provided command and records attestations about the execution. -- [Sign](docs/witness_sign.md) - Signs the provided file with the provided key. -- [Verify](docs/witness_verify.md) - Verifies a witness policy. - -## TOC - -- [Witness Attestors](#witness-attestors) - - [What is a witness attestor?](#what-is-a-witness-attestor) - - [Attestor Security Model](#attestor-security-model) - - [Attestor Life Cycle](#attestor-life-cycle) - - [Attestation Lifecycle](#attestation-lifecycle) - - [Attestor Types](#attestor-types) - - [Pre Run Attestors](#pre-run-attestors) - - [Internal Attestors](#internal-attestors) - - [Post Run Attestors](#post-run-attestors) - - [AttestationCollection](#attestationcollection) - - [Attestor Subjects](#attestor-subjects) - - [Witness Policy](#witness-policy) - - [What is a witness policy?](#what-is-a-witness-policy) - - [Witness Verification](#witness-verification) - - [Verification Lifecycle](#verification-lifecycle) - - [Using SPIRE for Keyless Signing](#using-spire-for-keyless-signing) - - [Witness Examples](#witness-examples) - - [Media](#media) - - [Roadmap](#roadmap) - - [Support](#support) - -## Getting Started - -### Download the Binary -[Releases](https://github.com/testifysec/witness/releases) -``` -curl -LO https://github.com/testifysec/witness/releases/download/${VERSION}/witness_${VERSION}_${ARCH}.tar.gz -tar -xzf witness_${VERSION}_${ARCH}.tar.gz -``` - -### Create a Keypair - -> Witness supports keyless signing with [SPIRE](https://spiffe.io/)! - -``` -openssl genpkey -algorithm ed25519 -outform PEM -out testkey.pem -openssl pkey -in testkey.pem -pubout > testpub.pem -``` - -### Create a Witness configuration - -> - This file generally resides in your source code repository along with the public keys generated above. -> - `.witness yaml` is the default location for the configuration file -> - `witness help` will show all configuration options -> - command-line arguments overrides configuration file values. - -``` -## .witness.yaml - -run: - key: testkey.pem - trace: false -verify: - attestations: - - "test-att.json" - policy: policy-signed.json - publickey: testpub.pem -``` - -### Record attestations for a build step - -> - The `-a {attestor}` flag allows you to define which attestors run -> - ex. `-a maven -a was -a gitlab` would be used for a maven build running on a GitLab runner on GCP. -> - Defining step names is important, these will be used in the policy. -> - This should happen as a part of a CI step - -``` -witness run --step build -o test-att.json -- go build -o=testapp . -``` - -### View the attestation data in the signed DSSE Envelope - -> - This data can be stored and retrieved from rekor! -> - This is the data that is evaluated against the Rego policy - -``` -cat test-att.json | jq -r .payload | base64 -d | jq -``` - -### Create a Policy File - -Look [here](docs/policy.md) for full documentation on Witness Policies. - -> - Make sure to replace the keys in this file with the ones from the step above (sed command below). -> - Rego policies should be base64 encoded -> - Steps are bound to keys. Policy can be written to check the certificate data. For example, we can require a step is signed by a key with a specific `CN` attribute. -> - Witness will require all attestations to succeed -> - Witness will evaluate the rego policy against the JSON object in the corresponding attestor - -``` -## policy.json - -{ - "expires": "2023-12-17T23:57:40-05:00", - "steps": { - "build": { - "name": "build", - "attestations": [ - { - "type": "https://witness.dev/attestations/material/v0.1", - "regopolicies": [] - }, - { - "type": "https://witness.dev/attestations/command-run/v0.1", - "regopolicies": [] - }, - { - "type": "https://witness.dev/attestations/product/v0.1", - "regopolicies": [] - } - ], - "functionaries": [ - { - "publickeyid": "{{PUBLIC_KEY_ID}}" - } - ] - } - }, - "publickeys": { - "{{PUBLIC_KEY_ID}}": { - "keyid": "{{PUBLIC_KEY_ID}}", - "key": "{{B64_PUBLIC_KEY}}" - } - } -} -``` - -### Replace the variables in the policy - -``` -id=`sha256sum testpub.pem | awk '{print $1}'` && sed -i "s/{{PUBLIC_KEY_ID}}/$id/g" policy.json -pubb64=`cat testpub.pem | base64 -w 0` && sed -i "s/{{B64_PUBLIC_KEY}}/$pubb64/g" policy.json -``` - -### Sign The Policy File - -Keep this key safe, its owner will control the policy gates. - -``` -witness sign -f policy.json --key testkey.pem --outfile policy-signed.json -``` - -### Verify the Binary Meets Policy Requirements - -> This process works across air-gap as long as you have the signed policy file, correct binary, and public key or certificate authority corresponding to the private key that signed the policy. -> `witness verify` will return a `non-zero` exit and reason in the case of failure. Success will be silent with a `0` exit status -> for policies that require multiple steps, multiple attestations are required. - -``` -witness verify -f testapp -a test-att.json -p policy-signed.json -k testpub.pem -``` - -# Witness Attestors - -## What is a witness attestor? - -Witness attestors are pieces of code that assert facts about a system and store those facts in a versioned schema. Each attestor has a `Name`, `Type`, and `RunType`. The `Type` is a versioned string corresponding to the JSON schema of the attestation. For example, the AWS attestor is defined as follows: - -``` - Name = "aws" - Type = "https://witness.dev/attestations/aws/v0.1" - RunType = attestation.PreRunType -``` - -The attestation types are used when we evaluate policy against these attestations. - -## Attestor Security Model - -Attestations are only as secure as the data that feeds them. Where possible cryptographic material should be validated, evidence of validation should be included in the attestation for out-of-band validation. - -Examples of cryptographic validation is found in the [GCP](https://github.com/in-toto/go-witness/blob/main/attestation/gcp-iit/gcp-iit.go), [AWS](https://github.com/in-toto/go-witness/blob/main/attestation/aws-iid/aws-iid.go), and [GitLab](https://github.com/in-toto/go-witness/blob/main/attestation/gitlab/gitlab.go) attestors. - -## Attestor Life Cycle - -- **Pre-material:** Pre-material attestors run before any other attestors. These attestors generally collect information about the environment. - -- **Material:** Material attestors run after any prematerial attestors and prior to any execute attestors. Generally these collect information about state that may change after any execute attestors, such as file hashes. - -- **Execute:**: Execute attestors run after any material attestors and generally record information about some command or process that is to be executed. - -- **Product:** Product attestors run after any execute attestors and generally record information about what changed during the execute lifecycle step, such as changed or created files. - -- **Post-product:** Post-product attestors run after product attestors and generally record some additional information about specific products, such as OCI image information from a saved image tarball. - -### Attestation Lifecycle - -![](docs/assets/attestation.png) - -## Attestor Types - -### Pre-material Attestors -- [AWS](docs/attestors/aws-iid.md) - Attestor for AWS Instance Metadata -- [GCP](docs/attestors/gcp-iit.md) - Attestor for GCP Instance Identity Service -- [GitLab](docs/attestors/gitlab.md) - Attestor for GitLab Pipelines -- [Git](docs/attestors/git.md) - Attestor for Git Repository -- [Maven](docs/attestors/maven.md) Attestor for Maven Projects -- [Environment](docs/attestors/environment.md) - Attestor for environment variables (**_be careful with this - there is no way to mask values yet_**) -- [JWT](docs/attestors/jwt.md) - Attestor for JWT Tokens - -### Material Attestors -- [Material](docs/attestors/material.md) - Records secure hashes of files in current working directory - -### Execute Attestors -- [CommandRun](docs/attestors/commandrun.md) - Records traces and metadata about the actual process being run - -### Product Attestors -- [Product](docs/attestors/product.md) - Records secure hashes of files produced by commandrun attestor (only detects new files) - -### Post-product Attestors - -- [OCI](docs/attestors/oci.md) - Attestor for tar'd OCI images - -### AttestationCollection - -An `attestationCollection` is a collection of attestations that are cryptographically bound together. Because the attestations are bound together, we can trust that they all happened as part of the same attesation life cycle. Witness policy defines which attestations are required. - -### Attestor Subjects - -Attestors define subjects that act as lookup indexes. The attestationCollection can be looked up by any of the subjects defined by the attestors. - -## Witness Policy - -### What is a witness policy? - -A witness policy is a signed document that encodes the requirements for an artifact to be validated. A witness policy includes public keys for trusted functionaries, which attestations must be found, and rego policy to evaluate against the attestation meta-data. - -I witness policy allowers administrators trace the compliance status of an artifact at any point during it's lifecycle. - -## Witness Verification - -### Verification Lifecycle - -![](docs/assets/verification.png) - -## Using [SPIRE](https://github.com/spiffe/spire) for Keyless Signing - -Witness can consume ephemeral keys from a [SPIRE](https://github.com/spiffe/spire) node agent. Configure witness with the flag `--spiffe-socket` to enable keyless signing. - -During the verification process witness will use the [Rekor](https://github.com/sigstore/rekor) integrated time to make a determination on certificate validity. The SPIRE certificate only needs to remain valid long enough for the attestation to be integrated into the Rekor log. - -## Witness Examples - -- [Using Witness To Prevent SolarWinds Type Attacks](examples/solarwinds/README.md) -- [Using Witness To Find Artifacts With Hidden Vulnerable Log4j Dependencies](examples/log4shell/README.md) - -## Media - -- [Blog - What is a supply chain attestation, and why do I need it?](https://www.testifysec.com/blog/what-is-a-supply-chain-attestation/) -- [Talk - Securing the Software Supply Chain with the in-toto & SPIRE projects](https://www.youtube.com/watch?v=4lFbdkB62QI) -- [Talk - Securing the Software Supply Chain with SBOM and Attestation](https://www.youtube.com/watch?v=wX6aTZfpJv0) - -## Roadmap - -- Attestors for all major platforms -- CaC Card Attestor -- GovCloud Attestor -- OIDC Attestor -- FIDO Attestor -- Vault Key Provider -- Cloud KMS Support -- Kubernetes Admission Controller -- SIEM Collection Agent -- Cosign Signature Validation -- Notary v2 Signature Validation -- [Zarf](https://github.com/defenseunicorns/zarf) Integration -- IronBank Attestor - -## Support - -[TestifySec](https://testifysec.com) Provides support for witness and other CI security tools. -[Contact Us](mailto:info@testifysec.com) +## Status +This library is currently pre-1.0 and therefore the API may be subject to breaking changes. + +## Features +- Creation and signing of in-toto attestations +- Verification of in-toto attestations and associated signatures with: + - Witness policy engine + - [OPA Rego policy language](https://www.openpolicyagent.org/docs/latest/policy-language/) +- A growing list of attestor types defined under a common interface +- A selection of attestation sources to search for attestation collections + +## Documentation +For more detail regarding the library itself, we recommend viewing [pkg.go.dev](https://pkg.go.dev/github.com/testifysec/go-witness). For +the documentation of the witness project, please view [the main witness repository](https://github.com/in-toto/witness/tree/main/docs). + +## Requirements +In order to effectively contribute to this library, you will need: +- A Unix-compatible Operating System +- GNU Make +- Go 1.19 + +## Running Tests +This repository uses Go tests for testing. You can run these tests by executing `make test`.