feat: add attest sign/verify
This commit is contained in:
@@ -1,2 +1,287 @@
|
||||
package attest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
"github.com/google/go-containerregistry/pkg/v1/match"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
"github.com/google/go-containerregistry/pkg/v1/static"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||
)
|
||||
|
||||
func SignIndexAttestations(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *SigningOptions) (v1.ImageIndex, error) {
|
||||
indexManifest, err := idx.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
||||
}
|
||||
|
||||
var originalManifestDigests []v1.Hash
|
||||
var muts []mutate.IndexAddendum
|
||||
for _, manifest := range indexManifest.Manifests {
|
||||
if manifest.Annotations[oci.DockerReferenceType] != oci.AttestationManifestType {
|
||||
continue
|
||||
}
|
||||
|
||||
originalManifestDigests = append(originalManifestDigests, manifest.Digest)
|
||||
|
||||
attestationImage, err := idx.Image(manifest.Digest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", manifest.Digest.String(), err)
|
||||
}
|
||||
layers, err := attestationImage.Layers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err)
|
||||
}
|
||||
|
||||
var signedLayers []mutate.Addendum
|
||||
var originalLayers []v1.Layer
|
||||
var statements []*intoto.Statement
|
||||
|
||||
for _, layer := range layers {
|
||||
// parse layer blob as json
|
||||
r, err := layer.Uncompressed()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
||||
}
|
||||
defer r.Close()
|
||||
mt, err := layer.MediaType()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
||||
}
|
||||
|
||||
if mt != types.MediaType(intoto.PayloadType) {
|
||||
originalLayers = append(originalLayers, layer)
|
||||
continue
|
||||
}
|
||||
var stmt = new(intoto.Statement)
|
||||
err = json.NewDecoder(r).Decode(&stmt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode statement layer contents: %w", err)
|
||||
}
|
||||
|
||||
statements = append(statements, stmt)
|
||||
layerDesc, err := partial.Descriptor(layer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get descriptor for layer: %w", err)
|
||||
}
|
||||
// copy original annotations and add new ones
|
||||
ann := make(map[string]string)
|
||||
for k, v := range layerDesc.Annotations {
|
||||
ann[k] = v
|
||||
}
|
||||
ann[InTotoReferenceLifecycleStage] = LifecycleStageExperimental
|
||||
|
||||
var env *attestation.Envelope
|
||||
var mediaType string
|
||||
switch opts.EnvelopeStyle {
|
||||
case OCIContentDescriptor:
|
||||
// Ensure we sign just the digest, size, and media type
|
||||
payloadDesc := v1.Descriptor{
|
||||
Digest: layerDesc.Digest,
|
||||
Size: layerDesc.Size,
|
||||
MediaType: layerDesc.MediaType,
|
||||
}
|
||||
payload, err := json.Marshal(payloadDesc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal descriptor: %w", err)
|
||||
}
|
||||
env, err = attestation.SignDSSE(ctx, payload, ociv1.MediaTypeDescriptor, signer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign statement: %w", err)
|
||||
}
|
||||
ann[oci.DockerReferenceDigest] = layerDesc.Digest.String()
|
||||
// this is a reference type
|
||||
opts.Replace = false
|
||||
mediaType = attestation.OCIDescriptorDSSEMediaType
|
||||
case EmbeddedDSSE:
|
||||
payload, err := json.Marshal(stmt)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal statement: %w", err)
|
||||
}
|
||||
env, err = attestation.SignDSSE(ctx, payload, intoto.PayloadType, signer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign statement: %w", err)
|
||||
}
|
||||
mediaType, err = attestation.DSSEMediaType(stmt.PredicateType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get DSSE media type: %w", err)
|
||||
}
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown envelope style %q", opts.EnvelopeStyle)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal envelope: %w", err)
|
||||
}
|
||||
newLayer := static.NewLayer(data, types.MediaType(mediaType))
|
||||
|
||||
withAnnotations := mutate.Addendum{
|
||||
Layer: newLayer,
|
||||
Annotations: ann,
|
||||
}
|
||||
signedLayers = append(signedLayers, withAnnotations)
|
||||
}
|
||||
|
||||
newImg, err := addSignedLayers(signedLayers, originalLayers, manifest.MediaType, attestationImage, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add signed layers: %w", err)
|
||||
}
|
||||
|
||||
if opts.VSAOptions != nil {
|
||||
newImg, err = addVSA(ctx, newImg, statements, manifest.MediaType, signer, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add VSA: %w", err)
|
||||
}
|
||||
}
|
||||
newDesc, err := partial.Descriptor(newImg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get descriptor: %w", err)
|
||||
}
|
||||
cf, err := attestationImage.ConfigFile()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get config file: %w", err)
|
||||
}
|
||||
newDesc.Platform = cf.Platform()
|
||||
newDesc.MediaType = manifest.MediaType
|
||||
newDesc.Annotations = manifest.Annotations
|
||||
|
||||
muts = append(muts, mutate.IndexAddendum{
|
||||
Add: newImg,
|
||||
Descriptor: *newDesc,
|
||||
})
|
||||
}
|
||||
// create new index with signed images
|
||||
newIndex := mutate.RemoveManifests(idx, match.Digests(originalManifestDigests...))
|
||||
newIndex = mutate.AppendManifests(newIndex, muts...)
|
||||
|
||||
return newIndex, nil
|
||||
}
|
||||
|
||||
func addVSA(ctx context.Context, image v1.Image, stmt []*intoto.Statement, outerMediaType types.MediaType, signer dsse.SignerVerifier, opts *SigningOptions) (v1.Image, error) {
|
||||
if len(stmt) == 0 {
|
||||
return nil, fmt.Errorf("no attestations found to generate VSA from")
|
||||
}
|
||||
sub := stmt[0].Subject[0]
|
||||
stype := stmt[0].Type
|
||||
|
||||
uri, err := attestation.ToVSAResourceURI(sub)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate VSA resource URI: %w", err)
|
||||
}
|
||||
|
||||
inputs := make([]attestation.VSAInputAttestation, 0, len(stmt))
|
||||
layers, err := image.Layers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layers: %w", err)
|
||||
}
|
||||
for _, layer := range layers {
|
||||
mt, err := layer.MediaType()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
||||
}
|
||||
mediaType := string(mt)
|
||||
if !strings.HasPrefix(mediaType, "application/vnd.in-toto.") ||
|
||||
!strings.HasSuffix(mediaType, "+dsse") {
|
||||
continue
|
||||
}
|
||||
|
||||
dgst, err := layer.Digest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer digest: %w", err)
|
||||
}
|
||||
inputs = append(inputs, attestation.VSAInputAttestation{
|
||||
Digest: map[string]string{"sha256": dgst.Hex},
|
||||
MediaType: string(mt),
|
||||
})
|
||||
}
|
||||
vsaStatement := &intoto.Statement{
|
||||
StatementHeader: intoto.StatementHeader{
|
||||
PredicateType: attestation.VSAPredicateType,
|
||||
Type: stype,
|
||||
Subject: stmt[0].Subject,
|
||||
},
|
||||
Predicate: attestation.VSAPredicate{
|
||||
Verifier: attestation.VSAVerifier{
|
||||
ID: opts.VSAOptions.VerifierID,
|
||||
},
|
||||
TimeVerified: time.Now().UTC().Format(time.RFC3339),
|
||||
ResourceUri: uri,
|
||||
Policy: attestation.VSAPolicy{URI: opts.VSAOptions.PolicyURI},
|
||||
VerificationResult: "PASSED",
|
||||
VerifiedLevels: []string{opts.VSAOptions.BuildLevel},
|
||||
InputAttestations: inputs,
|
||||
},
|
||||
}
|
||||
payload, err := json.Marshal(vsaStatement)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal statement: %w", err)
|
||||
}
|
||||
env, err := attestation.SignDSSE(ctx, payload, intoto.PayloadType, signer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign statement: %w", err)
|
||||
}
|
||||
mediaType, err := attestation.DSSEMediaType(vsaStatement.PredicateType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get DSSE media type: %w", err)
|
||||
}
|
||||
|
||||
data, err := json.Marshal(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal envelope: %w", err)
|
||||
}
|
||||
mt := types.MediaType(mediaType)
|
||||
newLayer := static.NewLayer(data, mt)
|
||||
ann := make(map[string]string)
|
||||
ann[InTotoReferenceLifecycleStage] = LifecycleStageExperimental
|
||||
ann[oci.InTotoPredicateType] = attestation.VSAPredicateType
|
||||
withAnnotations := mutate.Addendum{
|
||||
Layer: newLayer,
|
||||
Annotations: ann,
|
||||
}
|
||||
opts = &SigningOptions{
|
||||
Replace: false,
|
||||
}
|
||||
return addSignedLayers([]mutate.Addendum{withAnnotations}, layers, outerMediaType, image, opts)
|
||||
}
|
||||
|
||||
func addSignedLayers(signedLayers []mutate.Addendum, originalLayers []v1.Layer, mediaType types.MediaType, attestationImage v1.Image, opts *SigningOptions) (v1.Image, error) {
|
||||
var err error
|
||||
if opts.Replace {
|
||||
newImg := empty.Image
|
||||
newImg = mutate.MediaType(newImg, mediaType)
|
||||
newImg = mutate.ConfigMediaType(newImg, "application/vnd.oci.image.config.v1+json")
|
||||
for _, layer := range signedLayers {
|
||||
newImg, err = mutate.Append(newImg, layer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to append layer: %w", err)
|
||||
}
|
||||
}
|
||||
newImg, err = mutate.AppendLayers(newImg, originalLayers...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to append original layers: %w", err)
|
||||
}
|
||||
return newImg, nil
|
||||
|
||||
}
|
||||
for _, layer := range signedLayers {
|
||||
attestationImage, err = mutate.Append(attestationImage, layer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to append layer: %w", err)
|
||||
}
|
||||
}
|
||||
return attestationImage, nil
|
||||
}
|
||||
|
||||
176
pkg/attest/sign_test.go
Normal file
176
pkg/attest/sign_test.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package attest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/policy"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
"github.com/google/go-containerregistry/pkg/v1/static"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var (
|
||||
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
|
||||
LocalPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy")
|
||||
TestTempDir = "attest-sign-test"
|
||||
)
|
||||
|
||||
func TestSignVerifyOCILayout(t *testing.T) {
|
||||
ctx, signer := test.Setup(t)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
TestImage string
|
||||
expectedStatements int
|
||||
expectedAttestations int
|
||||
envelopeStyle envelopeStyle
|
||||
replace bool
|
||||
}{
|
||||
|
||||
{"signed replaced (does nothing)", UnsignedTestImage, 4, 4, OCIContentDescriptor, true},
|
||||
{"without replace", UnsignedTestImage, 4, 4, OCIContentDescriptor, false},
|
||||
{"embedded", UnsignedTestImage, 0, 6, EmbeddedDSSE, true},
|
||||
{"embedded", UnsignedTestImage, 4, 6, EmbeddedDSSE, false},
|
||||
|
||||
// image without provenance doesn't fail
|
||||
{"no provenance (replace)", NoProvenanceImage, 0, 4, EmbeddedDSSE, true},
|
||||
{"no provenance (no replace)", NoProvenanceImage, 2, 4, EmbeddedDSSE, false},
|
||||
}
|
||||
policyResolver := &policy.PolicyOptions{
|
||||
LocalPolicyDir: LocalPolicyDir,
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
tempDir := test.CreateTempDir(t, "", TestTempDir)
|
||||
outputLayout := tempDir
|
||||
opts := &SigningOptions{
|
||||
Replace: tc.replace,
|
||||
EnvelopeStyle: tc.envelopeStyle,
|
||||
VSAOptions: &attestation.VSAOptions{
|
||||
BuildLevel: "SLSA_BUILD_LEVEL_3",
|
||||
PolicyURI: "https://docker.com/attest/policy",
|
||||
VerifierID: "https://docker.com",
|
||||
},
|
||||
}
|
||||
attIdx, err := oci.AttestationIndexFromPath(tc.TestImage)
|
||||
assert.NoError(t, err)
|
||||
signedIndex, err := SignIndexAttestations(ctx, attIdx.Index, signer, opts)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// output signed attestations
|
||||
idx := v1.ImageIndex(empty.Index)
|
||||
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
|
||||
Add: signedIndex,
|
||||
Descriptor: v1.Descriptor{
|
||||
Annotations: map[string]string{
|
||||
oci.OciReferenceTarget: attIdx.Name,
|
||||
},
|
||||
},
|
||||
})
|
||||
_, err = layout.Write(outputLayout, idx)
|
||||
assert.NoError(t, err)
|
||||
|
||||
resolver := &oci.OCILayoutResolver{
|
||||
Path: outputLayout,
|
||||
Platform: "",
|
||||
}
|
||||
policy, err := Verify(ctx, policyResolver, resolver)
|
||||
assert.NoError(t, err)
|
||||
assert.Truef(t, policy, "Policy should have been found")
|
||||
|
||||
mt, _ := attestation.DSSEMediaType(attestation.VSAPredicateType)
|
||||
vsas, err := test.ExtractAnnotatedStatements(tempDir, mt)
|
||||
assert.NoError(t, err)
|
||||
assert.Equalf(t, len(vsas), 2, "expected %d vsa statement, got %d", 2, len(vsas))
|
||||
|
||||
switch tc.envelopeStyle {
|
||||
case OCIContentDescriptor:
|
||||
{
|
||||
statements, err := test.ExtractAnnotatedStatements(tempDir, intoto.PayloadType)
|
||||
assert.NoError(t, err)
|
||||
assert.Equalf(t, tc.expectedStatements, len(statements), "expected %d statement, got %d", tc.expectedStatements, len(statements))
|
||||
|
||||
statements, err = test.ExtractAnnotatedStatements(tempDir, attestation.OCIDescriptorDSSEMediaType)
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equalf(t, tc.expectedAttestations, len(statements), "expected %d attestations, got %d", tc.expectedAttestations, len(statements))
|
||||
}
|
||||
case EmbeddedDSSE:
|
||||
{
|
||||
var allEnvelopes []*test.AnnotatedStatement
|
||||
for _, predicate := range []string{intoto.PredicateSPDX, v02.PredicateSLSAProvenance, attestation.VSAPredicateType} {
|
||||
mt, _ := attestation.DSSEMediaType(predicate)
|
||||
statements, err := test.ExtractAnnotatedStatements(tempDir, mt)
|
||||
assert.NoError(t, err)
|
||||
allEnvelopes = append(allEnvelopes, statements...)
|
||||
|
||||
for _, stmt := range statements {
|
||||
assert.Equalf(t, predicate, stmt.Annotations[oci.InTotoPredicateType], "expected predicate-type annotation to be set to %s, got %s", predicate, stmt.Annotations[oci.InTotoPredicateType])
|
||||
assert.Equalf(t, LifecycleStageExperimental, stmt.Annotations[InTotoReferenceLifecycleStage], "expected reference lifecycle stage annotation to be set to %s, got %s", LifecycleStageExperimental, stmt.Annotations[InTotoReferenceLifecycleStage])
|
||||
}
|
||||
}
|
||||
assert.Equalf(t, tc.expectedAttestations, len(allEnvelopes), "expected %d attestations, got %d", tc.expectedAttestations, len(allEnvelopes))
|
||||
statements, err := test.ExtractAnnotatedStatements(tempDir, intoto.PayloadType)
|
||||
assert.NoError(t, err)
|
||||
assert.Equalf(t, tc.expectedStatements, len(statements), "expected %d statement, got %d", tc.expectedStatements, len(statements))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddSignedLayerAnnotations(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
replace bool
|
||||
}{
|
||||
{"replaced", true},
|
||||
{"not replaced", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
data := []byte("signed")
|
||||
signedLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType))
|
||||
signedLayers := []mutate.Addendum{
|
||||
{
|
||||
Layer: signedLayer,
|
||||
Annotations: map[string]string{"test": "test"},
|
||||
},
|
||||
}
|
||||
data = []byte("test")
|
||||
testLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType))
|
||||
mediaType := types.OCIManifestSchema1
|
||||
img := empty.Image
|
||||
opts := &SigningOptions{
|
||||
Replace: tc.replace,
|
||||
}
|
||||
newImg, err := addSignedLayers(signedLayers, []v1.Layer{testLayer}, mediaType, img, opts)
|
||||
assert.NoError(t, err)
|
||||
mf, _ := newImg.RawManifest()
|
||||
type Annotations struct {
|
||||
Annotations map[string]string `json:"annotations"`
|
||||
}
|
||||
type Layers struct {
|
||||
Layers []Annotations `json:"layers"`
|
||||
}
|
||||
l := &Layers{}
|
||||
err = json.Unmarshal(mf, l)
|
||||
assert.NoError(t, err)
|
||||
_, ok := l.Layers[0].Annotations["test"]
|
||||
assert.Truef(t, ok, "missing annotations")
|
||||
})
|
||||
}
|
||||
}
|
||||
33
pkg/attest/types.go
Normal file
33
pkg/attest/types.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package attest
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
)
|
||||
|
||||
type envelopeStyle string
|
||||
|
||||
const (
|
||||
OCIContentDescriptor envelopeStyle = "oci-content-descriptor"
|
||||
EmbeddedDSSE envelopeStyle = "embedded-dsse"
|
||||
InTotoReferenceLifecycleStage = "vnd.docker.lifecycle-stage"
|
||||
LifecycleStageExperimental = "experimental"
|
||||
)
|
||||
|
||||
type SigningOptions struct {
|
||||
Replace bool
|
||||
EnvelopeStyle envelopeStyle
|
||||
VSAOptions *attestation.VSAOptions
|
||||
}
|
||||
|
||||
func EnvelopeStyle(style string) (envelopeStyle, error) {
|
||||
switch style {
|
||||
case string(OCIContentDescriptor):
|
||||
return OCIContentDescriptor, nil
|
||||
case string(EmbeddedDSSE):
|
||||
return EmbeddedDSSE, nil
|
||||
default:
|
||||
return "", fmt.Errorf("unknown envelope style %q", style)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,18 @@
|
||||
package attestation
|
||||
|
||||
import "encoding/base64"
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
|
||||
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||
)
|
||||
|
||||
const (
|
||||
DockerDsseExtKind = "application/vnd.docker.attestation-verification.v1+json"
|
||||
RekorTlExtKind = "Rekor"
|
||||
DockerDsseExtKind = "application/vnd.docker.attestation-verification.v1+json"
|
||||
RekorTlExtKind = "Rekor"
|
||||
OCIDescriptorDSSEMediaType = ociv1.MediaTypeDescriptor + "+dsse"
|
||||
)
|
||||
|
||||
var base64Encoding = base64.StdEncoding.Strict()
|
||||
@@ -33,3 +41,20 @@ type DockerTlExtension struct {
|
||||
Kind string `json:"kind"`
|
||||
Data any `json:"data"`
|
||||
}
|
||||
|
||||
func DSSEMediaType(predicateType string) (string, error) {
|
||||
var predicateName string
|
||||
switch predicateType {
|
||||
case v02.PredicateSLSAProvenance:
|
||||
predicateName = "provenance"
|
||||
case intoto.PredicateSPDX:
|
||||
predicateName = "spdx"
|
||||
case VSAPredicateType:
|
||||
predicateName = "verification_summary"
|
||||
|
||||
default:
|
||||
return "", fmt.Errorf("unknown predicate type %q", predicateType)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("application/vnd.in-toto.%s+dsse", predicateName), nil
|
||||
}
|
||||
|
||||
55
pkg/attestation/vsa.go
Normal file
55
pkg/attestation/vsa.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/package-url/packageurl-go"
|
||||
)
|
||||
|
||||
const (
|
||||
VSAPredicateType = "https://slsa.dev/verification_summary/v1"
|
||||
)
|
||||
|
||||
type VSAPredicate struct {
|
||||
Verifier VSAVerifier `json:"verifier"`
|
||||
TimeVerified string `json:"timeVerified"`
|
||||
ResourceUri string `json:"resourceUri"`
|
||||
Policy VSAPolicy `json:"policy"`
|
||||
InputAttestations []VSAInputAttestation `json:"inputAttestations"`
|
||||
VerificationResult string `json:"verificationResult"`
|
||||
VerifiedLevels []string `json:"verifiedLevels"`
|
||||
}
|
||||
|
||||
type VSAVerifier struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type VSAPolicy struct {
|
||||
URI string `json:"uri"`
|
||||
}
|
||||
|
||||
type VSAInputAttestation struct {
|
||||
Digest map[string]string `json:"digest"`
|
||||
MediaType string `json:"mediaType"`
|
||||
}
|
||||
|
||||
type VSAOptions struct {
|
||||
BuildLevel string
|
||||
PolicyURI string
|
||||
VerifierID string
|
||||
}
|
||||
|
||||
func ToVSAResourceURI(sub intoto.Subject) (string, error) {
|
||||
//parse purl
|
||||
purl, err := packageurl.FromString(sub.Name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to parse package url: %w", err)
|
||||
}
|
||||
quals := purl.Qualifiers.Map()
|
||||
if quals["digest"] == "" {
|
||||
quals["digest"] = "sha256:" + sub.Digest["sha256"]
|
||||
}
|
||||
purl.Qualifiers = packageurl.QualifiersFromMap(quals)
|
||||
return purl.String(), nil
|
||||
}
|
||||
@@ -1,8 +1,69 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/google/go-containerregistry/pkg/authn"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
)
|
||||
|
||||
const (
|
||||
DockerReferenceType = "vnd.docker.reference.type"
|
||||
DockerReferenceDigest = "vnd.docker.reference.digest"
|
||||
AttestationManifestType = "attestation-manifest"
|
||||
InTotoPredicateType = "in-toto.io/predicate-type"
|
||||
OciReferenceTarget = "org.opencontainers.image.ref.name"
|
||||
)
|
||||
|
||||
type AttestationIndex struct {
|
||||
Index v1.ImageIndex
|
||||
Name string
|
||||
}
|
||||
|
||||
func AttestationIndexFromPath(path string) (*AttestationIndex, error) {
|
||||
wrapperIdx, err := layout.ImageIndexFromPath(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load image index: %w", err)
|
||||
}
|
||||
|
||||
idxm, err := wrapperIdx.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get digest: %w", err)
|
||||
}
|
||||
imageName := idxm.Manifests[0].Annotations[OciReferenceTarget]
|
||||
idxDigest := idxm.Manifests[0].Digest
|
||||
|
||||
idx, err := wrapperIdx.ImageIndex(idxDigest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
|
||||
}
|
||||
return &AttestationIndex{
|
||||
Index: idx,
|
||||
Name: imageName,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func AttestationIndexFromRemote(image string) (*AttestationIndex, error) {
|
||||
ref, err := name.ParseReference(image)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to parse image name: %v", err)
|
||||
}
|
||||
// Get the authenticator from the default Docker keychain
|
||||
auth, err := authn.DefaultKeychain.Resolve(ref.Context())
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get authenticator: %v", err)
|
||||
}
|
||||
// Pull the image from the registry
|
||||
idx, err := remote.Index(ref, remote.WithAuth(auth))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to pull image %s: %w", image, err)
|
||||
}
|
||||
return &AttestationIndex{
|
||||
Index: idx,
|
||||
Name: image,
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user