diff --git a/docs/config.md b/docs/config.md index a3eeafa809..b985399182 100644 --- a/docs/config.md +++ b/docs/config.md @@ -20,7 +20,7 @@ Supported keys include: | Key | Description | Supported Values | Default | | :-------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------------------------------------- | :-------- | | `artifacts.taskrun.format` | The format to store `TaskRun` payloads in. | `in-toto`, `slsa/v1`, `slsa/v2alpha3`, `slsa/v2alpha4` | `in-toto` | -| `artifacts.taskrun.storage` | The storage backend to store `TaskRun` signatures in. Multiple backends can be specified with comma-separated list ("tekton,oci"). To disable the `TaskRun` artifact input an empty string (""). | `tekton`, `oci`, `gcs`, `docdb`, `grafeas` | `tekton` | +| `artifacts.taskrun.storage` | The storage backend to store `TaskRun` signatures in. Multiple backends can be specified with comma-separated list ("tekton,oci"). To disable the `TaskRun` artifact input an empty string (""). | `tekton`, `oci`, `gcs`, `docdb`, `grafeas`, `archivista` | `tekton` | | `artifacts.taskrun.signer` | The signature backend to sign `TaskRun` payloads with. | `x509`, `kms` | `x509` | > NOTE: @@ -34,7 +34,7 @@ Supported keys include: | Key | Description | Supported Values | Default | | :--------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :----------------------------------------- | :-------- | | `artifacts.pipelinerun.format` | The format to store `PipelineRun` payloads in. | `in-toto`, `slsa/v1`, `slsa/v2alpha3`, `slsa/v2alpha4` | `in-toto` | -| `artifacts.pipelinerun.storage` | The storage backend to store `PipelineRun` signatures in. Multiple backends can be specified with comma-separated list ("tekton,oci"). To disable the `PipelineRun` artifact input an empty string (""). | `tekton`, `oci`, `gcs`, `docdb`, `grafeas` | `tekton` | +| `artifacts.pipelinerun.storage` | The storage backend to store `PipelineRun` signatures in. Multiple backends can be specified with comma-separated list ("tekton,oci"). To disable the `PipelineRun` artifact input an empty string (""). | `tekton`, `oci`, `gcs`, `docdb`, `grafeas`, `archivista` | `tekton` | | `artifacts.pipelinerun.signer` | The signature backend to sign `PipelineRun` payloads with. | `x509`, `kms` | `x509` | | `artifacts.pipelinerun.enable-deep-inspection` | This boolean option will configure whether Chains should inspect child taskruns in order to capture inputs/outputs within a pipelinerun. `"false"` means that Chains only checks pipeline level results, whereas `"true"` means Chains inspects both pipeline level and task level results. | `"true"`, `"false"` | `"false"` | @@ -73,6 +73,7 @@ Supported keys include: | `storage.grafeas.projectid` | The project of where grafeas server is located for storing occurrences | | | | `storage.grafeas.noteid` (optional) | This field will be used as the prefix part of the note name that will be created. The value of this field must be a string without spaces. (See more details [below](#grafeas).) | | | | `storage.grafeas.notehint` (optional) | This field is used to set the [human_readable_name](https://github.com/grafeas/grafeas/blob/cd23d4dc1bef740d6d6d90d5007db5c9a2431c41/proto/v1/attestation.proto#L49) field in the Grafeas ATTESTATION note. If it is not provided, the default `This attestation note was generated by Tekton Chains` will be used. | | | +| `storage.archivista.url` | The URL endpoint for the Archivista service. | A valid HTTPS URL pointing to your Archivista instance (e.g. `https://archivista.testifysec.io`). | None | #### docstore diff --git a/go.mod b/go.mod index b363e6999a..1fcd16ff95 100644 --- a/go.mod +++ b/go.mod @@ -16,7 +16,9 @@ require ( github.com/google/go-licenses v1.6.0 github.com/grafeas/grafeas v0.2.3 github.com/hashicorp/go-multierror v1.1.1 + github.com/in-toto/archivista v0.9.0 github.com/in-toto/attestation v1.1.1 + github.com/in-toto/go-witness v0.7.0 github.com/in-toto/in-toto-golang v0.9.1-0.20240317085821-8e2966059a09 github.com/opencontainers/go-digest v1.0.0 github.com/pkg/errors v0.9.1 @@ -183,6 +185,7 @@ require ( github.com/eapache/go-resiliency v1.7.0 // indirect github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 // indirect github.com/eapache/queue v1.1.0 // indirect + github.com/edwarnicke/gitoid v0.0.0-20220710194850-1be5bfda1f9d // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect github.com/emirpasic/gods v1.18.1 // indirect github.com/envoyproxy/go-control-plane v0.13.1 // indirect diff --git a/go.sum b/go.sum index bed5f65c23..0ec78d45fb 100644 --- a/go.sum +++ b/go.sum @@ -463,6 +463,8 @@ github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3 h1:Oy0F4A github.com/eapache/go-xerial-snappy v0.0.0-20230731223053-c322873962e3/go.mod h1:YvSRo5mw33fLEx1+DlK6L2VV43tJt5Eyel9n9XBcR+0= github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edwarnicke/gitoid v0.0.0-20220710194850-1be5bfda1f9d h1:4l+Uq5zFWSagXgGFaKRRVWJrnlzeathyagWgYUltCgY= +github.com/edwarnicke/gitoid v0.0.0-20220710194850-1be5bfda1f9d/go.mod h1:WxWwA3EYuCQjlR5EBUX3uaTS8bh9BOa7BcqVREHQ0uQ= github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/emicklei/proto v1.13.4 h1:myn1fyf8t7tAqIzV91Tj9qXpvyXXGXk8OS2H6IBSc9g= @@ -863,8 +865,12 @@ github.com/howeyc/gopass v0.0.0-20210920133722-c8aef6fb66ef/go.mod h1:lADxMC39cJ github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/in-toto/archivista v0.9.0 h1:XlS+jkrcFjmwSMhp6BZbP5y8FOvFPXM1h23WvCDT8bQ= +github.com/in-toto/archivista v0.9.0/go.mod h1:cLhrICj86j+8wJZmrUzDbNQdcwdc2lqX+v1SKV4tXpE= github.com/in-toto/attestation v1.1.1 h1:QD3d+oATQ0dFsWoNh5oT0udQ3tUrOsZZ0Fc3tSgWbzI= github.com/in-toto/attestation v1.1.1/go.mod h1:Dcq1zVwA2V7Qin8I7rgOi+i837wEf/mOZwRm047Sjys= +github.com/in-toto/go-witness v0.7.0 h1:I48FUCLfyos0uCSlHJoqCJO6HjtxF2f/y65TQVpxd8k= +github.com/in-toto/go-witness v0.7.0/go.mod h1:WZQY96yHqPPYkRcQU7dXl0d3saMKAg9DepWbUVL586E= github.com/in-toto/in-toto-golang v0.9.1-0.20240317085821-8e2966059a09 h1:cwCITdi9pF50CF8uh40qDbkJ/VrEVzx5AoaHP7OPdEo= github.com/in-toto/in-toto-golang v0.9.1-0.20240317085821-8e2966059a09/go.mod h1:yGCBn2JKF1m26FX8GmkcLSOFVjB6khWRxFsHwWIg7hw= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= diff --git a/pkg/chains/storage/archivista/archivista.go b/pkg/chains/storage/archivista/archivista.go new file mode 100644 index 0000000000..c94e7be571 --- /dev/null +++ b/pkg/chains/storage/archivista/archivista.go @@ -0,0 +1,101 @@ +/* +Copyright 2025 The Tekton Authors +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" + "encoding/json" + "errors" + "fmt" + "net/http" + "strings" + + archivistaClient "github.com/in-toto/archivista/pkg/http-client" + "github.com/in-toto/go-witness/dsse" + "github.com/tektoncd/chains/pkg/chains/objects" + "github.com/tektoncd/chains/pkg/config" + "knative.dev/pkg/logging" +) + +const ( + // StorageBackendArchivista is the name of the Archivista storage backend + StorageBackendArchivista = "archivista" +) + +// Backend is a storage backend that is capable of storing Payloaders that are signed and wrapped +// with a DSSE envelope. Archivista is an in-toto attestation storage service. +type Backend struct { + client *archivistaClient.ArchivistaClient + url string + cfg config.ArchivistaStorageConfig +} + +// NewStorageBackend returns a new Archivista StorageBackend that can store Payloaders that are signed +// and wrapped in a DSSE envelope +func NewStorageBackend(cfg config.Config) (*Backend, error) { + archCfg := cfg.Storage.Archivista + if strings.TrimSpace(archCfg.URL) == "" { + return nil, fmt.Errorf("missing archivista URL in storage configuration") + } + + client, err := archivistaClient.CreateArchivistaClient(&http.Client{}, archCfg.URL) + if err != nil { + return nil, fmt.Errorf("failed to create Archivista client: %w", err) + } + + return &Backend{ + client: client, + url: archCfg.URL, + cfg: archCfg, + }, nil +} + +// StorePayload attempts to parse `signature` as a DSSE envelope, and if successful +// sends it to an Archivista server for storage. +func (b *Backend) StorePayload(ctx context.Context, _ objects.TektonObject, _ []byte, signature string, _ config.StorageOpts) error { + logger := logging.FromContext(ctx) + var env dsse.Envelope + if err := json.Unmarshal([]byte(signature), &env); err != nil { + logger.Errorf("Failed to parse DSSE envelope: %w", err) + return errors.Join(errors.New("Failed to parse DSSE envelope"), err) + } + + uploadResp, err := b.client.Store(ctx, env) + if err != nil { + logger.Errorw("Failed to upload DSSE envelope to Archivista", "error", err) + return err + } + logger.Infof("Successfully uploaded DSSE envelope to Archivista, response: %+v", uploadResp) + return nil +} + +// RetrievePayload is not implemented for Archivista. +func (b *Backend) RetrievePayload(_ context.Context, _ string) ([]byte, []byte, error) { + return nil, nil, fmt.Errorf("RetrievePayload not implemented for Archivista") +} + +// RetrievePayloads is not implemented for Archivista. +func (b *Backend) RetrievePayloads(_ context.Context, _ objects.TektonObject, _ config.StorageOpts) (map[string]string, error) { + return nil, fmt.Errorf("RetrievePayloads not implemented for Archivista") +} + +// RetrieveSignatures is not implemented for Archivista. +func (b *Backend) RetrieveSignatures(_ context.Context, _ objects.TektonObject, _ config.StorageOpts) (map[string][]string, error) { + return nil, fmt.Errorf("RetrieveSignatures not implemented for Archivista") +} + +// Type returns the name of the storage backend +func (b *Backend) Type() string { + return StorageBackendArchivista +} diff --git a/pkg/chains/storage/archivista/archivista_test.go b/pkg/chains/storage/archivista/archivista_test.go new file mode 100644 index 0000000000..a3120b990a --- /dev/null +++ b/pkg/chains/storage/archivista/archivista_test.go @@ -0,0 +1,142 @@ +/* +Copyright 2025 The Tekton Authors +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" + "encoding/base64" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + archivistaClient "github.com/in-toto/archivista/pkg/http-client" + "github.com/stretchr/testify/assert" + "github.com/tektoncd/chains/pkg/chains/objects" + "github.com/tektoncd/chains/pkg/config" + v1 "github.com/tektoncd/pipeline/pkg/apis/pipeline/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const testProvenanceDSSE = `{"payload":"eyJfdHlwZSI6Imh0dHBzOi8vaW4tdG90by5pby9TdGF0ZW1lbnQvdjAuMSIsInByZWRpY2F0ZVR5cGUiOiJodHRwczovL3Nsc2EuZGV2L3Byb3ZlbmFuY2UvdjAuMiIsInByZWRpY2F0ZSI6eyJidWlsZENvbmZpZyI6eyJ0YXNrcyI6W3siZmluaXNoZWRPbiI6IjIwMjUtMDItMjdUMTc6MTY6MzFaIiwiaW52b2NhdGlvbiI6eyJjb25maWdTb3VyY2UiOnt9LCJlbnZpcm9ubWVudCI6eyJhbm5vdGF0aW9ucyI6eyJwaXBlbGluZS50ZWt0b24uZGV2L3JlbGVhc2UiOiJjNmQzOGM5In0sImxhYmVscyI6eyJhcHAua3ViZXJuZXRlcy5pby9tYW5hZ2VkLWJ5IjoidGVrdG9uLXBpcGVsaW5lcyIsInRla3Rvbi5kZXYvbWVtYmVyT2YiOiJ0YXNrcyIsInRla3Rvbi5kZXYvcGlwZWxpbmUiOiJoZWxsby13b3JsZC1waXBlbGluZSIsInRla3Rvbi5kZXYvcGlwZWxpbmVSdW4iOiJoZWxsby13b3JsZC1waXBlbGluZS1ydW4tMTc0MDY3NjU3OSIsInRla3Rvbi5kZXYvcGlwZWxpbmVSdW5VSUQiOiIyMDhlMjdmOS0zOWM4LTQxOWEtOGY2MC1kY2UzMmMxNDlhODgiLCJ0ZWt0b24uZGV2L3BpcGVsaW5lVGFzayI6InNheS1oZWxsbyIsInRla3Rvbi5kZXYvdGFzayI6ImhlbGxvLXdvcmxkLXRhc2sifX0sInBhcmFtZXRlcnMiOnt9fSwibmFtZSI6InNheS1oZWxsbyIsInJlZiI6eyJraW5kIjoiVGFzayIsIm5hbWUiOiJoZWxsby13b3JsZC10YXNrIn0sInJlc3VsdHMiOlt7Im5hbWUiOiJQSVBFTElORV9SVU5fQVJUSUZBQ1RfRElHRVNUIiwidHlwZSI6InN0cmluZyIsInZhbHVlIjoic2hhMjU2OjQ3OTJjZTEyMTBmZWRmNWY1MWZjMTRiZDFiZjAyOGFmNzFkMmU0NWE3ZTc0YzRhYmVjZGIzYzc3NGZlNjNmNmYifSx7Im5hbWUiOiJQSVBFTElORV9SVU5fQVJUSUZBQ1RfVVJJIiwidHlwZSI6InN0cmluZyIsInZhbHVlIjoiJChjb250ZXh0LnBpcGVsaW5lUnVuLm5hbWUpIn1dLCJzZXJ2aWNlQWNjb3VudE5hbWUiOiJkZWZhdWx0Iiwic3RhcnRlZE9uIjoiMjAyNS0wMi0yN1QxNzoxNjoxOVoiLCJzdGF0dXMiOiJTdWNjZWVkZWQiLCJzdGVwcyI6W3siYW5ub3RhdGlvbnMiOm51bGwsImFyZ3VtZW50cyI6bnVsbCwiZW50cnlQb2ludCI6IiMhL2Jpbi9iYXNoXG5lY2hvIFwiSGVsbG8gV29ybGRcIlxuZWNobyBcIlBpcGVsaW5lUnVuIG5hbWUgZnJvbSBlbnY6ICRQSVBFTElORV9SVU5fTkFNRVwiXG4jIFdyaXRlIHRoZSBQaXBlbGluZVJ1biBJRCBhcyB0aGUgYXJ0aWZhY3QgVVJJIHJlc3VsdFxuZWNobyAtbiBcIiRQSVBFTElORV9SVU5fTkFNRVwiID4gL3Rla3Rvbi9yZXN1bHRzL1BJUEVMSU5FX1JVTl9BUlRJRkFDVF9VUklcbiMgQ29tcHV0ZSB0aGUgU0hBMjU2IGRpZ2VzdCBvZiB0aGUgUGlwZWxpbmVSdW4gSURcbmRpZ2VzdD0kKGVjaG8gLW4gXCIkUElQRUxJTkVfUlVOX05BTUVcIiB8IHNoYTI1NnN1bSB8IGF3ayAne3ByaW50ICQxfScpXG4jIFdyaXRlIHRoZSBkaWdlc3QgKHByZWZpeGVkIHdpdGggXCJzaGEyNTY6XCIpIGFzIHRoZSBhcnRpZmFjdCBkaWdlc3QgcmVzdWx0XG5lY2hvIC1uIFwic2hhMjU2OiRkaWdlc3RcIiA+IC90ZWt0b24vcmVzdWx0cy9QSVBFTElORV9SVU5fQVJUSUZBQ1RfRElHRVNUXG4iLCJlbnZpcm9ubWVudCI6eyJjb250YWluZXIiOiJwcmludC1oZWxsbyIsImltYWdlIjoib2NpOi8vdWJ1bnR1QHNoYTI1Njo4ZTVjNGYwMjg1ZWNiYjRlYWQwNzA0MzFkMjliNTc2YTUzMGQzMTY2ZGY3M2VjNDRhZmZjMWNkMjc1NTUxNDFiIn19XX1dfSwiYnVpbGRUeXBlIjoidGVrdG9uLmRldi92MWJldGExL1BpcGVsaW5lUnVuIiwiYnVpbGRlciI6eyJpZCI6Imh0dHBzOi8vdGVrdG9uLmRldi9jaGFpbnMvdjIifSwiaW52b2NhdGlvbiI6eyJjb25maWdTb3VyY2UiOnt9LCJlbnZpcm9ubWVudCI6eyJsYWJlbHMiOnsidGVrdG9uLmRldi9waXBlbGluZSI6ImhlbGxvLXdvcmxkLXBpcGVsaW5lIn19LCJwYXJhbWV0ZXJzIjp7fX0sIm1hdGVyaWFscyI6W3siZGlnZXN0Ijp7InNoYTI1NiI6IjhlNWM0ZjAyODVlY2JiNGVhZDA3MDQzMWQyOWI1NzZhNTMwZDMxNjZkZjczZWM0NGFmZmMxY2QyNzU1NTE0MWIifSwidXJpIjoib2NpOi8vdWJ1bnR1In1dLCJtZXRhZGF0YSI6eyJidWlsZEZpbmlzaGVkT24iOiIyMDI1LTAyLTI3VDE3OjE2OjMxWiIsImJ1aWxkU3RhcnRlZE9uIjoiMjAyNS0wMi0yN1QxNzoxNjoxOVoiLCJjb21wbGV0ZW5lc3MiOnsiZW52aXJvbm1lbnQiOmZhbHNlLCJtYXRlcmlhbHMiOmZhbHNlLCJwYXJhbWV0ZXJzIjpmYWxzZX0sInJlcHJvZHVjaWJsZSI6ZmFsc2V9fX0=","payloadType":"application/vnd.in-toto+json","signatures":[{"keyid":"SHA256:ZnwkOhDkkbPcS5pY0SqpimYAJl2pqgHrxW9ECLcZvJ8","sig":"MEQCIHfE2iwOj13IJLoMXCQ2VvdkOvccX2BnaGZSr/m6+WPCAiAyK1HpCiHNBHJvyPJDl7cQIHtNkJQxLBGUDUsnycpfzQ=="}]}` + +// -------------------------- +// Helper: setupEnv +// -------------------------- + +// setupEnv creates a fresh ArchivistaStorage test environment using a given TaskRun name. +func setupEnv(taskRunName string, cfg config.Config, archClient *archivistaClient.ArchivistaClient) (*Backend, *objects.TaskRunObjectV1) { + tr := &v1.TaskRun{ + ObjectMeta: metav1.ObjectMeta{ + Name: taskRunName, + Namespace: "default", + }, + } + obj := objects.NewTaskRunObjectV1(tr) + aStorage, err := NewStorageBackend(cfg) + if err != nil { + panic("failed to initialize ArchivistaStorage: " + err.Error()) + } + // Override the Archivista client with the provided one. + aStorage.client = archClient + return aStorage, obj +} + +// -------------------------- +// StorePayload Tests +// -------------------------- + +// TestStorePayload_TaskRun tests the basic success path of StorePayload without certificate data. +func TestStorePayload_TaskRun(t *testing.T) { + ctx := context.Background() + + // Set up an httptest server to simulate Archivista. + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/upload" { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"gitoid": "fake-gitoid"}`)) + return + } + http.NotFound(w, r) + })) + defer ts.Close() + + httpClient := &http.Client{} + cfg := config.Config{ + Storage: config.StorageConfigs{ + Archivista: config.ArchivistaStorageConfig{ + URL: ts.URL, + }, + }, + } + archClient, err := archivistaClient.CreateArchivistaClient(httpClient, cfg.Storage.Archivista.URL) + if err != nil { + t.Fatalf("failed to create Archivista client: %v", err) + } + + aStorage, obj := setupEnv("test-taskrun", cfg, archClient) + + // Prepare a valid payload. + type mockPayload struct { + A string `json:"a"` + B int `json:"b"` + } + payload := mockPayload{ + A: "foo", + B: 3, + } + payloadBytes, err := json.Marshal(payload) + assert.NoError(t, err, "should marshal payload") + encodedPayload := base64.StdEncoding.EncodeToString(payloadBytes) + opts := config.StorageOpts{ + ShortKey: "mockpayload", + Cert: "", + Chain: "", + } + + // Call StorePayload. + err = aStorage.StorePayload(ctx, obj, []byte(encodedPayload), testProvenanceDSSE, opts) + assert.NoError(t, err, "StorePayload should succeed") +} + +// TestStorePayload_ErrorCases exercises error branches in StorePayload. +func TestStorePayload_ErrorCases(t *testing.T) { + ctx := context.Background() + + // Setup a common httptest server and configuration. + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"gitoid": "fake-gitoid"}`)) + })) + defer ts.Close() + + httpClient := &http.Client{} + cfg := config.Config{ + Storage: config.StorageConfigs{ + Archivista: config.ArchivistaStorageConfig{ + URL: ts.URL, + }, + }, + } + archClient, err := archivistaClient.CreateArchivistaClient(httpClient, cfg.Storage.Archivista.URL) + if err != nil { + t.Fatalf("failed to create Archivista client: %v", err) + } + + t.Run("non-dsse signature", func(t *testing.T) { + aStorage, obj := setupEnv("non-dsse signature", cfg, archClient) + err := aStorage.StorePayload(ctx, obj, []byte("dummy"), "abcde12354", config.StorageOpts{}) + assert.ErrorContains(t, err, "Failed to parse DSSE") + }) +} diff --git a/pkg/chains/storage/storage.go b/pkg/chains/storage/storage.go index dbb07a37b2..573609706c 100644 --- a/pkg/chains/storage/storage.go +++ b/pkg/chains/storage/storage.go @@ -18,6 +18,7 @@ import ( "errors" "github.com/tektoncd/chains/pkg/chains/objects" + "github.com/tektoncd/chains/pkg/chains/storage/archivista" "github.com/tektoncd/chains/pkg/chains/storage/docdb" "github.com/tektoncd/chains/pkg/chains/storage/gcs" "github.com/tektoncd/chains/pkg/chains/storage/grafeas" @@ -93,8 +94,13 @@ func InitializeBackends(ctx context.Context, ps versioned.Interface, kc kubernet return nil, err } backends[backendType] = pubsubBackend + case archivista.StorageBackendArchivista: + archivistaBackend, err := archivista.NewStorageBackend(cfg) + if err != nil { + return nil, err + } + backends[backendType] = archivistaBackend } - } logger.Infof("successfully initialized backends: %v", maps.Keys(backends)) diff --git a/pkg/config/config.go b/pkg/config/config.go index 4e9cef101a..4b825f2d74 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -53,12 +53,13 @@ type Artifact struct { // StorageConfigs contains the configuration to instantiate different storage providers type StorageConfigs struct { - GCS GCSStorageConfig - OCI OCIStorageConfig - Tekton TektonStorageConfig - DocDB DocDBStorageConfig - Grafeas GrafeasConfig - PubSub PubSubStorageConfig + GCS GCSStorageConfig + OCI OCIStorageConfig + Tekton TektonStorageConfig + DocDB DocDBStorageConfig + Grafeas GrafeasConfig + PubSub PubSubStorageConfig + Archivista ArchivistaStorageConfig } // SignerConfigs contains the configuration to instantiate different signers @@ -155,6 +156,12 @@ type TransparencyConfig struct { URL string } +// ArchivistaStorageConfig holds configuration for the Archivista storage backend. +type ArchivistaStorageConfig struct { + // URL is the endpoint for the Archivista service. + URL string `json:"url"` +} + const ( taskrunFormatKey = "artifacts.taskrun.format" taskrunStorageKey = "artifacts.taskrun.storage" @@ -177,6 +184,8 @@ const ( docDBMongoServerURLDirKey = "storage.docdb.mongo-server-url-dir" docDBMongoServerURLPathKey = "storage.docdb.mongo-server-url-path" + archivistaURLKey = "storage.archivista.url" + grafeasProjectIDKey = "storage.grafeas.projectid" grafeasNoteIDKey = "storage.grafeas.noteid" grafeasNoteHint = "storage.grafeas.notehint" @@ -276,18 +285,18 @@ func NewConfigFromMap(data map[string]string) (*Config, error) { // Artifact-specific configs // TaskRuns asString(taskrunFormatKey, &cfg.Artifacts.TaskRuns.Format, "in-toto", "slsa/v1", "slsa/v2alpha3", "slsa/v2alpha4"), - asStringSet(taskrunStorageKey, &cfg.Artifacts.TaskRuns.StorageBackend, sets.New[string]("tekton", "oci", "gcs", "docdb", "grafeas", "kafka")), + asStringSet(taskrunStorageKey, &cfg.Artifacts.TaskRuns.StorageBackend, sets.New[string]("tekton", "oci", "gcs", "docdb", "grafeas", "kafka", "archivista")), asString(taskrunSignerKey, &cfg.Artifacts.TaskRuns.Signer, "x509", "kms"), // PipelineRuns asString(pipelinerunFormatKey, &cfg.Artifacts.PipelineRuns.Format, "in-toto", "slsa/v1", "slsa/v2alpha3", "slsa/v2alpha4"), - asStringSet(pipelinerunStorageKey, &cfg.Artifacts.PipelineRuns.StorageBackend, sets.New[string]("tekton", "oci", "gcs", "docdb", "grafeas")), + asStringSet(pipelinerunStorageKey, &cfg.Artifacts.PipelineRuns.StorageBackend, sets.New[string]("tekton", "oci", "gcs", "docdb", "grafeas", "archivista")), asString(pipelinerunSignerKey, &cfg.Artifacts.PipelineRuns.Signer, "x509", "kms"), asBool(pipelinerunEnableDeepInspectionKey, &cfg.Artifacts.PipelineRuns.DeepInspectionEnabled), // OCI asString(ociFormatKey, &cfg.Artifacts.OCI.Format, "simplesigning"), - asStringSet(ociStorageKey, &cfg.Artifacts.OCI.StorageBackend, sets.New[string]("tekton", "oci", "gcs", "docdb", "grafeas", "kafka")), + asStringSet(ociStorageKey, &cfg.Artifacts.OCI.StorageBackend, sets.New[string]("tekton", "oci", "gcs", "docdb", "grafeas", "kafka", "archivista")), asString(ociSignerKey, &cfg.Artifacts.OCI.Signer, "x509", "kms"), // PubSub - General @@ -305,6 +314,9 @@ func NewConfigFromMap(data map[string]string) (*Config, error) { asString(docDBMongoServerURLKey, &cfg.Storage.DocDB.MongoServerURL), asString(docDBMongoServerURLDirKey, &cfg.Storage.DocDB.MongoServerURLDir), asString(docDBMongoServerURLPathKey, &cfg.Storage.DocDB.MongoServerURLPath), + + asString(archivistaURLKey, &cfg.Storage.Archivista.URL), + asString(grafeasProjectIDKey, &cfg.Storage.Grafeas.ProjectID), asString(grafeasNoteIDKey, &cfg.Storage.Grafeas.NoteID), asString(grafeasNoteHint, &cfg.Storage.Grafeas.NoteHint), diff --git a/vendor/github.com/edwarnicke/gitoid/.gitignore b/vendor/github.com/edwarnicke/gitoid/.gitignore new file mode 100644 index 0000000000..f3a1246a32 --- /dev/null +++ b/vendor/github.com/edwarnicke/gitoid/.gitignore @@ -0,0 +1,24 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +# Goland files +.idea/ diff --git a/vendor/github.com/edwarnicke/gitoid/LICENSE b/vendor/github.com/edwarnicke/gitoid/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/vendor/github.com/edwarnicke/gitoid/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/vendor/github.com/edwarnicke/gitoid/README.md b/vendor/github.com/edwarnicke/gitoid/README.md new file mode 100644 index 0000000000..a4e8dae360 --- /dev/null +++ b/vendor/github.com/edwarnicke/gitoid/README.md @@ -0,0 +1,111 @@ +gitoid provides a simple library to compute gitoids (git object ids) + +## Creating GitOIDs + +### Default Usage +By default it produces gitoids for git object type blob using sha1: + +```go +var reader os.Reader +gitoidHash, err := gitoid.New(reader) +fmt.Println(gitoidHash) +// Output: 261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64 +fmt.Println(gitoidHash.URI()) +// Output: gitoid:blob:sha1:261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64 +``` + +### GitOid from string or []byte + +It's simple to compute the gitoid from a string or []byte by using bytes.NewBuffer: + +```go +input := []byte("example") +gitoidHash, _ := gitoid.New(bytes.NewBuffer(input)) +fmt.Println(gitoidHash) +// Output: 96236f8158b12701d5e75c14fb876c4a0f31b963 +fmt.Println(gitoidHash.URI()) +// Output: gitoid:blob:sha1:96236f8158b12701d5e75c14fb876c4a0f31b963 +``` + +### GitOID from URIs + +GitOIDs can be represented as a [gitoid uri](https://www.iana.org/assignments/uri-schemes/prov/gitoid). + +```go +gitoidHash, _ := gitoid.FromURI("gitoid:blob:sha1:96236f8158b12701d5e75c14fb876c4a0f31b96") +fmt.Println(gitoidHash) +// Output: 96236f8158b12701d5e75c14fb876c4a0f31b963 +fmt.Println(gitoidHash.URI()) +// Output: gitoid:blob:sha1:96236f8158b12701d5e75c14fb876c4a0f31b963 +``` + +## Variations on GitOIDs + +### SHA256 gitoids + +Git defaults to computing gitoids with sha1. Git also supports sha256 gitoids. Sha256 gitoids are supported using +an Option: + +```go +var reader os.Reader +gitoidHash, err := gitoid.New(reader, gitoid.WithSha256()) +fmt.Println(gitoidHash) +// Output: ed43975fbdc3084195eb94723b5f6df44eeeed1cdda7db0c7121edf5d84569ab +fmt.Println(gitoidHash.URI()) +// Output: gitoid:blob:sha256:ed43975fbdc3084195eb94723b5f6df44eeeed1cdda7db0c7121edf5d84569ab +``` + +### Other git object types + +git has four object types: blob, tree, commit, tag. By default gitoid using object type blob. +You may optionally specify another object type using an Option: + +```go +var reader os.Reader +gitoidHash, err := gitoid.New(reader, gitoid.WithGitObjectType(gitoid.COMMIT)) +``` + +### Assert ContentLength + +git object ids consist of hash over a header followed by the file contents. The header contains the length of the file +contents. By default, gitoid simply copies the reader into a buffer to establish its contentLength to compute the header. + +If you wish to assert the contentLength yourself, you may do so with an Option: + +```go +var reader os.Reader +var contentLength int64 +gitoidHash, _ := gitoid.New(reader, gitoid.WithContentLength(contentLength)) +fmt.Println(gitoidHash) +// Output: 261eeb9e9f8b2b4b0d119366dda99c6fd7d35c64 +``` + +gitoid will read the first contentLength bytes from the provided reader. If the reader is unable to provide +contentLength bytes a wrapper error around io.ErrUnexpectedEOF will be returned from gitoid.New + +## Using GitOIDs + +### Match contents to a GitOID + +```go +var reader io.Reader +var gitoidHash *gitoid.GitOID +if gitoidHash.Match(reader) { + fmt.Println("matched") +} +``` + +### Find files that match GitOID + +```go +var path1 fs.FS = os.DirFS("./relative/path") +var path2 fs.FS = os.DirFS("/absolute/path") +var gitoidHash *gitoid.GitOID + +// Find a file in path1 and path2 that matches gitoidHash +file,_ := gitoidHash.Find(path1, path2) + +// Find all files in path1 and path2 that matches gitoidHash +files, := gitoidHash.FindAll(path1, path2) +``` + diff --git a/vendor/github.com/edwarnicke/gitoid/gitoid.go b/vendor/github.com/edwarnicke/gitoid/gitoid.go new file mode 100644 index 0000000000..12d5168b46 --- /dev/null +++ b/vendor/github.com/edwarnicke/gitoid/gitoid.go @@ -0,0 +1,215 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 gitoid + +import ( + "bytes" + "crypto/sha1" // #nosec G505 + "encoding/hex" + "errors" + "fmt" + "io" + "io/fs" + "strings" +) + +// GitObjectType type of git object - current values are "blob", "commit", "tag", "tree". +type GitObjectType string + +const ( + BLOB GitObjectType = "blob" + COMMIT GitObjectType = "commit" + TAG GitObjectType = "tag" + TREE GitObjectType = "tree" +) + +var ErrMayNotBeNil = errors.New("may not be nil") +var ErrInvalidGitOIDURI = errors.New("invalid uri in gitoid.FromURI") + +type GitOID struct { + gitObjectType GitObjectType + hashName string + hashValue []byte +} + +// New - create a new GitOID +// by default git object type is "blob" and hash is sha1 +func New(reader io.Reader, opts ...Option) (*GitOID, error) { + if reader == nil { + return nil, fmt.Errorf("reader in gitoid.New: %w", ErrMayNotBeNil) + } + + o := &option{ + gitObjectType: BLOB, + /* #nosec G401 */ + h: sha1.New(), + hashName: "sha1", + contentLength: 0, + } + + for _, opt := range opts { + opt(o) + } + + // If there is no declared o.contentLength, copy the entire reader into a buffer so we can compute + // the contentLength + if o.contentLength == 0 { + buf := bytes.NewBuffer(nil) + + contentLength, err := io.Copy(buf, reader) + if err != nil { + return nil, fmt.Errorf("error copying reader to buffer in gitoid.New: %w", err) + } + + reader = buf + o.contentLength = contentLength + } + + // Write the git object header + o.h.Write(Header(o.gitObjectType, o.contentLength)) + + // Copy the reader to the hash + n, err := io.Copy(o.h, io.LimitReader(reader, o.contentLength)) + if err != nil { + return nil, fmt.Errorf("error copying reader to hash.Hash.Writer in gitoid.New: %w", err) + } + + if n < o.contentLength { + return nil, fmt.Errorf("expected contentLength (%d) is less than actual contentLength (%d) in gitoid.New: %w", o.contentLength, n, io.ErrUnexpectedEOF) + } + + return &GitOID{ + gitObjectType: o.gitObjectType, + hashName: o.hashName, + hashValue: o.h.Sum(nil), + }, nil +} + +// Header - returns the git object header from the gitObjectType and contentLength. +func Header(gitObjectType GitObjectType, contentLength int64) []byte { + return []byte(fmt.Sprintf("%s %d\000", gitObjectType, contentLength)) +} + +// String - returns the gitoid in lowercase hex. +func (g *GitOID) String() string { + return fmt.Sprintf("%x", g.hashValue) +} + +// URI - returns the gitoid as a URI (https://www.iana.org/assignments/uri-schemes/prov/gitoid) +func (g *GitOID) URI() string { + return fmt.Sprintf("gitoid:%s:%s:%s", g.gitObjectType, g.hashName, g) +} + +func (g *GitOID) Bytes() []byte { + if g == nil { + return nil + } + + return g.hashValue +} + +// Equal - returns true of g == x. +func (g *GitOID) Equal(x *GitOID) bool { + if g == x { + return true + } + + if g == nil || x == nil || g.hashName != x.hashName { + return false + } + + if len(g.Bytes()) != len(x.Bytes()) { + return false + } + + for i, v := range g.Bytes() { + if x.Bytes()[i] != v { + return false + } + } + return true +} + +// FromURI - returns a *GitOID from a gitoid uri string - see https://www.iana.org/assignments/uri-schemes/prov/gitoid +func FromURI(uri string) (*GitOID, error) { + parts := strings.Split(uri, ":") + if len(parts) != 4 || parts[0] != "gitoid" { + return nil, fmt.Errorf("%w: %q in gitoid.FromURI", ErrInvalidGitOIDURI, uri) + } + hashValue, err := hex.DecodeString(parts[3]) + if err != nil { + return nil, fmt.Errorf("error decoding hash value (%s) in gitoid.FromURI: %w", parts[3], err) + } + return &GitOID{ + gitObjectType: GitObjectType(parts[1]), + hashName: parts[2], + hashValue: hashValue, + }, nil +} + +// Match - returns true if contents of reader generates a GitOID equal to g. +func (g *GitOID) Match(reader io.Reader) bool { + g2, err := New(reader, WithGitObjectType(g.gitObjectType)) + if err != nil { + return false + } + return g.Equal(g2) +} + +// Find - return the first fs.File in paths that Matches the *GitOID g. +func (g *GitOID) Find(paths ...fs.FS) fs.File { + foundFiles := g.findN(1, paths...) + if len(foundFiles) != 1 { + return nil + } + return foundFiles[0] +} + +// FindAll - return all fs.Files in paths that Matches the *GitOID g. +func (g *GitOID) FindAll(paths ...fs.FS) []fs.File { + return g.findN(0, paths...) +} + +func (g *GitOID) findN(n int, paths ...fs.FS) []fs.File { + var foundFiles []fs.File + for _, fsys := range paths { + _ = fs.WalkDir(fsys, ".", func(path string, d fs.DirEntry, err error) error { + if d == nil || d.IsDir() || err != nil { + //lint:ignore nilerr - returning non-nil error will stop the walk + return nil + } + file, err := fsys.Open(path) + defer func() { _ = file.Close() }() + if err != nil { + //lint:ignore nilerr - returning non-nil error will stop the walk + return nil + } + if !g.Match(file) { + return nil + } + foundFile, err := fsys.Open(path) + if err == nil { + foundFiles = append(foundFiles, foundFile) + } + if n > 0 && len(foundFiles) == n { + return io.EOF + } + return nil + }) + } + return foundFiles +} diff --git a/vendor/github.com/edwarnicke/gitoid/options.go b/vendor/github.com/edwarnicke/gitoid/options.go new file mode 100644 index 0000000000..f104e198b5 --- /dev/null +++ b/vendor/github.com/edwarnicke/gitoid/options.go @@ -0,0 +1,56 @@ +// Copyright (c) 2022 Cisco and/or its affiliates. +// +// SPDX-License-Identifier: Apache-2.0 +// +// 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 gitoid + +import ( + "crypto/sha256" + "hash" +) + +type option struct { + gitObjectType GitObjectType + h hash.Hash + hashName string + contentLength int64 +} + +// Option - option for GitOID creation. +type Option func(o *option) + +// WithSha256 - use sha256 for computing gitoids instead of the default sha1. +func WithSha256() Option { + return func(o *option) { + o.hashName = "sha256" + o.h = sha256.New() + } +} + +// WithGitObjectType - set the GitOobjectType to a value different than the default gitoid.BLOB type. +func WithGitObjectType(gitObjectType GitObjectType) Option { + return func(o *option) { + o.gitObjectType = gitObjectType + } +} + +// WithContentLength - allows the assertion of a contentLength to be read from the provided reader +// only the first contentLength of data will be read from the reader +// if contentLength bytes are unavailable from the reader, an error will be returned. +func WithContentLength(contentLength int64) Option { + return func(o *option) { + o.contentLength = contentLength + } +} diff --git a/vendor/github.com/in-toto/archivista/LICENSE b/vendor/github.com/in-toto/archivista/LICENSE new file mode 100644 index 0000000000..261eeb9e9f --- /dev/null +++ b/vendor/github.com/in-toto/archivista/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/vendor/github.com/in-toto/archivista/pkg/api/download.go b/vendor/github.com/in-toto/archivista/pkg/api/download.go new file mode 100644 index 0000000000..df76ae6918 --- /dev/null +++ b/vendor/github.com/in-toto/archivista/pkg/api/download.go @@ -0,0 +1,109 @@ +// Copyright 2023-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 api + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/url" + + "github.com/in-toto/go-witness/dsse" +) + +func DownloadReadCloser(ctx context.Context, baseURL string, gitoid string) (io.ReadCloser, error) { + return DownloadReadCloserWithHTTPClient(ctx, &http.Client{}, baseURL, gitoid) +} + +func DownloadReadCloserWithHTTPClient(ctx context.Context, client *http.Client, baseURL string, gitoid string) (io.ReadCloser, error) { + downloadURL, err := url.JoinPath(baseURL, "download", gitoid) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadURL, nil) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + resp, err := client.Do(req) + if err != nil { + return nil, err + } + if resp.StatusCode != http.StatusOK { + // NOTE: attempt to read body on error and + // only close if an error occurs + defer resp.Body.Close() + errMsg, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + return nil, errors.New(string(errMsg)) + } + return resp.Body, nil +} + +func Download(ctx context.Context, baseURL string, gitoid string) (dsse.Envelope, error) { + buf := &bytes.Buffer{} + if err := DownloadWithWriter(ctx, baseURL, gitoid, buf); err != nil { + return dsse.Envelope{}, err + } + + env := dsse.Envelope{} + dec := json.NewDecoder(buf) + if err := dec.Decode(&env); err != nil { + return env, err + } + + return env, nil +} + +func DownloadWithWriter(ctx context.Context, baseURL string, gitoid string, dst io.Writer) error { + return DownloadWithWriterWithHTTPClient(ctx, &http.Client{}, baseURL, gitoid, dst) +} + +func DownloadWithWriterWithHTTPClient(ctx context.Context, client *http.Client, baseURL string, gitoid string, dst io.Writer) error { + downloadUrl, err := url.JoinPath(baseURL, "download", gitoid) + if err != nil { + return err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, downloadUrl, nil) + if err != nil { + return err + } + + req.Header.Set("Content-Type", "application/json") + hc := &http.Client{} + resp, err := hc.Do(req) + if err != nil { + return err + } + + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + errMsg, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + return errors.New(string(errMsg)) + } + + _, err = io.Copy(dst, resp.Body) + return err +} diff --git a/vendor/github.com/in-toto/archivista/pkg/api/graphql.go b/vendor/github.com/in-toto/archivista/pkg/api/graphql.go new file mode 100644 index 0000000000..a7f29d237c --- /dev/null +++ b/vendor/github.com/in-toto/archivista/pkg/api/graphql.go @@ -0,0 +1,137 @@ +// Copyright 2023-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 api + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" +) + +const RetrieveSubjectsQuery = `query($gitoid: String!) { + subjects( + where: { + hasStatementWith:{ + hasDsseWith:{ + gitoidSha256: $gitoid + } + } + } + ) { + edges { + node{ + name + subjectDigests{ + algorithm + value + } + } + } + } +}` + +const SearchQuery = `query($algo: String!, $digest: String!) { + dsses( + where: { + hasStatementWith: { + hasSubjectsWith: { + hasSubjectDigestsWith: { + value: $digest, + algorithm: $algo + } + } + } + } + ) { + edges { + node { + gitoidSha256 + statement { + attestationCollections { + name + attestations { + type + } + } + } + } + } + } +}` + +func GraphQlQuery[TRes any, TVars any](ctx context.Context, baseUrl, query string, vars TVars) (TRes, error) { + return GraphQlQueryWithHeaders[TRes, TVars](ctx, baseUrl, query, vars, nil) +} + +func GraphQlQueryWithHeaders[TRes any, TVars any](ctx context.Context, baseUrl, query string, vars TVars, headers map[string]string) (TRes, error) { + var response TRes + queryUrl, err := url.JoinPath(baseUrl, "query") + if err != nil { + return response, err + } + + requestBody := GraphQLRequestBodyGeneric[TVars]{ + Query: query, + Variables: vars, + } + + reqBody, err := json.Marshal(requestBody) + if err != nil { + return response, err + } + + req, err := http.NewRequestWithContext(ctx, http.MethodPost, queryUrl, bytes.NewReader(reqBody)) + if err != nil { + return response, err + } + + for k, v := range headers { + req.Header.Set(k, v) + } + + req.Header.Set("Content-Type", "application/json") + hc := &http.Client{} + res, err := hc.Do(req) + if err != nil { + return response, err + } + + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + errMsg, err := io.ReadAll(res.Body) + if err != nil { + return response, err + } + + return response, errors.New(string(errMsg)) + } + + dec := json.NewDecoder(res.Body) + gqlRes := GraphQLResponseGeneric[TRes]{} + if err := dec.Decode(&gqlRes); err != nil { + return response, err + } + + if len(gqlRes.Errors) > 0 { + return response, fmt.Errorf("graph ql query failed: %v", gqlRes.Errors) + } + + return gqlRes.Data, nil +} diff --git a/vendor/github.com/in-toto/archivista/pkg/api/structs.go b/vendor/github.com/in-toto/archivista/pkg/api/structs.go new file mode 100644 index 0000000000..fcf295a40f --- /dev/null +++ b/vendor/github.com/in-toto/archivista/pkg/api/structs.go @@ -0,0 +1,90 @@ +// Copyright 2024 The Archivista 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 api + +type GraphQLError struct { + Message string `json:"message"` +} + +type GraphQLResponseGeneric[T any] struct { + Data T `json:"data,omitempty"` + Errors []GraphQLError `json:"errors,omitempty"` +} + +type GraphQLRequestBodyGeneric[TVars any] struct { + Query string `json:"query"` + Variables TVars `json:"variables,omitempty"` +} + +type RetrieveSubjectVars struct { + Gitoid string `json:"gitoid"` +} + +type SearchVars struct { + Algorithm string `json:"algo"` + Digest string `json:"digest"` +} + +type RetrieveSubjectResults struct { + Subjects Subjects `json:"subjects"` +} + +type Subjects struct { + Edges []SubjectEdge `json:"edges"` +} + +type SubjectEdge struct { + Node SubjectNode `json:"node"` +} + +type SubjectNode struct { + Name string `json:"name"` + SubjectDigests []SubjectDigest `json:"subjectDigests"` +} + +type SubjectDigest struct { + Algorithm string `json:"algorithm"` + Value string `json:"value"` +} + +type SearchResults struct { + Dsses DSSES `json:"dsses"` +} + +type DSSES struct { + Edges []SearchEdge `json:"edges"` +} + +type SearchEdge struct { + Node SearchNode `json:"node"` +} + +type SearchNode struct { + GitoidSha256 string `json:"gitoidSha256"` + Statement Statement `json:"statement"` +} + +type Statement struct { + AttestationCollection AttestationCollection `json:"attestationCollections"` +} + +type AttestationCollection struct { + Name string `json:"name"` + Attestations []Attestation `json:"attestations"` +} + +type Attestation struct { + Type string `json:"type"` +} diff --git a/vendor/github.com/in-toto/archivista/pkg/api/upload.go b/vendor/github.com/in-toto/archivista/pkg/api/upload.go new file mode 100644 index 0000000000..3bf9934420 --- /dev/null +++ b/vendor/github.com/in-toto/archivista/pkg/api/upload.go @@ -0,0 +1,89 @@ +// Copyright 2023-2024 The Archivista 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 api + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "io" + "net/http" + "net/url" + + "github.com/in-toto/go-witness/dsse" +) + +type UploadResponse struct { + Gitoid string `json:"gitoid"` +} + +// Deprecated: Use UploadResponse instead. It will be removed in version >= v0.6.0 +type StoreResponse = UploadResponse + +// Deprecated: Use Store instead. It will be removed in version >= v0.6.0 +func Upload(ctx context.Context, baseURL string, envelope dsse.Envelope) (UploadResponse, error) { + return Store(ctx, baseURL, envelope) +} + +func Store(ctx context.Context, baseURL string, envelope dsse.Envelope) (StoreResponse, error) { + buf := &bytes.Buffer{} + enc := json.NewEncoder(buf) + if err := enc.Encode(envelope); err != nil { + return StoreResponse{}, err + } + + return StoreWithReader(ctx, baseURL, buf) +} + +func StoreWithReader(ctx context.Context, baseURL string, r io.Reader) (StoreResponse, error) { + return StoreWithReaderWithHTTPClient(ctx, &http.Client{}, baseURL, r) +} + +func StoreWithReaderWithHTTPClient(ctx context.Context, client *http.Client, baseURL string, r io.Reader) (StoreResponse, error) { + uploadPath, err := url.JoinPath(baseURL, "upload") + if err != nil { + return UploadResponse{}, err + } + + req, err := http.NewRequestWithContext(ctx, "POST", uploadPath, r) + if err != nil { + return UploadResponse{}, err + } + + req.Header.Set("Content-Type", "application/json") + hc := &http.Client{} + resp, err := hc.Do(req) + if err != nil { + return UploadResponse{}, err + } + + defer resp.Body.Close() + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return UploadResponse{}, err + } + + if resp.StatusCode != http.StatusOK { + return UploadResponse{}, errors.New(string(bodyBytes)) + } + + uploadResp := UploadResponse{} + if err := json.Unmarshal(bodyBytes, &uploadResp); err != nil { + return UploadResponse{}, err + } + + return uploadResp, nil +} diff --git a/vendor/github.com/in-toto/archivista/pkg/http-client/client.go b/vendor/github.com/in-toto/archivista/pkg/http-client/client.go new file mode 100644 index 0000000000..22b87c1a9a --- /dev/null +++ b/vendor/github.com/in-toto/archivista/pkg/http-client/client.go @@ -0,0 +1,216 @@ +// Copyright 2024 The Archivista 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 httpclient + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "net/url" + + "github.com/in-toto/archivista/pkg/api" + "github.com/in-toto/go-witness/dsse" +) + +type ArchivistaClient struct { + BaseURL string + GraphQLURL string + *http.Client +} + +type HttpClienter interface { + DownloadDSSE(ctx context.Context, gitoid string) (dsse.Envelope, error) + DownloadReadCloser(ctx context.Context, gitoid string) (io.ReadCloser, error) + DownloadWithWriter(ctx context.Context, gitoid string, dst io.Writer) error + Store(ctx context.Context, envelope dsse.Envelope) (api.UploadResponse, error) + StoreWithReader(ctx context.Context, r io.Reader) (api.UploadResponse, error) + GraphQLRetrieveSubjectResults(ctx context.Context, gitoid string) (api.RetrieveSubjectResults, error) + GraphQLRetrieveSearchResults(ctx context.Context, algo string, digest string) (api.SearchResults, error) + GraphQLQueryIface(ctx context.Context, query string, variables interface{}) (*GraphQLResponseInterface, error) + GraphQLQueryToDst(ctx context.Context, query string, variables interface{}, dst interface{}) error + GraphQLQueryReadCloser(ctx context.Context, query string, variables interface{}) (io.ReadCloser, error) +} + +func CreateArchivistaClient(httpClient *http.Client, baseURL string) (*ArchivistaClient, error) { + client := ArchivistaClient{ + BaseURL: baseURL, + Client: http.DefaultClient, + } + if httpClient != nil { + client.Client = httpClient + } + var err error + client.GraphQLURL, err = url.JoinPath(client.BaseURL, "query") + if err != nil { + return nil, err + } + return &client, nil +} + +func (ac *ArchivistaClient) DownloadDSSE(ctx context.Context, gitoid string) (dsse.Envelope, error) { + reader, err := api.DownloadReadCloserWithHTTPClient(ctx, ac.Client, ac.BaseURL, gitoid) + if err != nil { + return dsse.Envelope{}, err + } + env := dsse.Envelope{} + if err := json.NewDecoder(reader).Decode(&env); err != nil { + return dsse.Envelope{}, err + } + return env, nil +} + +func (ac *ArchivistaClient) DownloadReadCloser(ctx context.Context, gitoid string) (io.ReadCloser, error) { + return api.DownloadReadCloserWithHTTPClient(ctx, ac.Client, ac.BaseURL, gitoid) +} + +func (ac *ArchivistaClient) DownloadWithWriter(ctx context.Context, gitoid string, dst io.Writer) error { + return api.DownloadWithWriterWithHTTPClient(ctx, ac.Client, ac.BaseURL, gitoid, dst) +} + +func (ac *ArchivistaClient) Store(ctx context.Context, envelope dsse.Envelope) (api.UploadResponse, error) { + return api.Store(ctx, ac.BaseURL, envelope) +} + +func (ac *ArchivistaClient) StoreWithReader(ctx context.Context, r io.Reader) (api.UploadResponse, error) { + return api.StoreWithReader(ctx, ac.BaseURL, r) +} + +type GraphQLRequestBodyInterface struct { + Query string `json:"query"` + Variables interface{} `json:"variables,omitempty"` +} + +type GraphQLResponseInterface struct { + Data interface{} + Errors []api.GraphQLError `json:"errors,omitempty"` +} + +// GraphQLRetrieveSubjectResults retrieves the subjects for a given gitoid. +func (ac *ArchivistaClient) GraphQLRetrieveSubjectResults( + ctx context.Context, + gitoid string, +) (api.RetrieveSubjectResults, error) { + return api.GraphQlQuery[api.RetrieveSubjectResults]( + ctx, + ac.BaseURL, + api.RetrieveSubjectsQuery, + api.RetrieveSubjectVars{Gitoid: gitoid}, + ) +} + +// GraphQLRetrieveSearchResults retrieves the search results for a given algorithm and digest. +func (ac *ArchivistaClient) GraphQLRetrieveSearchResults( + ctx context.Context, + algo string, + digest string, +) (api.SearchResults, error) { + return api.GraphQlQuery[api.SearchResults]( + ctx, + ac.BaseURL, + api.SearchQuery, + api.SearchVars{Algorithm: algo, Digest: digest}, + ) +} + +// GraphQLQueryIface executes a GraphQL query against the Archivista API and returns the response as an interface. +// +// Parameters: +// - ctx: The context to control the query's lifecycle, such as cancellations or deadlines. +// - query: A string representing the GraphQL query to be executed. +// - variables: A map or struct containing variables to parameterize the query. +// +// Returns: +// - A pointer to a GraphQLResponseInterface containing the query's result or errors. +// - An error if the query execution or response parsing fails. +// +// Example: +// +// response, err := client.GraphQLQueryIface(ctx, query, variables) +// if err != nil { +// log.Fatalf("GraphQL query failed: %v", err) +// } +// fmt.Printf("Response data: %+v\n", response.Data) +func (ac *ArchivistaClient) GraphQLQueryIface( + ctx context.Context, + query string, + variables interface{}, +) (*GraphQLResponseInterface, error) { + reader, err := ac.GraphQLQueryReadCloser(ctx, query, variables) + if err != nil { + return nil, err + } + defer reader.Close() + gqlRes := GraphQLResponseInterface{} + dec := json.NewDecoder(reader) + if err := dec.Decode(&gqlRes); err != nil { + return nil, err + } + if len(gqlRes.Errors) > 0 { + return nil, fmt.Errorf("graph ql query failed: %v", gqlRes.Errors) + } + return &gqlRes, nil +} + +// GraphQLQueryToDst executes a GraphQL query against the Archivista API and unmarshals the response into a destination object. +func (ac *ArchivistaClient) GraphQLQueryToDst(ctx context.Context, query string, variables interface{}, dst interface{}) error { + reader, err := ac.GraphQLQueryReadCloser(ctx, query, variables) + if err != nil { + return err + } + defer reader.Close() + dec := json.NewDecoder(reader) + if err := dec.Decode(&dst); err != nil { + return err + } + return nil +} + +// GraphQLQueryReadCloser executes a GraphQL query against the Archivista API and returns the response as an io.ReadCloser. +func (ac *ArchivistaClient) GraphQLQueryReadCloser( + ctx context.Context, + query string, + variables interface{}, +) (io.ReadCloser, error) { + requestBodyMap := GraphQLRequestBodyInterface{ + Query: query, + Variables: variables, + } + requestBodyJSON, err := json.Marshal(requestBodyMap) + if err != nil { + return nil, err + } + req, err := http.NewRequestWithContext(ctx, http.MethodPost, ac.GraphQLURL, bytes.NewReader(requestBodyJSON)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/json") + res, err := ac.Do(req) + if err != nil { + return nil, err + } + if res.StatusCode != http.StatusOK { + defer res.Body.Close() + errMsg, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + return nil, errors.New(string(errMsg)) + } + return res.Body, nil +} diff --git a/vendor/github.com/in-toto/go-witness/LICENSE b/vendor/github.com/in-toto/go-witness/LICENSE new file mode 100644 index 0000000000..c54e7d1566 --- /dev/null +++ b/vendor/github.com/in-toto/go-witness/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2021 TestifySec, LLC. + + 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. diff --git a/vendor/github.com/in-toto/go-witness/cryptoutil/digestset.go b/vendor/github.com/in-toto/go-witness/cryptoutil/digestset.go new file mode 100644 index 0000000000..3b91a8a08e --- /dev/null +++ b/vendor/github.com/in-toto/go-witness/cryptoutil/digestset.go @@ -0,0 +1,292 @@ +// Copyright 2022 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 cryptoutil + +import ( + "bytes" + "crypto" + "encoding/json" + "fmt" + "hash" + "io" + "os" + + "golang.org/x/mod/sumdb/dirhash" +) + +var ( + hashNames = map[DigestValue]string{ + { + Hash: crypto.SHA256, + GitOID: false, + DirHash: false, + }: "sha256", + { + Hash: crypto.SHA1, + GitOID: false, + DirHash: false, + }: "sha1", + { + Hash: crypto.SHA256, + GitOID: true, + DirHash: false, + }: "gitoid:sha256", + { + Hash: crypto.SHA1, + GitOID: true, + DirHash: false, + }: "gitoid:sha1", + { + Hash: crypto.SHA256, + GitOID: false, + DirHash: true, + }: "dirHash", + } + + hashesByName = map[string]DigestValue{ + "sha256": { + crypto.SHA256, + false, + false, + }, + "sha1": { + crypto.SHA1, + false, + false, + }, + "gitoid:sha256": { + crypto.SHA256, + true, + false, + }, + "gitoid:sha1": { + crypto.SHA1, + true, + false, + }, + "dirHash": { + crypto.SHA256, + false, + true, + }, + } +) + +type ErrUnsupportedHash string + +func (e ErrUnsupportedHash) Error() string { + return fmt.Sprintf("unsupported hash function: %v", string(e)) +} + +type DigestValue struct { + crypto.Hash + GitOID bool + DirHash bool +} + +func (dv DigestValue) New() hash.Hash { + if dv.GitOID { + return &gitoidHasher{hash: dv.Hash, buf: &bytes.Buffer{}} + } + + return dv.Hash.New() +} + +type DigestSet map[DigestValue]string + +func HashToString(h crypto.Hash) (string, error) { + if name, ok := hashNames[DigestValue{Hash: h}]; ok { + return name, nil + } + + return "", ErrUnsupportedHash(h.String()) +} + +func HashFromString(name string) (crypto.Hash, error) { + if hash, ok := hashesByName[name]; ok { + return hash.Hash, nil + } + + return crypto.Hash(0), ErrUnsupportedHash(name) +} + +// Equal returns true if every digest for hash functions both artifacts have in common are equal. +// If the two artifacts don't have any digests from common hash functions, equal will return false. +// If any digest from common hash functions differ between the two artifacts, equal will return false. +func (ds *DigestSet) Equal(second DigestSet) bool { + hasMatchingDigest := false + for hash, digest := range *ds { + otherDigest, ok := second[hash] + if !ok { + continue + } + + if digest == otherDigest { + hasMatchingDigest = true + } else { + return false + } + } + + return hasMatchingDigest +} + +func (ds *DigestSet) ToNameMap() (map[string]string, error) { + nameMap := make(map[string]string) + for hash, digest := range *ds { + name, ok := hashNames[hash] + if !ok { + return nameMap, ErrUnsupportedHash(hash.String()) + } + + nameMap[name] = digest + } + + return nameMap, nil +} + +func NewDigestSet(digestsByName map[string]string) (DigestSet, error) { + ds := make(DigestSet) + for hashName, digest := range digestsByName { + hash, ok := hashesByName[hashName] + if !ok { + return ds, ErrUnsupportedHash(hashName) + } + + ds[hash] = digest + } + + return ds, nil +} + +func CalculateDigestSet(r io.Reader, digestValues []DigestValue) (DigestSet, error) { + digestSet := make(DigestSet) + writers := []io.Writer{} + hashfuncs := map[DigestValue]hash.Hash{} + for _, digestValue := range digestValues { + hashfunc := digestValue.New() + hashfuncs[digestValue] = hashfunc + writers = append(writers, hashfunc) + } + + multiwriter := io.MultiWriter(writers...) + if _, err := io.Copy(multiwriter, r); err != nil { + return digestSet, err + } + + for digestValue, hashfunc := range hashfuncs { + // gitoids are somewhat special... we're using a custom implementation of hash.Hash + // to wrap the gitoid library. Sum will return a gitoid URI, so we don't want to hex + // encode it as it's already a string with a hex encoded hash. + if digestValue.GitOID { + digestSet[digestValue] = string(hashfunc.Sum(nil)) + continue + } + + digestSet[digestValue] = string(HexEncode(hashfunc.Sum(nil))) + } + + return digestSet, nil +} + +func CalculateDigestSetFromBytes(data []byte, hashes []DigestValue) (DigestSet, error) { + return CalculateDigestSet(bytes.NewReader(data), hashes) +} + +func CalculateDigestSetFromFile(path string, hashes []DigestValue) (DigestSet, error) { + file, err := os.Open(path) + if err != nil { + return DigestSet{}, err + } + + hashable, err := isHashableFile(file) + if err != nil { + return DigestSet{}, err + } + + if !hashable { + return DigestSet{}, fmt.Errorf("%s is not a hashable file", path) + } + + defer file.Close() + return CalculateDigestSet(file, hashes) +} + +func CalculateDigestSetFromDir(dir string, hashes []DigestValue) (DigestSet, error) { + + dirHash, err := dirhash.HashDir(dir, "", DirhHashSha256) + if err != nil { + return nil, err + } + + digestSetByName := make(map[string]string) + digestSetByName["dirHash"] = dirHash + + return NewDigestSet(digestSetByName) +} + +func (ds DigestSet) MarshalJSON() ([]byte, error) { + nameMap, err := ds.ToNameMap() + if err != nil { + return nil, err + } + + return json.Marshal(nameMap) +} + +func (ds *DigestSet) UnmarshalJSON(data []byte) error { + nameMap := make(map[string]string) + err := json.Unmarshal(data, &nameMap) + if err != nil { + return err + } + + newDs, err := NewDigestSet(nameMap) + if err != nil { + return err + } + + *ds = newDs + return nil +} + +func isHashableFile(f *os.File) (bool, error) { + stat, err := f.Stat() + if err != nil { + return false, err + } + + mode := stat.Mode() + + isSpecial := stat.Mode()&os.ModeCharDevice != 0 + + if isSpecial { + return false, nil + } + + if mode.IsRegular() { + return true, nil + } + + if mode.Perm().IsDir() { + return true, nil + } + + if mode&os.ModeSymlink == 1 { + return true, nil + } + + return false, nil +} diff --git a/vendor/github.com/in-toto/go-witness/cryptoutil/dirhash.go b/vendor/github.com/in-toto/go-witness/cryptoutil/dirhash.go new file mode 100644 index 0000000000..044a2b1519 --- /dev/null +++ b/vendor/github.com/in-toto/go-witness/cryptoutil/dirhash.go @@ -0,0 +1,61 @@ +// Copyright 2022 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 cryptoutil + +import ( + "crypto/sha256" + "encoding/hex" + "errors" + "fmt" + "io" + "sort" + "strings" +) + +// DirHashSha256 is the "h1:" directory hash function, using SHA-256. +// +// DirHashSha256 returns a SHA-256 hash of a summary +// prepared as if by the Unix command: +// +// sha256sum $(find . -type f | sort) | sha256sum +// +// More precisely, the hashed summary contains a single line for each file in the list, +// ordered by sort.Strings applied to the file names, where each line consists of +// the hexadecimal SHA-256 hash of the file content, +// two spaces (U+0020), the file name, and a newline (U+000A). +// +// File names with newlines (U+000A) are disallowed. +func DirhHashSha256(files []string, open func(string) (io.ReadCloser, error)) (string, error) { + h := sha256.New() + files = append([]string(nil), files...) + sort.Strings(files) + for _, file := range files { + if strings.Contains(file, "\n") { + return "", errors.New("dirhash: filenames with newlines are not supported") + } + r, err := open(file) + if err != nil { + return "", err + } + hf := sha256.New() + _, err = io.Copy(hf, r) + r.Close() + if err != nil { + return "", err + } + fmt.Fprintf(h, "%x %s\n", hf.Sum(nil), file) + } + return hex.EncodeToString(h.Sum(nil)), nil +} diff --git a/vendor/github.com/in-toto/go-witness/cryptoutil/ecdsa.go b/vendor/github.com/in-toto/go-witness/cryptoutil/ecdsa.go new file mode 100644 index 0000000000..172ad97831 --- /dev/null +++ b/vendor/github.com/in-toto/go-witness/cryptoutil/ecdsa.go @@ -0,0 +1,85 @@ +// Copyright 2021 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 cryptoutil + +import ( + "crypto" + "crypto/ecdsa" + "crypto/rand" + "io" +) + +type ErrVerifyFailed struct{} + +func (e ErrVerifyFailed) Error() string { + return "verification failed" +} + +type ECDSASigner struct { + priv *ecdsa.PrivateKey + hash crypto.Hash +} + +func NewECDSASigner(priv *ecdsa.PrivateKey, hash crypto.Hash) *ECDSASigner { + return &ECDSASigner{priv, hash} +} + +func (s *ECDSASigner) KeyID() (string, error) { + return GeneratePublicKeyID(&s.priv.PublicKey, s.hash) +} + +func (s *ECDSASigner) Sign(r io.Reader) ([]byte, error) { + digest, err := Digest(r, s.hash) + if err != nil { + return nil, err + } + + return ecdsa.SignASN1(rand.Reader, s.priv, digest) +} + +func (s *ECDSASigner) Verifier() (Verifier, error) { + return NewECDSAVerifier(&s.priv.PublicKey, s.hash), nil +} + +type ECDSAVerifier struct { + pub *ecdsa.PublicKey + hash crypto.Hash +} + +func NewECDSAVerifier(pub *ecdsa.PublicKey, hash crypto.Hash) *ECDSAVerifier { + return &ECDSAVerifier{pub, hash} +} + +func (v *ECDSAVerifier) KeyID() (string, error) { + return GeneratePublicKeyID(v.pub, v.hash) +} + +func (v *ECDSAVerifier) Verify(data io.Reader, sig []byte) error { + digest, err := Digest(data, v.hash) + if err != nil { + return err + } + + verified := ecdsa.VerifyASN1(v.pub, digest, sig) + if !verified { + return ErrVerifyFailed{} + } + + return nil +} + +func (v *ECDSAVerifier) Bytes() ([]byte, error) { + return PublicPemBytes(v.pub) +} diff --git a/vendor/github.com/in-toto/go-witness/cryptoutil/ed25519.go b/vendor/github.com/in-toto/go-witness/cryptoutil/ed25519.go new file mode 100644 index 0000000000..35f3741300 --- /dev/null +++ b/vendor/github.com/in-toto/go-witness/cryptoutil/ed25519.go @@ -0,0 +1,83 @@ +// Copyright 2021 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 cryptoutil + +import ( + "crypto" + "crypto/ed25519" + "fmt" + "io" +) + +type ED25519Signer struct { + priv ed25519.PrivateKey +} + +func NewED25519Signer(priv ed25519.PrivateKey) *ED25519Signer { + return &ED25519Signer{priv} +} + +func (s *ED25519Signer) KeyID() (string, error) { + return GeneratePublicKeyID(s.priv.Public(), crypto.SHA256) +} + +func (s *ED25519Signer) Sign(r io.Reader) ([]byte, error) { + msg, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + return ed25519.Sign(s.priv, msg), nil +} + +func (s *ED25519Signer) Verifier() (Verifier, error) { + pubKey := s.priv.Public() + edPubKey, ok := pubKey.(ed25519.PublicKey) + if !ok { + return nil, ErrUnsupportedKeyType{t: fmt.Sprintf("%T", edPubKey)} + } + + return NewED25519Verifier(edPubKey), nil +} + +type ED25519Verifier struct { + pub ed25519.PublicKey +} + +func NewED25519Verifier(pub ed25519.PublicKey) *ED25519Verifier { + return &ED25519Verifier{pub} +} + +func (v *ED25519Verifier) KeyID() (string, error) { + return GeneratePublicKeyID(v.pub, crypto.SHA256) +} + +func (v *ED25519Verifier) Verify(r io.Reader, sig []byte) error { + msg, err := io.ReadAll(r) + if err != nil { + return err + } + + verified := ed25519.Verify(v.pub, msg, sig) + if !verified { + return ErrVerifyFailed{} + } + + return nil +} + +func (v *ED25519Verifier) Bytes() ([]byte, error) { + return PublicPemBytes(v.pub) +} diff --git a/vendor/github.com/in-toto/go-witness/cryptoutil/gitoid.go b/vendor/github.com/in-toto/go-witness/cryptoutil/gitoid.go new file mode 100644 index 0000000000..f3fe365ca1 --- /dev/null +++ b/vendor/github.com/in-toto/go-witness/cryptoutil/gitoid.go @@ -0,0 +1,85 @@ +// Copyright 2023 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 cryptoutil + +import ( + "bytes" + "crypto" + "encoding/hex" + "fmt" + + "github.com/edwarnicke/gitoid" +) + +// gitoidHasher implements io.Writer so we can generate gitoids with our CalculateDigestSet function. +// CalculateDigestSet takes in an io.Reader pointing to some data we want to hash, and writes it to a +// MultiWriter that forwards it to writers for each hash we wish to calculate. +// This is a bit hacky -- it maintains an internal buffer and then when asked for the Sum, it calculates +// the gitoid. We may be able to contribute to the gitoid library to make this smoother +type gitoidHasher struct { + buf *bytes.Buffer + hash crypto.Hash +} + +// Write implments the io.Writer interface, and writes to the internal buffer +func (gh *gitoidHasher) Write(p []byte) (n int, err error) { + return gh.buf.Write(p) +} + +// Sum appends the current hash to b and returns the resulting slice. +// It does not change the underlying hash state. +func (gh *gitoidHasher) Sum(b []byte) []byte { + opts := []gitoid.Option{} + if gh.hash == crypto.SHA256 { + opts = append(opts, gitoid.WithSha256()) + } + + g, err := gitoid.New(gh.buf, opts...) + if err != nil { + return []byte{} + } + + return append(b, []byte(g.URI())...) +} + +// Reset resets the Hash to its initial state. +func (gh *gitoidHasher) Reset() { + gh.buf = &bytes.Buffer{} +} + +// Size returns the number of bytes Sum will return. +func (gh *gitoidHasher) Size() int { + hashName, err := HashToString(gh.hash) + if err != nil { + return 0 + } + + // this is somewhat fragile and knows too much about the internals of the gitoid code... + // we're assuming that the default gitoid content type will remain BLOB, and that our + // string representations of hash functions will remain consistent with their... + // and that the URI format will remain consistent. + // this should probably be changed, and this entire thing could maybe be upstreamed to the + // gitoid library. + return len(fmt.Sprintf("gitoid:%s:%s:", gitoid.BLOB, hashName)) + hex.EncodedLen(gh.hash.Size()) +} + +// BlockSize returns the hash's underlying block size. +// The Write method must be able to accept any amount +// of data, but it may operate more efficiently if all writes +// are a multiple of the block size. +func (gh *gitoidHasher) BlockSize() int { + hf := gh.hash.New() + return hf.BlockSize() +} diff --git a/vendor/github.com/in-toto/go-witness/cryptoutil/rsa.go b/vendor/github.com/in-toto/go-witness/cryptoutil/rsa.go new file mode 100644 index 0000000000..a3d617fcd4 --- /dev/null +++ b/vendor/github.com/in-toto/go-witness/cryptoutil/rsa.go @@ -0,0 +1,89 @@ +// Copyright 2021 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 cryptoutil + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "io" +) + +type RSASigner struct { + priv *rsa.PrivateKey + hash crypto.Hash +} + +func NewRSASigner(priv *rsa.PrivateKey, hash crypto.Hash) *RSASigner { + return &RSASigner{priv, hash} +} + +func (s *RSASigner) KeyID() (string, error) { + return GeneratePublicKeyID(&s.priv.PublicKey, s.hash) +} + +func (s *RSASigner) Sign(r io.Reader) ([]byte, error) { + digest, err := Digest(r, s.hash) + if err != nil { + return nil, err + } + + opts := &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + Hash: s.hash, + } + + return rsa.SignPSS(rand.Reader, s.priv, s.hash, digest, opts) +} + +func (s *RSASigner) Verifier() (Verifier, error) { + return NewRSAVerifier(&s.priv.PublicKey, s.hash), nil +} + +type RSAVerifier struct { + pub *rsa.PublicKey + hash crypto.Hash +} + +func NewRSAVerifier(pub *rsa.PublicKey, hash crypto.Hash) *RSAVerifier { + return &RSAVerifier{pub, hash} +} + +func (v *RSAVerifier) KeyID() (string, error) { + return GeneratePublicKeyID(v.pub, v.hash) +} + +func (v *RSAVerifier) Verify(data io.Reader, sig []byte) error { + digest, err := Digest(data, v.hash) + if err != nil { + return err + } + + pssOpts := &rsa.PSSOptions{ + SaltLength: rsa.PSSSaltLengthAuto, + Hash: v.hash, + } + + // AWS KMS introduces the chance that attestations get signed by PKCS1v15 instead of PSS + if err := rsa.VerifyPSS(v.pub, v.hash, digest, sig, pssOpts); err != nil { + return rsa.VerifyPKCS1v15(v.pub, v.hash, digest, sig) + } + + return nil +} + +func (v *RSAVerifier) Bytes() ([]byte, error) { + return PublicPemBytes(v.pub) +} diff --git a/vendor/github.com/in-toto/go-witness/cryptoutil/signer.go b/vendor/github.com/in-toto/go-witness/cryptoutil/signer.go new file mode 100644 index 0000000000..7c9dfbcaa2 --- /dev/null +++ b/vendor/github.com/in-toto/go-witness/cryptoutil/signer.go @@ -0,0 +1,121 @@ +// Copyright 2021 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 cryptoutil + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + "fmt" + "io" +) + +type ErrUnsupportedKeyType struct { + t string +} + +func (e ErrUnsupportedKeyType) Error() string { + return fmt.Sprintf("unsupported signer key type: %v", e.t) +} + +type Signer interface { + KeyIdentifier + Sign(r io.Reader) ([]byte, error) + Verifier() (Verifier, error) +} + +type KeyIdentifier interface { + KeyID() (string, error) +} + +type TrustBundler interface { + Certificate() *x509.Certificate + Intermediates() []*x509.Certificate + Roots() []*x509.Certificate +} + +type SignerOption func(*signerOptions) + +type signerOptions struct { + cert *x509.Certificate + intermediates []*x509.Certificate + roots []*x509.Certificate + hash crypto.Hash +} + +func SignWithCertificate(cert *x509.Certificate) SignerOption { + return func(so *signerOptions) { + so.cert = cert + } +} + +func SignWithIntermediates(intermediates []*x509.Certificate) SignerOption { + return func(so *signerOptions) { + so.intermediates = intermediates + } +} + +func SignWithRoots(roots []*x509.Certificate) SignerOption { + return func(so *signerOptions) { + so.roots = roots + } +} + +func SignWithHash(h crypto.Hash) SignerOption { + return func(so *signerOptions) { + so.hash = h + } +} + +func NewSigner(priv interface{}, opts ...SignerOption) (Signer, error) { + options := &signerOptions{ + hash: crypto.SHA256, + } + + for _, opt := range opts { + opt(options) + } + + var signer Signer + switch key := priv.(type) { + case *rsa.PrivateKey: + signer = NewRSASigner(key, options.hash) + case *ecdsa.PrivateKey: + signer = NewECDSASigner(key, options.hash) + case ed25519.PrivateKey: + signer = NewED25519Signer(key) + default: + return nil, ErrUnsupportedKeyType{ + t: fmt.Sprintf("%T", priv), + } + } + + if options.cert != nil { + return NewX509Signer(signer, options.cert, options.intermediates, options.roots) + } + + return signer, nil +} + +func NewSignerFromReader(r io.Reader, opts ...SignerOption) (Signer, error) { + key, err := TryParseKeyFromReader(r) + if err != nil { + return nil, err + } + + return NewSigner(key, opts...) +} diff --git a/vendor/github.com/in-toto/go-witness/cryptoutil/util.go b/vendor/github.com/in-toto/go-witness/cryptoutil/util.go new file mode 100644 index 0000000000..2b96280be9 --- /dev/null +++ b/vendor/github.com/in-toto/go-witness/cryptoutil/util.go @@ -0,0 +1,201 @@ +// Copyright 2021 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 cryptoutil + +import ( + "bytes" + "crypto" + "crypto/x509" + "encoding/hex" + "encoding/pem" + "errors" + "fmt" + "io" +) + +// PEMType is a specific type for string constants used during PEM encoding and decoding +type PEMType string + +const ( + // PublicKeyPEMType is the string "PUBLIC KEY" to be used during PEM encoding and decoding + PublicKeyPEMType PEMType = "PUBLIC KEY" + // PKCS1PublicKeyPEMType is the string "RSA PUBLIC KEY" used to parse PKCS#1-encoded public keys + PKCS1PublicKeyPEMType PEMType = "RSA PUBLIC KEY" +) + +type ErrUnsupportedPEM struct { + t string +} + +func (e ErrUnsupportedPEM) Error() string { + return fmt.Sprintf("unsupported pem type: %v", e.t) +} + +type ErrInvalidPemBlock struct{} + +func (e ErrInvalidPemBlock) Error() string { + return "invalid pem block" +} + +func DigestBytes(data []byte, hash crypto.Hash) ([]byte, error) { + return Digest(bytes.NewReader(data), hash) +} + +func Digest(r io.Reader, hash crypto.Hash) ([]byte, error) { + hashFunc := hash.New() + if _, err := io.Copy(hashFunc, r); err != nil { + return nil, err + } + + return hashFunc.Sum(nil), nil +} + +func HexEncode(src []byte) []byte { + dst := make([]byte, hex.EncodedLen(len(src))) + hex.Encode(dst, src) + return dst +} + +func GeneratePublicKeyID(pub interface{}, hash crypto.Hash) (string, error) { + pemBytes, err := PublicPemBytes(pub) + if err != nil { + return "", err + } + + digest, err := DigestBytes(pemBytes, hash) + if err != nil { + return "", err + } + + return string(HexEncode(digest)), nil +} + +func PublicPemBytes(pub interface{}) ([]byte, error) { + keyBytes, err := x509.MarshalPKIXPublicKey(pub) + if err != nil { + return nil, err + } + + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "PUBLIC KEY", Bytes: keyBytes}) + if err != nil { + return nil, err + } + + return pemBytes, err +} + +// UnmarshalPEMToPublicKey converts a PEM-encoded byte slice into a crypto.PublicKey +func UnmarshalPEMToPublicKey(pemBytes []byte) (crypto.PublicKey, error) { + derBytes, _ := pem.Decode(pemBytes) + if derBytes == nil { + return nil, errors.New("PEM decoding failed") + } + switch derBytes.Type { + case string(PublicKeyPEMType): + return x509.ParsePKIXPublicKey(derBytes.Bytes) + case string(PKCS1PublicKeyPEMType): + return x509.ParsePKCS1PublicKey(derBytes.Bytes) + default: + return nil, fmt.Errorf("unknown Public key PEM file type: %v. Are you passing the correct public key?", + derBytes.Type) + } +} + +func TryParsePEMBlock(block *pem.Block) (interface{}, error) { + if block == nil { + return nil, ErrInvalidPemBlock{} + } + + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err == nil { + return key, err + } + + key, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err == nil { + return key, err + } + + key, err = x509.ParseECPrivateKey(block.Bytes) + if err == nil { + return key, err + } + + key, err = x509.ParsePKIXPublicKey(block.Bytes) + if err == nil { + return key, err + } + + key, err = x509.ParsePKCS1PublicKey(block.Bytes) + if err == nil { + return key, err + } + + key, err = x509.ParseCertificate(block.Bytes) + if err == nil { + return key, err + } + + return nil, ErrUnsupportedPEM{block.Type} +} + +func TryParseKeyFromReader(r io.Reader) (interface{}, error) { + bytes, err := io.ReadAll(r) + if err != nil { + return nil, err + } + + // we may want to handle files with multiple pem blocks in them, but for now... + pemBlock, _ := pem.Decode(bytes) + return TryParsePEMBlock(pemBlock) +} + +func TryParseCertificate(data []byte) (*x509.Certificate, error) { + possibleCert, err := TryParseKeyFromReader(bytes.NewReader(data)) + if err != nil { + return nil, err + } + + cert, ok := possibleCert.(*x509.Certificate) + if !ok { + return nil, fmt.Errorf("data was a valid verifier but not a certificate") + } + + return cert, nil +} + +// ComputeDigest calculates the digest value for the specified message using the supplied hash function +func ComputeDigest(rawMessage io.Reader, hashFunc crypto.Hash, supportedHashFuncs []crypto.Hash) ([]byte, crypto.Hash, error) { + var cryptoSignerOpts crypto.SignerOpts = hashFunc + hashedWith := cryptoSignerOpts.HashFunc() + if !isSupportedAlg(hashedWith, supportedHashFuncs) { + return nil, crypto.Hash(0), fmt.Errorf("unsupported hash algorithm: %q not in %v", hashedWith.String(), supportedHashFuncs) + } + + digest, err := Digest(rawMessage, hashedWith) + return digest, hashedWith, err +} + +func isSupportedAlg(alg crypto.Hash, supportedAlgs []crypto.Hash) bool { + if supportedAlgs == nil { + return true + } + for _, supportedAlg := range supportedAlgs { + if alg == supportedAlg { + return true + } + } + return false +} diff --git a/vendor/github.com/in-toto/go-witness/cryptoutil/verifier.go b/vendor/github.com/in-toto/go-witness/cryptoutil/verifier.go new file mode 100644 index 0000000000..b243e286ec --- /dev/null +++ b/vendor/github.com/in-toto/go-witness/cryptoutil/verifier.go @@ -0,0 +1,99 @@ +// Copyright 2021 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 cryptoutil + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" + "crypto/x509" + "fmt" + "io" + "time" +) + +type Verifier interface { + KeyIdentifier + Verify(body io.Reader, sig []byte) error + Bytes() ([]byte, error) +} + +type VerifierOption func(*verifierOptions) + +type verifierOptions struct { + roots []*x509.Certificate + intermediates []*x509.Certificate + hash crypto.Hash + trustedTime time.Time +} + +func VerifyWithRoots(roots []*x509.Certificate) VerifierOption { + return func(vo *verifierOptions) { + vo.roots = roots + } +} + +func VerifyWithIntermediates(intermediates []*x509.Certificate) VerifierOption { + return func(vo *verifierOptions) { + vo.intermediates = intermediates + } +} + +func VerifyWithHash(h crypto.Hash) VerifierOption { + return func(vo *verifierOptions) { + vo.hash = h + } +} + +func VerifyWithTrustedTime(t time.Time) VerifierOption { + return func(vo *verifierOptions) { + vo.trustedTime = t + } +} + +func NewVerifier(pub interface{}, opts ...VerifierOption) (Verifier, error) { + options := &verifierOptions{ + hash: crypto.SHA256, + } + + for _, opt := range opts { + opt(options) + } + + switch key := pub.(type) { + case *rsa.PublicKey: + return NewRSAVerifier(key, options.hash), nil + case *ecdsa.PublicKey: + return NewECDSAVerifier(key, options.hash), nil + case ed25519.PublicKey: + return NewED25519Verifier(key), nil + case *x509.Certificate: + return NewX509Verifier(key, options.intermediates, options.roots, options.trustedTime) + default: + return nil, ErrUnsupportedKeyType{ + t: fmt.Sprintf("%T", pub), + } + } +} + +func NewVerifierFromReader(r io.Reader, opts ...VerifierOption) (Verifier, error) { + key, err := TryParseKeyFromReader(r) + if err != nil { + return nil, err + } + + return NewVerifier(key, opts...) +} diff --git a/vendor/github.com/in-toto/go-witness/cryptoutil/x509.go b/vendor/github.com/in-toto/go-witness/cryptoutil/x509.go new file mode 100644 index 0000000000..4bf2217c59 --- /dev/null +++ b/vendor/github.com/in-toto/go-witness/cryptoutil/x509.go @@ -0,0 +1,173 @@ +// Copyright 2021 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 cryptoutil + +import ( + "crypto/x509" + "encoding/pem" + "io" + "time" +) + +type X509Verifier struct { + cert *x509.Certificate + roots []*x509.Certificate + intermediates []*x509.Certificate + verifier Verifier + trustedTime time.Time +} + +func NewX509Verifier(cert *x509.Certificate, intermediates, roots []*x509.Certificate, trustedTime time.Time) (*X509Verifier, error) { + verifier, err := NewVerifier(cert.PublicKey) + if err != nil { + return nil, err + } + + return &X509Verifier{ + cert: cert, + roots: roots, + intermediates: intermediates, + verifier: verifier, + trustedTime: trustedTime, + }, nil +} + +func (v *X509Verifier) KeyID() (string, error) { + return v.verifier.KeyID() +} + +func (v *X509Verifier) Verify(body io.Reader, sig []byte) error { + rootPool := certificatesToPool(v.roots) + intermediatePool := certificatesToPool(v.intermediates) + if _, err := v.cert.Verify(x509.VerifyOptions{ + CurrentTime: v.trustedTime, + Roots: rootPool, + Intermediates: intermediatePool, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + }); err != nil { + return err + } + + return v.verifier.Verify(body, sig) +} + +func (v *X509Verifier) BelongsToRoot(root *x509.Certificate) error { + rootPool := certificatesToPool([]*x509.Certificate{root}) + intermediatePool := certificatesToPool(v.intermediates) + _, err := v.cert.Verify(x509.VerifyOptions{ + Roots: rootPool, + Intermediates: intermediatePool, + CurrentTime: v.trustedTime, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + }) + + return err +} + +func (v *X509Verifier) Bytes() ([]byte, error) { + pemBytes := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: v.cert.Raw}) + return pemBytes, nil +} + +func (v *X509Verifier) Certificate() *x509.Certificate { + return v.cert +} + +func (v *X509Verifier) Intermediates() []*x509.Certificate { + return v.intermediates +} + +func (v *X509Verifier) Roots() []*x509.Certificate { + return v.roots +} + +type X509Signer struct { + cert *x509.Certificate + roots []*x509.Certificate + intermediates []*x509.Certificate + signer Signer +} + +type ErrInvalidSigner struct{} + +func (e ErrInvalidSigner) Error() string { + return "signer must not be nil" +} + +type ErrInvalidCertificate struct{} + +func (e ErrInvalidCertificate) Error() string { + return "certificate must not be nil" +} + +func NewX509Signer(signer Signer, cert *x509.Certificate, intermediates, roots []*x509.Certificate) (*X509Signer, error) { + if signer == nil { + return nil, ErrInvalidSigner{} + } + + if cert == nil { + return nil, ErrInvalidCertificate{} + } + + return &X509Signer{ + signer: signer, + cert: cert, + roots: roots, + intermediates: intermediates, + }, nil +} + +func (s *X509Signer) KeyID() (string, error) { + return s.signer.KeyID() +} + +func (s *X509Signer) Sign(r io.Reader) ([]byte, error) { + return s.signer.Sign(r) +} + +func (s *X509Signer) Verifier() (Verifier, error) { + verifier, err := s.signer.Verifier() + if err != nil { + return nil, err + } + + return &X509Verifier{ + verifier: verifier, + cert: s.cert, + roots: s.roots, + intermediates: s.intermediates, + }, nil +} + +func (s *X509Signer) Certificate() *x509.Certificate { + return s.cert +} + +func (s *X509Signer) Intermediates() []*x509.Certificate { + return s.intermediates +} + +func (s *X509Signer) Roots() []*x509.Certificate { + return s.roots +} + +func certificatesToPool(certs []*x509.Certificate) *x509.CertPool { + pool := x509.NewCertPool() + for _, cert := range certs { + pool.AddCert(cert) + } + + return pool +} diff --git a/vendor/github.com/in-toto/go-witness/dsse/dsse.go b/vendor/github.com/in-toto/go-witness/dsse/dsse.go new file mode 100644 index 0000000000..81d13e2249 --- /dev/null +++ b/vendor/github.com/in-toto/go-witness/dsse/dsse.go @@ -0,0 +1,96 @@ +// Copyright 2021 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 dsse + +import ( + "fmt" + + "github.com/in-toto/go-witness/log" +) + +type ErrNoSignatures struct{} + +func (e ErrNoSignatures) Error() string { + return "no signatures in dsse envelope" +} + +type ErrNoMatchingSigs struct { + Verifiers []CheckedVerifier +} + +func (e ErrNoMatchingSigs) Error() string { + mess := "no valid signatures for the provided verifiers found for keyids:\n" + for _, v := range e.Verifiers { + if v.Error != nil { + kid, err := v.Verifier.KeyID() + if err != nil { + log.Warnf("failed to get key id from verifier: %w", err) + } + + s := fmt.Sprintf(" %s: %v\n", kid, v.Error) + mess += s + } + } + + return mess +} + +type ErrThresholdNotMet struct { + Theshold int + Actual int +} + +func (e ErrThresholdNotMet) Error() string { + return fmt.Sprintf("envelope did not meet verifier threshold. expected %v valid verifiers but got %v", e.Theshold, e.Actual) +} + +type ErrInvalidThreshold int + +func (e ErrInvalidThreshold) Error() string { + return fmt.Sprintf("invalid threshold (%v). thresholds must be greater than 0", int(e)) +} + +const PemTypeCertificate = "CERTIFICATE" + +type Envelope struct { + Payload []byte `json:"payload"` + PayloadType string `json:"payloadType"` + Signatures []Signature `json:"signatures"` +} + +type Signature struct { + KeyID string `json:"keyid"` + Signature []byte `json:"sig"` + Certificate []byte `json:"certificate,omitempty"` + Intermediates [][]byte `json:"intermediates,omitempty"` + Timestamps []SignatureTimestamp `json:"timestamps,omitempty"` +} + +type SignatureTimestampType string + +const TimestampRFC3161 SignatureTimestampType = "tsp" + +type SignatureTimestamp struct { + Type SignatureTimestampType `json:"type"` + Data []byte `json:"data"` +} + +// preauthEncode wraps the data to be signed or verified and it's type in the DSSE protocol's +// pre-authentication encoding as detailed at https://github.com/secure-systems-lab/dsse/blob/master/protocol.md +// PAE(type, body) = "DSSEv1" + SP + LEN(type) + SP + type + SP + LEN(body) + SP + body +func preauthEncode(bodyType string, body []byte) []byte { + const dsseVersion = "DSSEv1" + return []byte(fmt.Sprintf("%s %d %s %d %s", dsseVersion, len(bodyType), bodyType, len(body), body)) +} diff --git a/vendor/github.com/in-toto/go-witness/dsse/sign.go b/vendor/github.com/in-toto/go-witness/dsse/sign.go new file mode 100644 index 0000000000..267ec079b3 --- /dev/null +++ b/vendor/github.com/in-toto/go-witness/dsse/sign.go @@ -0,0 +1,115 @@ +// Copyright 2022 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 dsse + +import ( + "bytes" + "context" + "encoding/pem" + "fmt" + "io" + + "github.com/in-toto/go-witness/cryptoutil" + "github.com/in-toto/go-witness/timestamp" +) + +type signOptions struct { + signers []cryptoutil.Signer + timestampers []timestamp.Timestamper +} + +type SignOption func(*signOptions) + +func SignWithSigners(signers ...cryptoutil.Signer) SignOption { + return func(so *signOptions) { + so.signers = signers + } +} + +func SignWithTimestampers(timestampers ...timestamp.Timestamper) SignOption { + return func(so *signOptions) { + so.timestampers = timestampers + } +} + +func Sign(bodyType string, body io.Reader, opts ...SignOption) (Envelope, error) { + so := &signOptions{} + env := Envelope{} + for _, opt := range opts { + opt(so) + } + + if len(so.signers) == 0 { + return env, fmt.Errorf("must have at least one signer, have %v", len(so.signers)) + } + + bodyBytes, err := io.ReadAll(body) + if err != nil { + return env, err + } + + env.PayloadType = bodyType + env.Payload = bodyBytes + env.Signatures = make([]Signature, 0) + pae := preauthEncode(bodyType, bodyBytes) + for _, signer := range so.signers { + if signer == nil { + continue + } + + sig, err := signer.Sign(bytes.NewReader(pae)) + if err != nil { + return env, err + } + + keyID, err := signer.KeyID() + if err != nil { + return env, err + } + + dsseSig := Signature{ + KeyID: keyID, + Signature: sig, + } + + for _, timestamper := range so.timestampers { + timestamp, err := timestamper.Timestamp(context.TODO(), bytes.NewReader(sig)) + if err != nil { + return env, err + } + + dsseSig.Timestamps = append(dsseSig.Timestamps, SignatureTimestamp{ + Type: TimestampRFC3161, + Data: timestamp, + }) + } + + if trustBundler, ok := signer.(cryptoutil.TrustBundler); ok { + leaf := trustBundler.Certificate() + intermediates := trustBundler.Intermediates() + if leaf != nil { + dsseSig.Certificate = pem.EncodeToMemory(&pem.Block{Type: PemTypeCertificate, Bytes: leaf.Raw}) + } + + for _, intermediate := range intermediates { + dsseSig.Intermediates = append(dsseSig.Intermediates, pem.EncodeToMemory(&pem.Block{Type: PemTypeCertificate, Bytes: intermediate.Raw})) + } + } + + env.Signatures = append(env.Signatures, dsseSig) + } + + return env, nil +} diff --git a/vendor/github.com/in-toto/go-witness/dsse/verify.go b/vendor/github.com/in-toto/go-witness/dsse/verify.go new file mode 100644 index 0000000000..9c94a7446c --- /dev/null +++ b/vendor/github.com/in-toto/go-witness/dsse/verify.go @@ -0,0 +1,201 @@ +// Copyright 2022 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 dsse + +import ( + "bytes" + "context" + "crypto/x509" + "fmt" + "time" + + "github.com/in-toto/go-witness/cryptoutil" + "github.com/in-toto/go-witness/log" + "github.com/in-toto/go-witness/timestamp" +) + +type verificationOptions struct { + roots []*x509.Certificate + intermediates []*x509.Certificate + verifiers []cryptoutil.Verifier + threshold int + timestampVerifiers []timestamp.TimestampVerifier +} + +type VerificationOption func(*verificationOptions) + +func VerifyWithRoots(roots ...*x509.Certificate) VerificationOption { + return func(vo *verificationOptions) { + vo.roots = roots + } +} + +func VerifyWithIntermediates(intermediates ...*x509.Certificate) VerificationOption { + return func(vo *verificationOptions) { + vo.intermediates = intermediates + } +} + +func VerifyWithVerifiers(verifiers ...cryptoutil.Verifier) VerificationOption { + return func(vo *verificationOptions) { + vo.verifiers = verifiers + } +} + +func VerifyWithThreshold(threshold int) VerificationOption { + return func(vo *verificationOptions) { + vo.threshold = threshold + } +} + +func VerifyWithTimestampVerifiers(verifiers ...timestamp.TimestampVerifier) VerificationOption { + return func(vo *verificationOptions) { + vo.timestampVerifiers = verifiers + } +} + +type CheckedVerifier struct { + Verifier cryptoutil.Verifier + TimestampVerifiers []timestamp.TimestampVerifier + Error error +} + +func (e Envelope) Verify(opts ...VerificationOption) ([]CheckedVerifier, error) { + options := &verificationOptions{ + threshold: 1, + } + + for _, opt := range opts { + opt(options) + } + + if options.threshold <= 0 { + return nil, ErrInvalidThreshold(options.threshold) + } + + pae := preauthEncode(e.PayloadType, e.Payload) + if len(e.Signatures) == 0 { + return nil, ErrNoSignatures{} + } + + checkedVerifiers := make([]CheckedVerifier, 0) + verified := 0 + for _, sig := range e.Signatures { + if len(sig.Certificate) > 0 { + cert, err := cryptoutil.TryParseCertificate(sig.Certificate) + if err != nil { + continue + } + + sigIntermediates := make([]*x509.Certificate, 0) + for _, int := range sig.Intermediates { + intCert, err := cryptoutil.TryParseCertificate(int) + if err != nil { + continue + } + + sigIntermediates = append(sigIntermediates, intCert) + } + + sigIntermediates = append(sigIntermediates, options.intermediates...) + if len(options.timestampVerifiers) == 0 { + if verifier, err := verifyX509Time(cert, sigIntermediates, options.roots, pae, sig.Signature, time.Now()); err == nil { + checkedVerifiers = append(checkedVerifiers, CheckedVerifier{Verifier: verifier}) + verified += 1 + } else { + checkedVerifiers = append(checkedVerifiers, CheckedVerifier{Verifier: verifier, Error: err}) + log.Debugf("failed to verify with timestamp verifier: %w", err) + } + } else { + var passedVerifier cryptoutil.Verifier + failed := []cryptoutil.Verifier{} + passedTimestampVerifiers := []timestamp.TimestampVerifier{} + failedTimestampVerifiers := []timestamp.TimestampVerifier{} + + for _, timestampVerifier := range options.timestampVerifiers { + for _, sigTimestamp := range sig.Timestamps { + timestamp, err := timestampVerifier.Verify(context.TODO(), bytes.NewReader(sigTimestamp.Data), bytes.NewReader(sig.Signature)) + if err != nil { + continue + } + + if verifier, err := verifyX509Time(cert, sigIntermediates, options.roots, pae, sig.Signature, timestamp); err == nil { + // NOTE: do we not want to save all the passed verifiers? + passedVerifier = verifier + passedTimestampVerifiers = append(passedTimestampVerifiers, timestampVerifier) + } else { + failed = append(failed, verifier) + failedTimestampVerifiers = append(failedTimestampVerifiers, timestampVerifier) + log.Debugf("failed to verify with timestamp verifier: %w", err) + } + + } + } + + if len(passedTimestampVerifiers) > 0 { + verified += 1 + checkedVerifiers = append(checkedVerifiers, CheckedVerifier{ + Verifier: passedVerifier, + TimestampVerifiers: passedTimestampVerifiers, + }) + } else { + for _, v := range failed { + checkedVerifiers = append(checkedVerifiers, CheckedVerifier{ + Verifier: v, + TimestampVerifiers: failedTimestampVerifiers, + Error: fmt.Errorf("no valid timestamps found"), + }) + } + } + } + } + + for _, verifier := range options.verifiers { + if verifier != nil { + kid, err := verifier.KeyID() + if err != nil { + log.Warn("failed to get key id from verifier: %v", err) + } + log.Debug("verifying with verifier with KeyID ", kid) + + if err := verifier.Verify(bytes.NewReader(pae), sig.Signature); err == nil { + verified += 1 + checkedVerifiers = append(checkedVerifiers, CheckedVerifier{Verifier: verifier}) + } else { + checkedVerifiers = append(checkedVerifiers, CheckedVerifier{Verifier: verifier, Error: err}) + } + } + } + } + + if verified == 0 { + return nil, ErrNoMatchingSigs{Verifiers: checkedVerifiers} + } else if verified < options.threshold { + return checkedVerifiers, ErrThresholdNotMet{Theshold: options.threshold, Actual: verified} + } + + return checkedVerifiers, nil +} + +func verifyX509Time(cert *x509.Certificate, sigIntermediates, roots []*x509.Certificate, pae, sig []byte, trustedTime time.Time) (cryptoutil.Verifier, error) { + verifier, err := cryptoutil.NewX509Verifier(cert, sigIntermediates, roots, trustedTime) + if err != nil { + return nil, err + } + + err = verifier.Verify(bytes.NewReader(pae), sig) + + return verifier, err +} diff --git a/vendor/github.com/in-toto/go-witness/log/log.go b/vendor/github.com/in-toto/go-witness/log/log.go new file mode 100644 index 0000000000..31396dc17a --- /dev/null +++ b/vendor/github.com/in-toto/go-witness/log/log.go @@ -0,0 +1,94 @@ +// Copyright 2022 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 log + +import ( + "fmt" +) + +var log Logger = SilentLogger{} + +// Logger is used by witness library code to print out relevant information at runtime. +type Logger interface { + Errorf(format string, args ...interface{}) + Error(args ...interface{}) + Warnf(format string, args ...interface{}) + Warn(args ...interface{}) + Debugf(format string, args ...interface{}) + Debug(args ...interface{}) + Infof(format string, args ...interface{}) + Info(args ...interface{}) +} + +// SetLogger will set the Logger instance that all Witness library code will use as logging output. +// The default is a SilentLogger that will output nothing. +func SetLogger(l Logger) { + log = l +} + +// GetLogger returns the Logger instance currently being used by Witness library code. +func GetLogger() Logger { + return log +} + +func Errorf(format string, args ...interface{}) { + err := fmt.Errorf(format, args...) + log.Error(err) +} + +func Error(args ...interface{}) { + log.Error(args...) +} + +func Warnf(format string, args ...interface{}) { + // We want to wrap the error if there is one. + for _, a := range args { + if _, ok := a.(error); ok { + err := fmt.Errorf(format, args...) + log.Warn(err) + return + } + } + + log.Warnf(format, args...) +} + +func Warn(args ...interface{}) { + log.Warn(args...) +} + +func Debugf(format string, args ...interface{}) { + for _, a := range args { + if _, ok := a.(error); ok { + err := fmt.Errorf(format, args...) + log.Debug(err) + return + } + } + + log.Debugf(format, args...) +} + +func Debug(args ...interface{}) { + log.Debug(args...) +} + +func Infof(format string, args ...interface{}) { + log.Infof(format, args...) +} + +func Info(args ...interface{}) { + log.Info(args...) +} diff --git a/vendor/github.com/in-toto/go-witness/log/silent.go b/vendor/github.com/in-toto/go-witness/log/silent.go new file mode 100644 index 0000000000..000236c064 --- /dev/null +++ b/vendor/github.com/in-toto/go-witness/log/silent.go @@ -0,0 +1,30 @@ +// Copyright 2022 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 log + +// SilentLogger is an implementation of the Logger interface that suppresses +// all logging output. This is the default logger when using Witness as a +// library, so that we don't interfere with the caller's stdout/stderr. Callers +// should supply their own Logger to capture Witness logging if desired. +type SilentLogger struct{} + +func (l SilentLogger) Errorf(format string, args ...interface{}) {} +func (l SilentLogger) Error(args ...interface{}) {} +func (l SilentLogger) Warnf(format string, args ...interface{}) {} +func (l SilentLogger) Warn(args ...interface{}) {} +func (l SilentLogger) Debugf(format string, args ...interface{}) {} +func (l SilentLogger) Debug(args ...interface{}) {} +func (l SilentLogger) Infof(format string, args ...interface{}) {} +func (l SilentLogger) Info(args ...interface{}) {} diff --git a/vendor/github.com/in-toto/go-witness/timestamp/fake.go b/vendor/github.com/in-toto/go-witness/timestamp/fake.go new file mode 100644 index 0000000000..1d50954683 --- /dev/null +++ b/vendor/github.com/in-toto/go-witness/timestamp/fake.go @@ -0,0 +1,43 @@ +// Copyright 2022 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 timestamp + +import ( + "context" + "fmt" + "io" + "time" +) + +type FakeTimestamper struct { + T time.Time +} + +func (ft FakeTimestamper) Timestamp(context.Context, io.Reader) ([]byte, error) { + return []byte(ft.T.Format(time.RFC3339)), nil +} + +func (ft FakeTimestamper) Verify(ctx context.Context, ts io.Reader, sig io.Reader) (time.Time, error) { + b, err := io.ReadAll(ts) + if err != nil { + return time.Time{}, err + } + + if string(b) != ft.T.Format(time.RFC3339) { + return time.Time{}, fmt.Errorf("mismatched time") + } + + return ft.T, nil +} diff --git a/vendor/github.com/in-toto/go-witness/timestamp/timestamp.go b/vendor/github.com/in-toto/go-witness/timestamp/timestamp.go new file mode 100644 index 0000000000..6408190056 --- /dev/null +++ b/vendor/github.com/in-toto/go-witness/timestamp/timestamp.go @@ -0,0 +1,29 @@ +// Copyright 2022 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 timestamp + +import ( + "context" + "io" + "time" +) + +type TimestampVerifier interface { + Verify(context.Context, io.Reader, io.Reader) (time.Time, error) +} + +type Timestamper interface { + Timestamp(context.Context, io.Reader) ([]byte, error) +} diff --git a/vendor/github.com/in-toto/go-witness/timestamp/tsp.go b/vendor/github.com/in-toto/go-witness/timestamp/tsp.go new file mode 100644 index 0000000000..e8a1e596d4 --- /dev/null +++ b/vendor/github.com/in-toto/go-witness/timestamp/tsp.go @@ -0,0 +1,176 @@ +// Copyright 2022 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 timestamp + +import ( + "bytes" + "context" + "crypto" + "crypto/x509" + "fmt" + "io" + "net/http" + "time" + + "github.com/digitorus/pkcs7" + "github.com/digitorus/timestamp" + "github.com/in-toto/go-witness/cryptoutil" +) + +type TSPTimestamper struct { + url string + hash crypto.Hash + requestCertificate bool +} + +type TSPTimestamperOption func(*TSPTimestamper) + +func TimestampWithUrl(url string) TSPTimestamperOption { + return func(t *TSPTimestamper) { + t.url = url + } +} + +func TimestampWithHash(h crypto.Hash) TSPTimestamperOption { + return func(t *TSPTimestamper) { + t.hash = h + } +} + +func TimestampWithRequestCertificate(requestCertificate bool) TSPTimestamperOption { + return func(t *TSPTimestamper) { + t.requestCertificate = requestCertificate + } +} + +func NewTimestamper(opts ...TSPTimestamperOption) TSPTimestamper { + t := TSPTimestamper{ + hash: crypto.SHA256, + requestCertificate: true, + } + + for _, opt := range opts { + opt(&t) + } + + return t +} + +func (t TSPTimestamper) Timestamp(ctx context.Context, r io.Reader) ([]byte, error) { + tsq, err := timestamp.CreateRequest(r, ×tamp.RequestOptions{ + Hash: t.hash, + Certificates: t.requestCertificate, + }) + if err != nil { + return nil, err + } + + req, err := http.NewRequestWithContext(ctx, "POST", t.url, bytes.NewReader(tsq)) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", "application/timestamp-query") + client := http.Client{} + resp, err := client.Do(req) + if err != nil { + return nil, err + } + + switch resp.StatusCode { + case http.StatusOK, http.StatusCreated, http.StatusAccepted: + default: + return nil, fmt.Errorf("request to timestamp authority failed: %v", resp.Status) + } + + defer resp.Body.Close() + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + timestamp, err := timestamp.ParseResponse(bodyBytes) + if err != nil { + return nil, err + } + + return timestamp.RawToken, nil +} + +type TSPVerifier struct { + certChain *x509.CertPool + hash crypto.Hash +} + +type TSPVerifierOption func(*TSPVerifier) + +func VerifyWithCerts(certs []*x509.Certificate) TSPVerifierOption { + return func(t *TSPVerifier) { + t.certChain = x509.NewCertPool() + for _, cert := range certs { + t.certChain.AddCert(cert) + } + } +} + +func VerifyWithHash(h crypto.Hash) TSPVerifierOption { + return func(t *TSPVerifier) { + t.hash = h + } +} + +func NewVerifier(opts ...TSPVerifierOption) TSPVerifier { + v := TSPVerifier{ + hash: crypto.SHA256, + } + + for _, opt := range opts { + opt(&v) + } + + return v +} + +func (v TSPVerifier) Verify(ctx context.Context, tsrData, signedData io.Reader) (time.Time, error) { + tsrBytes, err := io.ReadAll(tsrData) + if err != nil { + return time.Time{}, err + } + + ts, err := timestamp.Parse(tsrBytes) + if err != nil { + return time.Time{}, err + } + + hashedData, err := cryptoutil.Digest(signedData, v.hash) + if err != nil { + return time.Time{}, err + } + + if !bytes.Equal(ts.HashedMessage, hashedData) { + return time.Time{}, fmt.Errorf("signed payload does not match timestamped payload") + } + + p7, err := pkcs7.Parse(tsrBytes) + if err != nil { + return time.Time{}, err + } + + if err := p7.VerifyWithChain(v.certChain); err != nil { + return time.Time{}, err + } + + return ts.Time, nil +} diff --git a/vendor/golang.org/x/mod/sumdb/dirhash/hash.go b/vendor/golang.org/x/mod/sumdb/dirhash/hash.go new file mode 100644 index 0000000000..51ec4db873 --- /dev/null +++ b/vendor/golang.org/x/mod/sumdb/dirhash/hash.go @@ -0,0 +1,135 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package dirhash defines hashes over directory trees. +// These hashes are recorded in go.sum files and in the Go checksum database, +// to allow verifying that a newly-downloaded module has the expected content. +package dirhash + +import ( + "archive/zip" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "strings" +) + +// DefaultHash is the default hash function used in new go.sum entries. +var DefaultHash Hash = Hash1 + +// A Hash is a directory hash function. +// It accepts a list of files along with a function that opens the content of each file. +// It opens, reads, hashes, and closes each file and returns the overall directory hash. +type Hash func(files []string, open func(string) (io.ReadCloser, error)) (string, error) + +// Hash1 is the "h1:" directory hash function, using SHA-256. +// +// Hash1 is "h1:" followed by the base64-encoded SHA-256 hash of a summary +// prepared as if by the Unix command: +// +// sha256sum $(find . -type f | sort) | sha256sum +// +// More precisely, the hashed summary contains a single line for each file in the list, +// ordered by sort.Strings applied to the file names, where each line consists of +// the hexadecimal SHA-256 hash of the file content, +// two spaces (U+0020), the file name, and a newline (U+000A). +// +// File names with newlines (U+000A) are disallowed. +func Hash1(files []string, open func(string) (io.ReadCloser, error)) (string, error) { + h := sha256.New() + files = append([]string(nil), files...) + sort.Strings(files) + for _, file := range files { + if strings.Contains(file, "\n") { + return "", errors.New("dirhash: filenames with newlines are not supported") + } + r, err := open(file) + if err != nil { + return "", err + } + hf := sha256.New() + _, err = io.Copy(hf, r) + r.Close() + if err != nil { + return "", err + } + fmt.Fprintf(h, "%x %s\n", hf.Sum(nil), file) + } + return "h1:" + base64.StdEncoding.EncodeToString(h.Sum(nil)), nil +} + +// HashDir returns the hash of the local file system directory dir, +// replacing the directory name itself with prefix in the file names +// used in the hash function. +func HashDir(dir, prefix string, hash Hash) (string, error) { + files, err := DirFiles(dir, prefix) + if err != nil { + return "", err + } + osOpen := func(name string) (io.ReadCloser, error) { + return os.Open(filepath.Join(dir, strings.TrimPrefix(name, prefix))) + } + return hash(files, osOpen) +} + +// DirFiles returns the list of files in the tree rooted at dir, +// replacing the directory name dir with prefix in each name. +// The resulting names always use forward slashes. +func DirFiles(dir, prefix string) ([]string, error) { + var files []string + dir = filepath.Clean(dir) + err := filepath.Walk(dir, func(file string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } else if file == dir { + return fmt.Errorf("%s is not a directory", dir) + } + + rel := file + if dir != "." { + rel = file[len(dir)+1:] + } + f := filepath.Join(prefix, rel) + files = append(files, filepath.ToSlash(f)) + return nil + }) + if err != nil { + return nil, err + } + return files, nil +} + +// HashZip returns the hash of the file content in the named zip file. +// Only the file names and their contents are included in the hash: +// the exact zip file format encoding, compression method, +// per-file modification times, and other metadata are ignored. +func HashZip(zipfile string, hash Hash) (string, error) { + z, err := zip.OpenReader(zipfile) + if err != nil { + return "", err + } + defer z.Close() + var files []string + zfiles := make(map[string]*zip.File) + for _, file := range z.File { + files = append(files, file.Name) + zfiles[file.Name] = file + } + zipOpen := func(name string) (io.ReadCloser, error) { + f := zfiles[name] + if f == nil { + return nil, fmt.Errorf("file %q not found in zip", name) // should never happen + } + return f.Open() + } + return hash(files, zipOpen) +} diff --git a/vendor/modules.txt b/vendor/modules.txt index ad09c51697..bbb7f711d4 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -705,6 +705,9 @@ github.com/eapache/go-xerial-snappy # github.com/eapache/queue v1.1.0 ## explicit github.com/eapache/queue +# github.com/edwarnicke/gitoid v0.0.0-20220710194850-1be5bfda1f9d +## explicit; go 1.18 +github.com/edwarnicke/gitoid # github.com/emicklei/go-restful/v3 v3.11.0 ## explicit; go 1.13 github.com/emicklei/go-restful/v3 @@ -1358,10 +1361,20 @@ github.com/hashicorp/vault/api github.com/hexops/gotextdiff github.com/hexops/gotextdiff/myers github.com/hexops/gotextdiff/span +# github.com/in-toto/archivista v0.9.0 +## explicit; go 1.23.0 +github.com/in-toto/archivista/pkg/api +github.com/in-toto/archivista/pkg/http-client # github.com/in-toto/attestation v1.1.1 ## explicit; go 1.20 github.com/in-toto/attestation/go/predicates/provenance/v1 github.com/in-toto/attestation/go/v1 +# github.com/in-toto/go-witness v0.7.0 +## explicit; go 1.22.8 +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/timestamp # github.com/in-toto/in-toto-golang v0.9.1-0.20240317085821-8e2966059a09 ## explicit; go 1.20 github.com/in-toto/in-toto-golang/in_toto @@ -2476,6 +2489,7 @@ golang.org/x/mod/internal/lazyregexp golang.org/x/mod/modfile golang.org/x/mod/module golang.org/x/mod/semver +golang.org/x/mod/sumdb/dirhash golang.org/x/mod/sumdb/note # golang.org/x/net v0.34.0 ## explicit; go 1.18