Skip to content
Draft

VSA POC #2497

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 83 additions & 24 deletions cmd/validate/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -73,6 +74,7 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
noColor bool
forceColor bool
workers int
attestorKey string
}{
strict: true,
workers: 5,
Expand Down Expand Up @@ -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{
Expand All @@ -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
}
Expand Down Expand Up @@ -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")

Expand Down
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/ec_validate_image.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
23 changes: 18 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
)
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
49 changes: 49 additions & 0 deletions internal/applicationsnapshot/vsa.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
package applicationsnapshot

import (
"encoding/json"
"fmt"
"strings"

"github.com/in-toto/in-toto-golang/in_toto"
)

Expand All @@ -31,6 +35,51 @@
Predicate Report `json:"predicate"`
}

// splitDigest returns the algorithm and hash from an image reference of the form
//
// "<repo>@<algorithm>:<hex>"
//
// e.g. "quay.io/foo/bar@sha256:abcdef1234…" → ("sha256", "abcdef1234…", nil)
func splitDigest(ref string) (algorithm, hash string, err error) {

Check failure on line 43 in internal/applicationsnapshot/vsa.go

View workflow job for this annotation

GitHub Actions / Lint

func `splitDigest` is unused (unused)
// 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 {
Expand Down
57 changes: 57 additions & 0 deletions internal/applicationsnapshot/vsa_types.go
Original file line number Diff line number Diff line change
@@ -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"`
}
Loading
Loading