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