diff --git a/cmd/validate/image.go b/cmd/validate/image.go index eed29bf1c..61e6de650 100644 --- a/cmd/validate/image.go +++ b/cmd/validate/image.go @@ -35,6 +35,7 @@ import ( "github.com/enterprise-contract/ec-cli/internal/applicationsnapshot" "github.com/enterprise-contract/ec-cli/internal/evaluator" "github.com/enterprise-contract/ec-cli/internal/format" + "github.com/enterprise-contract/ec-cli/internal/image" "github.com/enterprise-contract/ec-cli/internal/output" "github.com/enterprise-contract/ec-cli/internal/policy" "github.com/enterprise-contract/ec-cli/internal/policy/source" @@ -73,6 +74,7 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command { noColor bool forceColor bool workers int + attestorKey string }{ strict: true, workers: 5, @@ -354,7 +356,14 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command { } log.Debugf("Worker %d got a component %q", id, comp.ContainerImage) - out, err := validate(ctx, comp, data.spec, data.policy, evaluators, data.info) + + vsaVerify, err := image.VerifyVSA(comp.ContainerImage, data.publicKey) + if err != nil { + log.Errorf("error retrieving VSA: %v", err) + } else if vsaVerify == nil { + log.Warnf("No VSA verification found for image: %s", comp.ContainerImage) + } + res := result{ err: err, component: applicationsnapshot.Component{ @@ -363,34 +372,81 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command { }, } - // Skip on err to not panic. Error is return on routine completion. - if err == nil { - res.component.Violations = out.Violations() - res.component.Warnings = out.Warnings() - - successes := out.Successes() - res.component.SuccessCount = len(successes) - if showSuccesses { - res.component.Successes = successes + var maxTime int64 + var attestation string + if vsaVerify != nil { + log.Warnf("VSA verification found for image: %s", comp.ContainerImage) + log.Warnf("VSA Payload: %+v", vsaVerify.Payload) + // capture the latest vsa + for _, uuid := range vsaVerify.Payload { + entries := image.GetByUUID(uuid) + log.Warnf("Found %d entries for UUID: %s", len(entries), uuid) + for _, entry := range entries { + if entry.IntegratedTime >= maxTime { + maxTime = entry.IntegratedTime + attestation = entry.Attestation + log.Warnf("Selected attestation with time %d: %s", maxTime, attestation) + } + } + } + log.Warnf("Unmarshalling attestation with time %d: %s", maxTime, attestation) + // print integratedTime + var stmt applicationsnapshot.Statement + err := json.Unmarshal([]byte(attestation), &stmt) + log.Warnf("Unmarshalled attestation to statement with time %d: %v", maxTime, stmt) + if err != nil { + log.Errorf("Failed to unmarshal attestation: %v", err) + log.Debugf("Attestation content: %s", attestation) + } else { + if stmt.Predicate.Component.ContainerImage == "" { + log.Warnf("Component information is empty in attestation") + log.Debugf("Statement structure: %+v", stmt) + log.Debugf("Predicate structure: %+v", stmt.Predicate) + } + res.component = stmt.Predicate.Component } + } else { + out, err := validate(ctx, comp, data.spec, data.policy, evaluators, data.info) + + // Skip on err to not panic. Error is return on routine completion. + if err == nil { + res.component.Violations = out.Violations() + res.component.Warnings = out.Warnings() + + successes := out.Successes() + res.component.SuccessCount = len(successes) + if showSuccesses { + res.component.Successes = successes + } - res.component.Signatures = out.Signatures - // Create a new result object for attestations. The point is to only keep the data that's needed. - // For example, the Statement is only needed when the full attestation is printed. - for _, att := range out.Attestations { - attResult := applicationsnapshot.NewAttestationResult(att) - if containsOutput(data.output, "attestation") { - attResult.Statement = att.Statement() + res.component.Signatures = out.Signatures + // Create a new result object for attestations. The point is to only keep the data that's needed. + // For example, the Statement is only needed when the full attestation is printed. + for _, att := range out.Attestations { + attResult := applicationsnapshot.NewAttestationResult(att) + if containsOutput(data.output, "attestation") { + attResult.Statement = att.Statement() + } + res.component.Attestations = append(res.component.Attestations, attResult) } - res.component.Attestations = append(res.component.Attestations, attResult) + res.component.ContainerImage = out.ImageURL + res.policyInput = out.PolicyInput } - res.component.ContainerImage = out.ImageURL - res.policyInput = out.PolicyInput - } - res.component.Success = err == nil && len(res.component.Violations) == 0 + res.component.Success = err == nil && len(res.component.Violations) == 0 - if task != nil { - task.End() + if task != nil { + task.End() + } + + vsa, err := applicationsnapshot.ComponentVSA(res.component) + if err != nil { + fmt.Printf("unable to generate VSA for component: %s. %v", res.component.ContainerImage, err) + } + + // we need a private key for the signing + if err := image.ToRekor(vsa, comp.ContainerImage, data.attestorKey); err != nil { + fmt.Println(err) + } } results <- res } @@ -473,6 +529,9 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command { cmd.Flags().StringVarP(&data.publicKey, "public-key", "k", data.publicKey, "path to the public key. Overrides publicKey from EnterpriseContractPolicy") + cmd.Flags().StringVarP(&data.attestorKey, "attestor-key", "a", data.attestorKey, + "path to the private key to sign a VSA.") + cmd.Flags().StringVarP(&data.rekorURL, "rekor-url", "r", data.rekorURL, "Rekor URL. Overrides rekorURL from EnterpriseContractPolicy") diff --git a/docs/modules/ROOT/pages/ec_validate_image.adoc b/docs/modules/ROOT/pages/ec_validate_image.adoc index f196d9947..3da195726 100644 --- a/docs/modules/ROOT/pages/ec_validate_image.adoc +++ b/docs/modules/ROOT/pages/ec_validate_image.adoc @@ -110,6 +110,7 @@ Use a regular expression to match certificate attributes. == Options +-a, --attestor-key:: path to the private key to sign a VSA. --certificate-identity:: URL of the certificate identity for keyless verification --certificate-identity-regexp:: Regular expression for the URL of the certificate identity for keyless verification --certificate-oidc-issuer:: URL of the certificate OIDC issuer for keyless verification diff --git a/go.mod b/go.mod index a4bcf38eb..86454ab48 100644 --- a/go.mod +++ b/go.mod @@ -60,6 +60,14 @@ require ( // use forked version until we can get the fixes merged see https://github.com/enterprise-contract/go-containerregistry/blob/main/hack/ec-patches.sh for a list of patches we carry replace github.com/google/go-containerregistry => github.com/enterprise-contract/go-containerregistry v0.20.3-0.20250120083621-7be5271048b1 +require ( + github.com/go-openapi/runtime v0.28.0 + github.com/go-openapi/strfmt v0.23.0 + github.com/go-openapi/swag v0.23.0 + github.com/sigstore/rekor v1.3.6 + sigs.k8s.io/release-utils v0.8.4 +) + require ( cloud.google.com/go v0.115.1 // indirect cloud.google.com/go/auth v0.9.3 // indirect @@ -130,6 +138,10 @@ require ( github.com/blang/semver v3.5.1+incompatible // indirect github.com/blendle/zapdriver v1.3.1 // indirect github.com/bufbuild/protocompile v0.14.1 // indirect + github.com/buildkite/agent/v3 v3.81.0 // indirect + github.com/buildkite/go-pipeline v0.13.1 // indirect + github.com/buildkite/interpolate v0.1.3 // indirect + github.com/buildkite/roko v1.2.0 // indirect github.com/bytecodealliance/wasmtime-go/v3 v3.0.2 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/census-instrumentation/opencensus-proto v0.4.1 // indirect @@ -189,11 +201,9 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/loads v0.22.0 // indirect - github.com/go-openapi/runtime v0.28.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect - github.com/go-openapi/strfmt v0.23.0 // indirect - github.com/go-openapi/swag v0.23.0 // indirect github.com/go-openapi/validate v0.24.0 // indirect + github.com/go-piv/piv-go v1.11.0 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/gogo/protobuf v1.3.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect @@ -210,6 +220,7 @@ require ( github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/s2a-go v0.1.8 // indirect + github.com/google/trillian v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.3 // indirect github.com/googleapis/gax-go/v2 v2.13.0 // indirect @@ -262,10 +273,12 @@ require ( github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/nozzle/throttler v0.0.0-20180817012639-2ea982251481 // indirect github.com/oklog/ulid v1.3.1 // indirect + github.com/oleiade/reflections v1.1.0 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pborman/uuid v1.2.1 // indirect github.com/pelletier/go-toml/v2 v2.2.3 // indirect github.com/peterh/liner v1.2.2 // indirect github.com/pjbgf/sha1cd v0.3.2 // indirect @@ -292,12 +305,12 @@ require ( github.com/shteou/go-ignore v0.3.1 // indirect github.com/sigstore/fulcio v1.6.3 // indirect github.com/sigstore/protobuf-specs v0.3.2 // indirect - github.com/sigstore/rekor v1.3.6 // indirect github.com/sigstore/timestamp-authority v1.2.2 // indirect github.com/skeema/knownhosts v1.3.0 // indirect github.com/skratchdot/open-golang v0.0.0-20200116055534-eef842397966 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/cast v1.7.0 // indirect + github.com/spiffe/go-spiffe/v2 v2.3.0 // indirect github.com/stoewer/go-strcase v1.3.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect @@ -326,6 +339,7 @@ require ( github.com/yashtewari/glob-intersection v0.2.0 // indirect github.com/yusufpapurcu/wmi v1.2.3 // indirect github.com/zclconf/go-cty v1.15.0 // indirect + github.com/zeebo/errs v1.3.0 // indirect go.mongodb.org/mongo-driver v1.16.1 // indirect go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/auto/sdk v1.1.0 // indirect @@ -370,6 +384,5 @@ require ( olympos.io/encoding/edn v0.0.0-20201019073823-d3554ca0b0a3 // indirect sigs.k8s.io/controller-runtime v0.19.0 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect - sigs.k8s.io/release-utils v0.8.4 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect ) diff --git a/go.sum b/go.sum index 3d7b1ae35..bd3ff1c0c 100644 --- a/go.sum +++ b/go.sum @@ -503,8 +503,8 @@ github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.19 h1:tUN6H7LWqNx4hQVxomd0CVsDwaDr9gaRQaI4GpSmrsA= +github.com/creack/pty v1.1.19/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f h1:eHnXnuK47UlSTOQexbzxAZfekVz6i+LKRdj1CU5DPaM= github.com/cyberphone/json-canonicalization v0.0.0-20231217050601-ba74d44ecf5f/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= github.com/cyphar/filepath-securejoin v0.3.6 h1:4d9N5ykBnSp5Xn2JkhocYDkOpURL/18CYMpo6xB9uWM= @@ -711,6 +711,7 @@ github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -804,6 +805,7 @@ github.com/google/tink/go v1.7.0 h1:6Eox8zONGebBFcCBqkVmt60LaWZa6xg1cl/DwAh/J1w= github.com/google/tink/go v1.7.0/go.mod h1:GAUOd+QE3pgj9q8VKIGTCP33c/B7eb4NhxLcgTJZStM= github.com/google/trillian v1.6.0 h1:jMBeDBIkINFvS2n6oV5maDqfRlxREAc6CW9QYWQ0qT4= github.com/google/trillian v1.6.0/go.mod h1:Yu3nIMITzNhhMJEHjAtp6xKiu+H/iHu2Oq5FjV2mCWI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= diff --git a/internal/applicationsnapshot/vsa.go b/internal/applicationsnapshot/vsa.go index c787cccb9..1774ab6eb 100644 --- a/internal/applicationsnapshot/vsa.go +++ b/internal/applicationsnapshot/vsa.go @@ -17,6 +17,10 @@ package applicationsnapshot import ( + "encoding/json" + "fmt" + "strings" + "github.com/in-toto/in-toto-golang/in_toto" ) @@ -31,6 +35,51 @@ type ProvenanceStatementVSA struct { Predicate Report `json:"predicate"` } +// splitDigest returns the algorithm and hash from an image reference of the form +// +// "@:" +// +// e.g. "quay.io/foo/bar@sha256:abcdef1234…" → ("sha256", "abcdef1234…", nil) +func splitDigest(ref string) (algorithm, hash string, err error) { + // find the “@” that precedes the digest + at := strings.LastIndex(ref, "@") + if at < 0 { + return "", "", fmt.Errorf("no digest separator '@' in %q", ref) + } + // everything after “@” + digestPart := ref[at+1:] + parts := strings.SplitN(digestPart, ":", 2) + if len(parts) != 2 { + return "", "", fmt.Errorf("invalid digest format %q", digestPart) + } + algorithm, hash = parts[0], parts[1] + if hash == "" { + return "", "", fmt.Errorf("empty hash in %q", ref) + } + return algorithm, hash, nil +} + +func ComponentVSA(comp Component) ([]byte, error) { + stmt := Predicate{ + Verifier: Verifier{ID: "conforma.dev"}, + Policy: Policy{ + URI: "github.com/enterprise-contract/ec-policies//policy/release", + Digest: map[string]string{ + // this needs to be passed in also + "sha256": "3e1f8b9a4e6e1f795b084fc7e0e18b427826f0d9f78e2dbe7e5a9fd6541bd0e9", + }, + }, + Component: comp, + } + + // 2) Marshal it to JSON bytes + b, err := json.MarshalIndent(stmt, "", " ") + if err != nil { + panic(err) + } + return b, nil +} + func NewVSA(report Report) (ProvenanceStatementVSA, error) { subjects, err := getSubjects(report) if err != nil { diff --git a/internal/applicationsnapshot/vsa_types.go b/internal/applicationsnapshot/vsa_types.go new file mode 100644 index 000000000..1099978d4 --- /dev/null +++ b/internal/applicationsnapshot/vsa_types.go @@ -0,0 +1,57 @@ +// Copyright The Conforma 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. +// +// SPDX-License-Identifier: Apache-2.0 + +package applicationsnapshot + +// ---------------------------------------------------------------- +// Top-level in-toto Statement +// ---------------------------------------------------------------- +type Statement struct { + Type string `json:"_type"` + PredicateType string `json:"predicateType"` + Subject []Subject `json:"subject"` + Predicate Predicate `json:"predicate"` +} + +// ---------------------------------------------------------------- +// The actual verification_summary payload +// ---------------------------------------------------------------- +type Predicate struct { + Component Component `json:"component"` + Policy Policy `json:"policy"` + PolicyLevel string `json:"policy_level"` + VerificationResult string `json:"verification_result"` + Verifier Verifier `json:"verifier"` +} + +// ---------------------------------------------------------------- +// Subject array element +// ---------------------------------------------------------------- +type Subject struct { + Name string `json:"name"` + Digest map[string]string `json:"digest"` +} + +// Verifier identifies who ran the check. +type Verifier struct { + ID string `json:"id"` +} + +// Policy describes which policy was used. +type Policy struct { + URI string `json:"uri"` + Digest map[string]string `json:"digest"` +} diff --git a/internal/image/verify_vsa.go b/internal/image/verify_vsa.go new file mode 100644 index 000000000..abcf36151 --- /dev/null +++ b/internal/image/verify_vsa.go @@ -0,0 +1,305 @@ +// Copyright The Conforma 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. +// +// SPDX-License-Identifier: Apache-2.0 + +package image + +import ( + "bytes" + "context" + "crypto" + "crypto/sha256" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/pem" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "runtime" + "strconv" + "strings" + + openairuntime "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" + "github.com/go-openapi/swag" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/attest" + "github.com/sigstore/cosign/v2/cmd/cosign/cli/options" + "github.com/sigstore/rekor/pkg/client" + rclient "github.com/sigstore/rekor/pkg/generated/client" + "github.com/sigstore/rekor/pkg/generated/client/entries" + "github.com/sigstore/rekor/pkg/generated/client/index" + "github.com/sigstore/rekor/pkg/generated/client/pubkey" + "github.com/sigstore/rekor/pkg/generated/models" + "github.com/sigstore/rekor/pkg/sharding" + "github.com/sigstore/rekor/pkg/types" + "github.com/sigstore/rekor/pkg/verify" + "github.com/sigstore/sigstore/pkg/signature" + "github.com/spf13/viper" + "sigs.k8s.io/release-utils/version" +) + +var ( + // uaString is meant to resemble the User-Agent sent by browsers with requests. + // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/User-Agent + uaString = fmt.Sprintf("rekor-cli/%s (%s; %s)", version.GetVersionInfo().GitVersion, runtime.GOOS, runtime.GOARCH) +) + +type getCmdOutput struct { + Attestation string + AttestationType string + Body interface{} + LogIndex int + IntegratedTime int64 + UUID string + LogID string +} + +// UserAgent returns the User-Agent string which `rekor-cli` should send with HTTP requests. +func UserAgent() string { + return uaString +} + +func extractDigest(ref string) (string, error) { + parts := strings.SplitN(ref, "@sha256:", 2) + if len(parts) != 2 || parts[1] == "" { + return "", fmt.Errorf("no sha256 digest found in %q", ref) + } + return parts[1], nil +} + +func VerifyVSA(containerImage string, keyPub string) (*index.SearchIndexOK, error) { + params := index.NewSearchIndexParams() + params.Query = &models.SearchIndex{} + + digest, err := extractDigest(containerImage) + if err != nil { + return nil, fmt.Errorf("extracting digest from %q: %w", containerImage, err) + } + params.Query.Hash = digest + + rekorClient, err := client.GetRekorClient("https://rekor.sigstore.dev", client.WithUserAgent(UserAgent()), client.WithRetryCount(5)) + if err != nil { + return nil, err + } + params.Query.PublicKey = &models.SearchIndexPublicKey{} + params.Query.PublicKey.Format = swag.String(models.SearchIndexPublicKeyFormatX509) + + keyBytes, _ := os.ReadFile(filepath.Clean(keyPub)) + + params.Query.PublicKey.Content = strfmt.Base64(keyBytes) + + resp, err := rekorClient.Index.SearchIndex(params) + if err != nil { + return nil, err + } + + return resp, nil +} + +func ToRekor(vsa []byte, containerImage string, keyRef string) error { + dirName, err := os.MkdirTemp("", "vsadir-*") + if err != nil { + return fmt.Errorf("creating temp dir: %w", err) + } + blobPath := filepath.Join(dirName, "vsa.json") + if err := os.WriteFile(blobPath, vsa, 0o644); err != nil { + return fmt.Errorf("writing VSA blob: %w", err) + } + + // fmt.Println(fileSHA256(blobPath)) + + at := attest.AttestCommand{ + KeyOpts: options.KeyOpts{KeyRef: keyRef, SkipConfirmation: true, RekorURL: "https://rekor.sigstore.dev"}, + PredicatePath: blobPath, + PredicateType: "https://slsa.dev/verification_summary/v0.1", + RekorEntryType: "intoto", + TlogUpload: true, + } + + // 5) Run the attest command + // at := attest.AttestBlobCommand{ + // KeyOpts: options.KeyOpts{KeyRef: keyRef, SkipConfirmation: true, RekorURL: "https://rekor.sigstore.dev"}, + // PredicatePath: blobPath, + // PredicateType: "https://slsa.dev/verification_summary/v0.1", + // OutputSignature: filepath.Join(dirName, "vsa.signature"), + // RekorEntryType: "intoto", + // TlogUpload: true, + // } + + if err := at.Exec(context.Background(), containerImage); err != nil { + return fmt.Errorf("attesting image: %w", err) + } + + return nil +} + +func GetByUUID(uuid string) []*getCmdOutput { + var logEntries []*getCmdOutput + if uuid != "" { + params := entries.NewGetLogEntryByUUIDParams() + params.SetTimeout(viper.GetDuration("timeout")) + params.EntryUUID = uuid + + rekorClient, err := client.GetRekorClient("https://rekor.sigstore.dev", client.WithUserAgent(UserAgent()), client.WithRetryCount(5)) + if err != nil { + fmt.Println(err) + } + resp, err := rekorClient.Entries.GetLogEntryByUUID(params) + if err != nil { + fmt.Println(err) + } + + for k, entry := range resp.Payload { + // retrieve rekor pubkey for verification + treeID, err := sharding.TreeID(k) + if err != nil { + fmt.Println(err) + } + verifier, err := loadVerifier(rekorClient, strconv.FormatInt(treeID, 10)) + if err != nil { + fmt.Printf("retrieving rekor public key: %w", err) + } + + if err := compareEntryUUIDs(params.EntryUUID, k); err != nil { + fmt.Printf("error comparing entry UUIDs: %w\n", err) + } + + if err := verify.VerifyLogEntry(context.Background(), &entry, verifier); err != nil { + fmt.Printf("unable to verify entry was added to log: %w", err) + } + + e, _ := parseEntry(k, entry) + logEntries = append(logEntries, e) + } + } + return logEntries +} + +func parseEntry(uuid string, e models.LogEntryAnon) (*getCmdOutput, error) { + b, err := base64.StdEncoding.DecodeString(e.Body.(string)) + if err != nil { + return nil, err + } + + pe, err := models.UnmarshalProposedEntry(bytes.NewReader(b), openairuntime.JSONConsumer()) + if err != nil { + return nil, err + } + eimpl, err := types.UnmarshalEntry(pe) + if err != nil { + return nil, err + } + + obj := getCmdOutput{ + Body: eimpl, + UUID: uuid, + IntegratedTime: *e.IntegratedTime, + LogIndex: int(*e.LogIndex), + LogID: *e.LogID, + } + + if e.Attestation != nil { + obj.Attestation = string(e.Attestation.Data) + } + + return &obj, nil +} + +func compareEntryUUIDs(requestEntryUUID string, responseEntryUUID string) error { + requestUUID, err := sharding.GetUUIDFromIDString(requestEntryUUID) + if err != nil { + return err + } + responseUUID, err := sharding.GetUUIDFromIDString(responseEntryUUID) + if err != nil { + return err + } + // Compare UUIDs. + if requestUUID != responseUUID { + return fmt.Errorf("unexpected entry returned from rekor server: expected %s, got %s", requestEntryUUID, responseEntryUUID) + } + // If the request contains a Tree ID, then compare that. + requestTreeID, err := sharding.GetTreeIDFromIDString(requestEntryUUID) + if err != nil { + if errors.Is(err, sharding.ErrPlainUUID) { + // The request did not contain a Tree ID, we're good. + return nil + } + // The request had a bad Tree ID, error out. + return err + } + // We requested an entry from a given Tree ID. + responseTreeID, err := sharding.GetTreeIDFromIDString(responseEntryUUID) + if err != nil { + if errors.Is(err, sharding.ErrPlainUUID) { + // The response does not contain a Tree ID, we can only do so much. + // Old rekor instances may not have returned one. + return nil + } + return err + } + // We have Tree IDs. Compare. + if requestTreeID != responseTreeID { + return fmt.Errorf("unexpected entry returned from rekor server: expected %s, got %s", requestEntryUUID, responseEntryUUID) + } + return nil +} + +func loadVerifier(rekorClient *rclient.Rekor, treeID string) (signature.Verifier, error) { + publicKey := viper.GetString("rekor_server_public_key") + if publicKey == "" { + // fetch key from server + keyResp, err := rekorClient.Pubkey.GetPublicKey(pubkey.NewGetPublicKeyParams().WithTreeID(swag.String(treeID))) + if err != nil { + return nil, err + } + publicKey = keyResp.Payload + } + + block, _ := pem.Decode([]byte(publicKey)) + if block == nil { + return nil, errors.New("failed to decode public key of server") + } + + pub, err := x509.ParsePKIXPublicKey(block.Bytes) + if err != nil { + return nil, err + } + + return signature.LoadVerifier(pub, crypto.SHA256) +} + +func fileSHA256(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", fmt.Errorf("opening file: %w", err) + } + defer f.Close() + + // 2) Create a new SHA‑256 hash + hasher := sha256.New() + + // 3) Copy the file's contents into the hash + if _, err := io.Copy(hasher, f); err != nil { + return "", fmt.Errorf("hashing file: %w", err) + } + + // 4) Compute the final digest and hex‑encode it + sum := hasher.Sum(nil) + return hex.EncodeToString(sum), nil +}