From 0cd05b61f8ba81310806680001614ff7034e1d52 Mon Sep 17 00:00:00 2001 From: Kairo Araujo Date: Wed, 8 May 2024 06:09:14 +0200 Subject: [PATCH] feat: Enable Witness Policy verify from Archivista (#438) * feat: Enable Witness Policy verify from Archivista Enables Witness to retrieve Policy from Archivista. If a user provides a Policy `gitoid`, Witness will attempt to retrieve it from Archivista. To maintain backward compatibility, Witness first tries to load the file locally. If the local file loading fails and an Archivista is configured, it will retrieve the Policy from Archivista. --------- Signed-off-by: Kairo Araujo --- .gitignore | 4 +- Makefile | 3 + cmd/verify.go | 38 +++++------ go.mod | 1 + go.sum | 2 + internal/archivista/archivista.go | 58 +++++++++++++++++ internal/policy/policy.go | 57 +++++++++++++++++ internal/policy/policy_test.go | 103 ++++++++++++++++++++++++++++++ test/policy-hello-signed.json | 10 +++ 9 files changed, 257 insertions(+), 19 deletions(-) create mode 100644 internal/archivista/archivista.go create mode 100644 internal/policy/policy.go create mode 100644 internal/policy/policy_test.go create mode 100644 test/policy-hello-signed.json diff --git a/.gitignore b/.gitignore index f9bff618..52f824a6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ bin/ build/ vendor/ .dccache +.vscode +.profile.cov test/testapp test/test-attestation.json test/policy-signed.json @@ -15,4 +17,4 @@ sarif-report.json test/log node_modules .DS_Store -docs-website/.docusaurus \ No newline at end of file +docs-website/.docusaurus diff --git a/Makefile b/Makefile index f5cadd08..f21635ba 100644 --- a/Makefile +++ b/Makefile @@ -17,6 +17,9 @@ vet: ## Run go vet test: ## Run go tests go test -v -coverprofile=profile.cov -covermode=atomic ./... +coverage: ## Show the coverage + go tool cover -html=profile.cov + docgen: ## Generate the docs go run ./docgen diff --git a/cmd/verify.go b/cmd/verify.go index 9873cfe0..9a225861 100644 --- a/cmd/verify.go +++ b/cmd/verify.go @@ -1,4 +1,4 @@ -// Copyright 2021 The Witness Contributors +// Copyright 2021-2024 The Witness Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package cmd import ( "context" "crypto" - "encoding/json" "errors" "fmt" "os" @@ -25,9 +24,10 @@ import ( witness "github.com/in-toto/go-witness" "github.com/in-toto/go-witness/archivista" "github.com/in-toto/go-witness/cryptoutil" - "github.com/in-toto/go-witness/dsse" "github.com/in-toto/go-witness/log" "github.com/in-toto/go-witness/source" + archivista_client "github.com/in-toto/witness/internal/archivista" + "github.com/in-toto/witness/internal/policy" "github.com/in-toto/witness/options" "github.com/spf13/cobra" ) @@ -64,6 +64,22 @@ const ( // todo: this logic should be broken out and moved to pkg/ // we need to abstract where keys are coming from, etc func runVerify(ctx context.Context, vo options.VerifyOptions, verifiers ...cryptoutil.Verifier) error { + var ( + collectionSource source.Sourcer + archivistaClient *archivista.Client + ) + memSource := source.NewMemorySource() + + collectionSource = memSource + if vo.ArchivistaOptions.Enable { + archivistaClient = archivista.New(vo.ArchivistaOptions.Url) + collectionSource = source.NewMultiSource(collectionSource, source.NewArchvistSource(archivistaClient)) + } + + if vo.KeyPath == "" && len(vo.CAPaths) == 0 && len(verifiers) == 0 { + return fmt.Errorf("must supply either a public key, CA certificates or a verifier") + } + if vo.KeyPath != "" { keyFile, err := os.Open(vo.KeyPath) if err != nil { @@ -79,18 +95,11 @@ func runVerify(ctx context.Context, vo options.VerifyOptions, verifiers ...crypt verifiers = append(verifiers, v) } - inFile, err := os.Open(vo.PolicyFilePath) + policyEnvelope, err := policy.LoadPolicy(ctx, vo.PolicyFilePath, archivista_client.NewArchivistaClient(vo.ArchivistaOptions.Url, archivistaClient)) if err != nil { return fmt.Errorf("failed to open policy file: %w", err) } - defer inFile.Close() - policyEnvelope := dsse.Envelope{} - decoder := json.NewDecoder(inFile) - if err := decoder.Decode(&policyEnvelope); err != nil { - return fmt.Errorf("could not unmarshal policy envelope: %w", err) - } - subjects := []cryptoutil.DigestSet{} if len(vo.ArtifactFilePath) > 0 { artifactDigestSet, err := cryptoutil.CalculateDigestSetFromFile(vo.ArtifactFilePath, []cryptoutil.DigestValue{{Hash: crypto.SHA256, GitOID: false}}) @@ -109,19 +118,12 @@ func runVerify(ctx context.Context, vo options.VerifyOptions, verifiers ...crypt return errors.New("at least one subject is required, provide an artifact file or subject") } - var collectionSource source.Sourcer - memSource := source.NewMemorySource() for _, path := range vo.AttestationFilePaths { if err := memSource.LoadFile(path); err != nil { return fmt.Errorf("failed to load attestation file: %w", err) } } - collectionSource = memSource - if vo.ArchivistaOptions.Enable { - collectionSource = source.NewMultiSource(collectionSource, source.NewArchvistSource(archivista.New(vo.ArchivistaOptions.Url))) - } - verifiedEvidence, err := witness.Verify( ctx, policyEnvelope, diff --git a/go.mod b/go.mod index 605b12c0..2bdaa576 100644 --- a/go.mod +++ b/go.mod @@ -120,6 +120,7 @@ require ( github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/afero v1.11.0 // indirect github.com/spf13/cast v1.6.0 // indirect + github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect github.com/titanous/rocacheck v0.0.0-20171023193734-afe73141d399 // indirect diff --git a/go.sum b/go.sum index cc2a1745..ae0c0194 100644 --- a/go.sum +++ b/go.sum @@ -331,6 +331,8 @@ github.com/spiffe/go-spiffe/v2 v2.1.7/go.mod h1:QJDGdhXllxjxvd5B+2XnhhXB/+rC8gr+ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= diff --git a/internal/archivista/archivista.go b/internal/archivista/archivista.go new file mode 100644 index 00000000..840e3f8f --- /dev/null +++ b/internal/archivista/archivista.go @@ -0,0 +1,58 @@ +// Copyright 2024 The Witness Contributors +// +// 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 archivista + +import ( + "context" + + "github.com/in-toto/go-witness/archivista" + "github.com/in-toto/go-witness/dsse" +) + +type aClient struct { + url string + client Clienter +} + +// Define Client Interface for Archivista +type Clienter interface { + Download(ctx context.Context, gitoid string) (dsse.Envelope, error) + Store(ctx context.Context, env dsse.Envelope) (string, error) + SearchGitoids(ctx context.Context, vars archivista.SearchGitoidVariables) ([]string, error) +} + +func NewArchivistaClient(url string, client *archivista.Client) Clienter { + + if client == nil { + return nil + } + + return &aClient{ + url: url, + client: client, + } +} + +func (ac *aClient) Download(ctx context.Context, gitoid string) (dsse.Envelope, error) { + return ac.client.Download(ctx, gitoid) +} + +func (ac *aClient) Store(ctx context.Context, env dsse.Envelope) (string, error) { + return ac.client.Store(ctx, env) +} + +func (ac *aClient) SearchGitoids(ctx context.Context, vars archivista.SearchGitoidVariables) ([]string, error) { + return ac.client.SearchGitoids(ctx, vars) +} diff --git a/internal/policy/policy.go b/internal/policy/policy.go new file mode 100644 index 00000000..fc260d7f --- /dev/null +++ b/internal/policy/policy.go @@ -0,0 +1,57 @@ +// Copyright 2024 The Witness Contributors +// +// 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 policy + +import ( + "context" + "encoding/json" + "fmt" + "os" + + "github.com/in-toto/go-witness/dsse" + "github.com/in-toto/go-witness/log" + "github.com/in-toto/witness/internal/archivista" +) + +// Load policy from a file or Archivista +// +// It prefers to load from a file, if it fails, it tries to load from Archivista +func LoadPolicy(ctx context.Context, policy string, ac archivista.Clienter) (dsse.Envelope, error) { + policyEnvelope := dsse.Envelope{} + + filePolicy, err := os.Open(policy) + if err != nil { + log.Debug("failed to open policy file: ", policy) + if ac == nil { + return policyEnvelope, fmt.Errorf("failed to open file to sign: %w", err) + } else { + log.Debug("attempting to fetch policy " + policy + " from archivista") + policyEnvelope, err = ac.Download(ctx, policy) + if err != nil { + return policyEnvelope, fmt.Errorf("failed to fetch policy from archivista: %w", err) + } + log.Debug("policy " + policy + " downloaded from archivista") + } + + } else { + defer filePolicy.Close() + decoder := json.NewDecoder(filePolicy) + if err := decoder.Decode(&policyEnvelope); err != nil { + return policyEnvelope, fmt.Errorf("could not unmarshal policy envelope: %w", err) + } + } + + return policyEnvelope, nil +} diff --git a/internal/policy/policy_test.go b/internal/policy/policy_test.go new file mode 100644 index 00000000..da0c09da --- /dev/null +++ b/internal/policy/policy_test.go @@ -0,0 +1,103 @@ +// Copyright 2024 The Witness Contributors +// +// 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 policy + +import ( + "context" + "errors" + "testing" + + "github.com/in-toto/go-witness/dsse" + "github.com/in-toto/witness/internal/archivista" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/suite" +) + +// Mock archivista client +type ArchivistaClienterMock struct { + mock.Mock + archivista.Clienter +} + +func (m *ArchivistaClienterMock) Download(ctx context.Context, path string) (dsse.Envelope, error) { + args := m.Called() + return dsse.Envelope{}, args.Error(1) +} + +// Define test suite +type UTPolicySuite struct { + suite.Suite + mockedAC *ArchivistaClienterMock +} + +func TestUTPolicySuite(t *testing.T) { + suite.Run(t, new(UTPolicySuite)) +} + +// Setup test suite +func (ut *UTPolicySuite) SetupTest() { + ut.mockedAC = &ArchivistaClienterMock{} + +} + +// Test LoadPolicy with file +func (ut *UTPolicySuite) TestLoadPolicyFile() { + ctx := context.Background() + policy := "../../test/policy-hello-signed.json" + + // Load policy from file + policyEnvelope, err := LoadPolicy(ctx, policy, nil) + ut.NoError(err) + ut.NotNil(policyEnvelope) +} + +// Test LoadPolicy with file not found +func (ut *UTPolicySuite) TestLoadPolicyFileNotFound() { + ctx := context.Background() + policy := "notfound" + + // Load policy from file + _, err := LoadPolicy(ctx, policy, nil) + ut.Error(err) + ut.Contains(err.Error(), "no such file or directory") +} + +// Test LoadPolicy with archivista +func (ut *UTPolicySuite) TestLoadPolicyArchivista() { + ctx := context.Background() + policy := "testgitoid" + + // Mock archivista client + ut.mockedAC.On("Download").Return(dsse.Envelope{}, nil) + + // Load policy from archivista + policyEnvelope, err := LoadPolicy(ctx, policy, ut.mockedAC) + ut.NoError(err) + ut.NotNil(policyEnvelope) +} + +// Test LoadPolicy with archivista not found +func (ut *UTPolicySuite) TestLoadPolicyArchivistaNotFound() { + ctx := context.Background() + policy := "testgitoid" + + // Mock archivista client + ut.mockedAC.On("Download").Return(dsse.Envelope{}, errors.New("not found")) + + // Load policy from archivista + _, err := LoadPolicy(ctx, policy, ut.mockedAC) + ut.Error(err) + ut.Contains(err.Error(), "failed to fetch policy from archivista") +} diff --git a/test/policy-hello-signed.json b/test/policy-hello-signed.json new file mode 100644 index 00000000..445b9c29 --- /dev/null +++ b/test/policy-hello-signed.json @@ -0,0 +1,10 @@ +{ + "payload": "ewogICAgImV4cGlyZXMiOiAiMjAzNS0xMi0xN1QyMzo1Nzo0MC0wNTowMCIsCiAgICAibmFtZSI6ICJidWlsZC1oZWxsbyIsCiAgICAic3RlcHMiOiB7CiAgICAgICJyZWxlYXNlIjogewogICAgICAgICJuYW1lIjogInJlbGVhc2UiLAogICAgICAgICJhdHRlc3RhdGlvbnMiOiBbCiAgICAgICAgICB7CiAgICAgICAgICAgICJ0eXBlIjogImh0dHBzOi8vd2l0bmVzcy5kZXYvYXR0ZXN0YXRpb25zL2NvbW1hbmQtcnVuL3YwLjEiLAogICAgICAgICAgICAicmVnb3BvbGljaWVzIjogWwogICAgICAgICAgICAgIHsKICAgICAgICAgICAgICAgICJuYW1lIjogImV4cGVjdGVkIGV4aXQgY29kZSIsCiAgICAgICAgICAgICAgICAibW9kdWxlIjogImNHRmphMkZuWlNCamIyMXRZVzVrY25WdUNncGtaVzU1VzIxeloxMGdld29nSUNBZ2FXNXdkWFF1WlhocGRHTnZaR1VnSVQwZ01Bb2dJQ0FnYlhObklEbzlJQ0psZUdsMFkyOWtaU0J1YjNRZ01DSUtmUW89IgogICAgICAgICAgICAgIH0KICAgICAgICAgICAgXQogICAgICAgICAgfSwKICAgICAgICAgIHsKICAgICAgICAgICAgInR5cGUiOiAiaHR0cHM6Ly93aXRuZXNzLmRldi9hdHRlc3RhdGlvbnMvcHJvZHVjdC92MC4xIiwKICAgICAgICAgICAgInN1YmplY3RzY29wZXMiOiBbCiAgICAgICAgICAgICAgewogICAgICAgICAgICAgICAgInN1YmplY3QiOiAiZmlsZSIsCiAgICAgICAgICAgICAgICAic2NvcGUiOiAidGVzdC9oZWxsby50eHQiCiAgICAgICAgICAgICAgfQogICAgICAgICAgICBdCiAgICAgICAgICB9LAogICAgICAgICAgewogICAgICAgICAgICAidHlwZSI6ICJodHRwczovL3dpdG5lc3MuZGV2L2F0dGVzdGF0aW9ucy9lbnZpcm9ubWVudC92MC4xIgogICAgICAgICAgfQogICAgICAgIF0sCiAgICAgICAgImZ1bmN0aW9uYXJpZXMiOiBbCiAgICAgICAgICB7CiAgICAgICAgICAgICJ0eXBlIjogInB1YmxpY2tleSIsCiAgICAgICAgICAgICJwdWJsaWNrZXlpZCI6ICJhd3NrbXM6Ly8vYXJuOmF3czprbXM6dXMtZWFzdC0xOjAwMDAwMDAwMDAwMDphbGlhcy9hd3Mta21zLWtleWlkLXRlc3QiCiAgICAgICAgICB9CiAgICAgICAgXQogICAgICB9CiAgICB9LAogICAgInB1YmxpY2tleXMiOiB7CiAgICAgICJhd3NrbXM6Ly8vYXJuOmF3czprbXM6dXMtZWFzdC0xOjAwMDAwMDAwMDAwMDphbGlhcy9hd3Mta21zLWtleWlkLXRlc3QiOiB7CiAgICAgICAgImtleWlkIjogImF3c2ttczovLy9hcm46YXdzOmttczp1cy1lYXN0LTE6MDAwMDAwMDAwMDAwOmFsaWFzL2F3cy1rbXMta2V5aWQtdGVzdCIKICAgICAgfQogICAgfQogIH0=", + "payloadType": "https://witness.testifysec.com/policy/v0.1", + "signatures": [ + { + "keyid": "awskms:///arn:aws:kms:us-east-1:000000000000:alias/aws-kms-keyid-test", + "sig": "MEUCICAdESkQlElhtUClriWKpLGIS6EOq+9EZfSpnmJGV3WcAiEAks2r7+9AyCYnzHUA6Ix3LOtE6pmEBe8eS/RkplQ8c3Y=" + } + ] +} \ No newline at end of file