Unify functions for use in sign & verify --vsa (#71)

* Use receivers for manifest functions
* Move SaveImage/SaveIndex from image-signing-verifier
* Ignore test fixtures in coverage
* Add AddImagesToIndex function
This commit is contained in:
James Carnegie
2024-07-05 09:29:14 +01:00
committed by GitHub
parent 0dd63bf5a3
commit 0038e3d23d
15 changed files with 278 additions and 386 deletions

2
codecov.yml Normal file
View File

@@ -0,0 +1,2 @@
ignore:
- "internal/test"

View File

@@ -32,22 +32,30 @@ func ExampleSign_remote() {
// load image index with unsigned attestation-manifests
ref := "docker/image-signer-verifier:latest"
att, err := oci.SubjectIndexFromRemote(ref)
attIdx, err := oci.IndexFromRemote(ref)
if err != nil {
panic(err)
}
// example for local image index
// path := "/myimage"
// att, err := oci.AttestationIndexFromLocal(path)
// attIdx, err = oci.IndexFromPath(path)
// if err != nil {
// panic(err)
// }
// sign attestations
signedImageIndex, err := attest.Sign(context.Background(), att.Index, signer, opts)
// sign all attestations in an image index
signedManifests, err := attest.SignStatements(context.Background(), attIdx.Index, signer, opts)
if err != nil {
panic(err)
}
signedIndex := attIdx.Index
signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests)
if err != nil {
panic(err)
}
// push image index with signed attestation-manifests
err = mirror.PushIndexToRegistry(signedImageIndex, ref)
err = mirror.PushIndexToRegistry(signedIndex, ref)
if err != nil {
panic(err)
}
@@ -55,10 +63,10 @@ func ExampleSign_remote() {
path := "/myimage"
idx := v1.ImageIndex(empty.Index)
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
Add: signedImageIndex,
Add: signedIndex,
Descriptor: v1.Descriptor{
Annotations: map[string]string{
oci.OciReferenceTarget: att.Name,
oci.OciReferenceTarget: attIdx.Name,
},
},
})

View File

@@ -2,246 +2,25 @@ package attest
import (
"context"
"encoding/json"
"fmt"
"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"
"github.com/secure-systems-lab/go-securesystemslib/dsse"
)
func Sign(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *attestation.SigningOptions) (v1.ImageIndex, error) {
images, err := SignedAttestationImages(ctx, idx, signer, opts)
if err != nil {
return nil, fmt.Errorf("failed to sign attestation images: %w", err)
}
for _, image := range images {
idx, err = addImageToIndex(idx, image.Image, image.Descriptor, image.AttestationManifest)
if err != nil {
return nil, fmt.Errorf("failed to add signed layers to index: %w", err)
}
}
return idx, nil
}
func SignedAttestationImages(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *attestation.SigningOptions) ([]*attestation.SignedAttestationImage, error) {
// this is only relevant if there are (unsigned) in-toto statements
func SignStatements(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *attestation.SigningOptions) ([]*attestation.AttestationManifest, error) {
// extract attestation manifests from index
attestationManifests, err := attestation.GetAttestationManifestsFromIndex(idx)
if err != nil {
return nil, fmt.Errorf("failed to get attestation manifests: %w", err)
return nil, fmt.Errorf("failed to load attestation manifests from index: %w", err)
}
if len(attestationManifests) == 0 {
return nil, fmt.Errorf("no attestation manifests found")
}
images := []*attestation.SignedAttestationImage{}
// sign every attestation layer in each manifest
for _, manifest := range attestationManifests {
newImg, newDescriptor, err := SignLayersAndAddToImage(ctx, manifest.Attestation.Layers, manifest, signer, opts)
if err != nil {
return nil, fmt.Errorf("failed to add signed layers to image: %w", err)
}
images = append(images, &attestation.SignedAttestationImage{
Image: newImg,
Descriptor: newDescriptor,
AttestationManifest: manifest,
})
}
return images, nil
}
func AddAttestation(ctx context.Context, idx v1.ImageIndex, statement *intoto.Statement, signer dsse.SignerVerifier) (v1.ImageIndex, error) {
if len(statement.Subject) == 0 {
return nil, fmt.Errorf("statement has no subjects")
}
subjectDigests := make(map[string]bool)
for _, subject := range statement.Subject {
subjectDigest := fmt.Sprintf("sha256:%s", subject.Digest["sha256"])
subjectDigests[subjectDigest] = true
}
attestationManifests, err := attestation.GetAttestationManifestsFromIndex(idx)
if err != nil {
return nil, fmt.Errorf("failed to get attestation manifests: %w", err)
}
updatedIndex := false
for _, manifest := range attestationManifests {
if subjectDigests[manifest.Annotations[attestation.DockerReferenceDigest]] {
attestationLayers := []attestation.AttestationLayer{
{
Statement: statement,
MediaType: types.MediaType(intoto.PayloadType),
Annotations: map[string]string{
oci.InTotoPredicateType: statement.PredicateType,
},
},
}
// hard-coding replace to false here, because if it's true we will remove any unsigned statements, even unrelated ones
newImg, newDec, err := SignLayersAndAddToImage(ctx, attestationLayers, manifest, signer, &attestation.SigningOptions{Replace: false})
if err != nil {
return nil, fmt.Errorf("failed to add signed layers to image: %w", err)
}
idx, err = addImageToIndex(idx, newImg, newDec, manifest)
if err != nil {
return nil, fmt.Errorf("failed to add attestation image to index: %w", err)
}
updatedIndex = true
}
}
if !updatedIndex {
return nil, fmt.Errorf("no attestation manifest found for statement")
}
return idx, nil
}
func SignLayersAndAddToImage(
ctx context.Context,
attestationLayers []attestation.AttestationLayer,
manifest attestation.AttestationManifest,
signer dsse.SignerVerifier,
opts *attestation.SigningOptions) (v1.Image, *v1.Descriptor, error) {
signedLayers, err := signLayers(ctx, attestationLayers, signer, opts)
if err != nil {
return nil, nil, fmt.Errorf("failed to sign attestations: %w", err)
}
newImg, err := addSignedLayers(signedLayers, manifest, opts)
if err != nil {
return nil, nil, fmt.Errorf("failed to add signed layers: %w", err)
}
if !opts.SkipSubject {
newImg = mutate.Subject(newImg, *manifest.SubjectDescriptor).(v1.Image)
}
newDesc, err := partial.Descriptor(newImg)
if err != nil {
return nil, nil, fmt.Errorf("failed to get descriptor: %w", err)
}
cf, err := manifest.Attestation.Image.ConfigFile()
if err != nil {
return nil, nil, fmt.Errorf("failed to get config file: %w", err)
}
newDesc.Platform = cf.Platform()
if newDesc.Platform == nil {
newDesc.Platform = &v1.Platform{
Architecture: "unknown",
OS: "unknown",
}
}
newDesc.MediaType = manifest.MediaType
newDesc.Annotations = manifest.Annotations
return newImg, newDesc, nil
}
func addImageToIndex(
idx v1.ImageIndex,
img v1.Image,
desc *v1.Descriptor,
manifest attestation.AttestationManifest,
) (v1.ImageIndex, error) {
idx = mutate.RemoveManifests(idx, match.Digests(manifest.Digest))
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
Add: img,
Descriptor: *desc,
})
return idx, nil
}
// signLayers signs each intoto attestation layer with the given signer
func signLayers(ctx context.Context, layers []attestation.AttestationLayer, signer dsse.SignerVerifier, opts *attestation.SigningOptions) ([]mutate.Addendum, error) {
var signedLayers []mutate.Addendum
for _, layer := range layers {
// only sign intoto layers
if layer.MediaType != types.MediaType(intoto.PayloadType) {
continue
}
// mark attestation as experimental
layer.Annotations[InTotoReferenceLifecycleStage] = LifecycleStageExperimental
// sign the statement
env, err := signInTotoStatement(ctx, layer.Statement, signer, opts)
if err != nil {
return nil, fmt.Errorf("failed to sign statement: %w", err)
}
mediaType, err := attestation.DSSEMediaType(layer.Statement.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)
}
newLayer := static.NewLayer(data, types.MediaType(mediaType))
withAnnotations := mutate.Addendum{
Layer: newLayer,
Annotations: layer.Annotations,
}
signedLayers = append(signedLayers, withAnnotations)
}
return signedLayers, nil
}
func signInTotoStatement(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *attestation.SigningOptions) (*attestation.Envelope, error) {
payload, err := json.Marshal(statement)
if err != nil {
return nil, fmt.Errorf("failed to marshal statement: %w", err)
}
env, err := attestation.SignDSSE(ctx, payload, signer, opts)
if err != nil {
return nil, fmt.Errorf("failed to sign statement: %w", err)
}
return env, nil
}
// addSignedLayers adds signed layers to a new or existing attestation image
func addSignedLayers(signedLayers []mutate.Addendum, manifest attestation.AttestationManifest, opts *attestation.SigningOptions) (v1.Image, error) {
withAnnotations := func(img v1.Image) v1.Image {
// this is handy when dealing with referrers
return mutate.Annotations(img, map[string]string{
attestation.DockerReferenceType: attestation.AttestationManifestType,
attestation.DockerReferenceDigest: manifest.SubjectDescriptor.Digest.String(),
}).(v1.Image)
}
var err error
if opts.Replace {
// create a new attestation image with only signed layers
newImg := empty.Image
newImg = mutate.MediaType(newImg, manifest.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 signed layer: %w", err)
}
}
// add any existing unsigned (non-intoto) layers to the new image
for _, layer := range manifest.Attestation.Layers {
if layer.MediaType != types.MediaType(intoto.PayloadType) {
newImg, err = mutate.AppendLayers(newImg, layer.Layer)
if err != nil {
return nil, fmt.Errorf("failed to append unsigned layer: %w", err)
}
}
}
return withAnnotations(newImg), nil
}
// Add signed layers to the existing image
for _, layer := range signedLayers {
manifest.Attestation.Image, err = mutate.Append(manifest.Attestation.Image, layer)
if err != nil {
return nil, fmt.Errorf("failed to append layer: %w", err)
manifest.AddAttestation(ctx, signer, layer.Statement, opts)
}
}
return withAnnotations(manifest.Attestation.Image), nil
return attestationManifests, nil
}

View File

@@ -2,7 +2,6 @@ package attest
import (
"encoding/json"
"fmt"
"path/filepath"
"testing"
@@ -41,7 +40,6 @@ func TestSignVerifyOCILayout(t *testing.T) {
expectedAttestations int
replace bool
}{
{"signed replaced", UnsignedTestImage, 0, 4, true},
{"without replace", UnsignedTestImage, 4, 4, false},
// image without provenance doesn't fail
@@ -57,11 +55,13 @@ func TestSignVerifyOCILayout(t *testing.T) {
opts := &attestation.SigningOptions{
Replace: tc.replace,
}
attIdx, err := oci.SubjectIndexFromPath(tc.TestImage)
attIdx, err := oci.IndexFromPath(tc.TestImage)
require.NoError(t, err)
signedIndex, err := Sign(ctx, attIdx.Index, signer, opts)
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
signedIndex := attIdx.Index
signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests)
require.NoError(t, err)
// output signed attestations
idx := v1.ImageIndex(empty.Index)
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
@@ -88,8 +88,8 @@ func TestSignVerifyOCILayout(t *testing.T) {
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, predicate, stmt.Annotations[attestation.InTotoPredicateType], "expected predicate-type annotation to be set to %s, got %s", predicate, stmt.Annotations[attestation.InTotoPredicateType])
assert.Equalf(t, attestation.LifecycleStageExperimental, stmt.Annotations[attestation.InTotoReferenceLifecycleStage], "expected reference lifecycle stage annotation to be set to %s, got %s", attestation.LifecycleStageExperimental, stmt.Annotations[attestation.InTotoReferenceLifecycleStage])
}
}
assert.Equalf(t, tc.expectedAttestations, len(allEnvelopes), "expected %d attestations, got %d", tc.expectedAttestations, len(allEnvelopes))
@@ -100,70 +100,6 @@ func TestSignVerifyOCILayout(t *testing.T) {
}
}
func TestAddAttestation(t *testing.T) {
ctx, signer := test.Setup(t)
expectedAttestations := 2
expectedStatements := 4
outputLayout := test.CreateTempDir(t, "", TestTempDir)
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
require.NoError(t, err)
statementToAdd := &intoto.Statement{
StatementHeader: intoto.StatementHeader{
PredicateType: attestation.VSAPredicateType,
Type: intoto.StatementInTotoV01,
Subject: []intoto.Subject{
{
Name: attIdx.Name,
Digest: map[string]string{
"sha256": "da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620",
},
},
{
Name: attIdx.Name,
Digest: map[string]string{
"sha256": "7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e",
},
},
},
},
}
signedIndex, err := AddAttestation(ctx, attIdx.Index, statementToAdd, signer)
require.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)
require.NoError(t, err)
var allEnvelopes []*test.AnnotatedStatement
mt, _ := attestation.DSSEMediaType(attestation.VSAPredicateType)
statements, err := test.ExtractAnnotatedStatements(outputLayout, mt)
require.NoError(t, err)
allEnvelopes = append(allEnvelopes, statements...)
for _, stmt := range statements {
assert.Equalf(t, attestation.VSAPredicateType, stmt.Annotations[oci.InTotoPredicateType], "expected predicate-type annotation to be set to %s, got %s", attestation.VSAPredicateType, 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, expectedAttestations, len(allEnvelopes), "expected %d attestations, got %d", expectedAttestations, len(allEnvelopes))
statements, err = test.ExtractAnnotatedStatements(outputLayout, intoto.PayloadType)
fmt.Printf("statements: %+v\n", statements)
require.NoError(t, err)
assert.Equalf(t, expectedStatements, len(statements), "expected %d statement, got %d", expectedStatements, len(statements))
}
func TestAddSignedLayerAnnotations(t *testing.T) {
testCases := []struct {
name string
@@ -176,33 +112,29 @@ func TestAddSignedLayerAnnotations(t *testing.T) {
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
opts := &attestation.SigningOptions{
Replace: tc.replace,
}
manifest := attestation.AttestationManifest{
originalLayer := &attestation.AttestationLayer{
Layer: testLayer,
Statement: &intoto.Statement{},
Annotations: map[string]string{"test": "test"},
}
manifest := &attestation.AttestationManifest{
MediaType: mediaType,
Attestation: attestation.AttestationImage{
Attestation: &attestation.AttestationImage{
Image: empty.Image,
Layers: []attestation.AttestationLayer{
{
Layer: testLayer,
Statement: &intoto.Statement{},
},
Layers: []*attestation.AttestationLayer{
originalLayer,
},
},
SubjectDescriptor: &v1.Descriptor{},
}
newImg, err := addSignedLayers(signedLayers, manifest, opts)
err := manifest.AddOrReplaceLayer(originalLayer, opts)
newImg := manifest.Attestation.Image
require.NoError(t, err)
mf, _ := newImg.RawManifest()
type Annotations struct {

View File

@@ -7,11 +7,6 @@ import (
intoto "github.com/in-toto/in-toto-golang/in_toto"
)
const (
InTotoReferenceLifecycleStage = "vnd.docker.lifecycle-stage"
LifecycleStageExperimental = "experimental"
)
type Outcome string
const (

View File

@@ -80,10 +80,13 @@ func TestVSA(t *testing.T) {
opts := &attestation.SigningOptions{
Replace: true,
}
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
assert.NoError(t, err)
signedIndex, err := Sign(ctx, attIdx.Index, signer, opts)
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
assert.NoError(t, err)
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
signedIndex := attIdx.Index
signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests)
require.NoError(t, err)
// output signed attestations
idx := v1.ImageIndex(empty.Index)
@@ -136,10 +139,13 @@ func TestVerificationFailure(t *testing.T) {
opts := &attestation.SigningOptions{
Replace: true,
}
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
assert.NoError(t, err)
signedIndex, err := Sign(ctx, attIdx.Index, signer, opts)
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
assert.NoError(t, err)
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
signedIndex := attIdx.Index
signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests)
require.NoError(t, err)
// output signed attestations
idx := v1.ImageIndex(empty.Index)
@@ -201,7 +207,7 @@ func TestSignVerifyNoTL(t *testing.T) {
{name: "no tl", signTL: false, policyDir: PassPolicyDir, success: false},
}
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
assert.NoError(t, err)
for _, tc := range testCases {
@@ -211,9 +217,11 @@ func TestSignVerifyNoTL(t *testing.T) {
SkipTL: tc.signTL,
}
signedIndex, err := Sign(ctx, attIdx.Index, signer, opts)
assert.NoError(t, err)
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
signedIndex := attIdx.Index
signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests)
require.NoError(t, err)
// output signed attestations
idx := v1.ImageIndex(empty.Index)
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{

View File

@@ -1,18 +1,24 @@
package attestation
import (
"context"
"encoding/json"
"fmt"
"maps"
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"
"github.com/secure-systems-lab/go-securesystemslib/dsse"
)
// GetAttestationManifestsFromIndex extracts all attestation manifests from an index
func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]AttestationManifest, error) {
func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*AttestationManifest, error) {
idx, err := index.IndexManifest()
if err != nil {
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
@@ -22,9 +28,8 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]AttestationManifes
subjects[subject.Digest.String()] = &subject
}
var attestationManifests []AttestationManifest
var attestationManifests []*AttestationManifest
for _, manifest := range idx.Manifests {
if manifest.Annotations[DockerReferenceType] == AttestationManifestType {
subject := subjects[manifest.Annotations[DockerReferenceDigest]]
if subject == nil {
@@ -39,10 +44,10 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]AttestationManifes
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
}
attestationManifests = append(attestationManifests,
AttestationManifest{
Descriptor: manifest,
&AttestationManifest{
Descriptor: &manifest,
SubjectDescriptor: subject,
Attestation: AttestationImage{
Attestation: &AttestationImage{
Layers: attestationLayers,
Image: attestationImage},
MediaType: manifest.MediaType,
@@ -54,12 +59,12 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]AttestationManifes
}
// GetAttestationsFromImage extracts all attestation layers from an image
func GetAttestationsFromImage(image v1.Image) ([]AttestationLayer, error) {
func GetAttestationsFromImage(image v1.Image) ([]*AttestationLayer, error) {
layers, err := image.Layers()
if err != nil {
return nil, fmt.Errorf("failed to extract layers from image: %w", err)
}
var attestationLayers []AttestationLayer
var attestationLayers []*AttestationLayer
for _, layer := range layers {
// parse layer blob as json
r, err := layer.Uncompressed()
@@ -85,7 +90,162 @@ func GetAttestationsFromImage(image v1.Image) ([]AttestationLayer, error) {
return nil, fmt.Errorf("failed to decode statement layer contents: %w", err)
}
}
attestationLayers = append(attestationLayers, AttestationLayer{Layer: layer, MediaType: mt, Statement: stmt, Annotations: ann})
attestationLayers = append(attestationLayers, &AttestationLayer{Layer: layer, MediaType: mt, Statement: stmt, Annotations: ann})
}
return attestationLayers, nil
}
func (manifest *AttestationManifest) AddAttestation(ctx context.Context, signer dsse.SignerVerifier, statement *intoto.Statement, opts *SigningOptions) error {
layer, err := createSignedImageLayer(ctx, statement, signer, opts)
if err != nil {
return fmt.Errorf("failed to create signed layer: %w", err)
}
newImg, newDesc, err := addLayerToImage(manifest, layer, opts)
if err != nil {
return fmt.Errorf("failed to add signed layers to image: %w", err)
}
manifest.Attestation.Image = newImg
manifest.Descriptor = newDesc
return nil
}
func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*AttestationLayer, error) {
// sign the statement
env, err := SignInTotoStatement(ctx, statement, signer, opts)
if err != nil {
return nil, fmt.Errorf("failed to sign statement: %w", err)
}
mediaType, err := DSSEMediaType(statement.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)
}
return &AttestationLayer{
Statement: statement,
MediaType: types.MediaType(intoto.PayloadType),
Annotations: map[string]string{
InTotoPredicateType: statement.PredicateType,
InTotoReferenceLifecycleStage: LifecycleStageExperimental,
},
Layer: static.NewLayer(data, types.MediaType(mediaType)),
}, nil
}
func SignInTotoStatement(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*Envelope, error) {
payload, err := json.Marshal(statement)
if err != nil {
return nil, fmt.Errorf("failed to marshal statement: %w", err)
}
env, err := SignDSSE(ctx, payload, signer, opts)
if err != nil {
return nil, fmt.Errorf("failed to sign statement: %w", err)
}
return env, nil
}
func addLayerToImage(
manifest *AttestationManifest,
layer *AttestationLayer,
opts *SigningOptions) (v1.Image, *v1.Descriptor, error) {
err := manifest.AddOrReplaceLayer(layer, opts)
if err != nil {
return nil, nil, fmt.Errorf("failed to add signed layers: %w", err)
}
newImg := manifest.Attestation.Image
if !opts.SkipSubject {
newImg = mutate.Subject(newImg, *manifest.SubjectDescriptor).(v1.Image)
}
newDesc, err := partial.Descriptor(newImg)
if err != nil {
return nil, nil, fmt.Errorf("failed to get descriptor: %w", err)
}
cf, err := manifest.Attestation.Image.ConfigFile()
if err != nil {
return nil, nil, fmt.Errorf("failed to get config file: %w", err)
}
newDesc.Platform = cf.Platform()
if newDesc.Platform == nil {
newDesc.Platform = &v1.Platform{
Architecture: "unknown",
OS: "unknown",
}
}
newDesc.MediaType = manifest.MediaType
newDesc.Annotations = manifest.Annotations
return newImg, newDesc, nil
}
// AddOrReplaceLayer adds signed layers to a new or existing attestation image
// NOTE: the pointers attestation.AttestationLayer.Statement are compared when replacing,
// so make sure you are signing a layer extracted from the original attestation-manifest image!
func (manifest *AttestationManifest) AddOrReplaceLayer(signedLayer *AttestationLayer, opts *SigningOptions) error {
var err error
// always create a new image from all the layers
newImg := empty.Image
newImg = mutate.Annotations(newImg, map[string]string{
DockerReferenceType: AttestationManifestType,
DockerReferenceDigest: manifest.SubjectDescriptor.Digest.String(),
}).(v1.Image)
newImg = mutate.MediaType(newImg, manifest.MediaType)
newImg = mutate.ConfigMediaType(newImg, "application/vnd.oci.image.config.v1+json")
add := mutate.Addendum{
Layer: signedLayer.Layer,
Annotations: signedLayer.Annotations,
}
newImg, err = mutate.Append(newImg, add)
if err != nil {
return fmt.Errorf("failed to add signed layer to image: %w", err)
}
layers := make([]*AttestationLayer, 0)
for _, layer := range manifest.Attestation.Layers {
if layer.Statement == signedLayer.Statement && opts.Replace {
continue
}
add := mutate.Addendum{
Layer: layer.Layer,
Annotations: layer.Annotations,
}
newImg, err = mutate.Append(newImg, add)
layers = append(layers, layer)
if err != nil {
return fmt.Errorf("failed to add layer to image: %w", err)
}
}
manifest.Attestation.Layers = append(layers, signedLayer)
manifest.Attestation.Image = newImg
return nil
}
func AddImageToIndex(
idx v1.ImageIndex,
manifest *AttestationManifest,
) (v1.ImageIndex, error) {
idx = mutate.RemoveManifests(idx, match.Digests(manifest.Digest))
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
Add: manifest.Attestation.Image,
Descriptor: *manifest.Descriptor,
})
return idx, nil
}
func AddImagesToIndex(
idx v1.ImageIndex,
manifests []*AttestationManifest,
) (v1.ImageIndex, error) {
for _, manifest := range manifests {
var err error
idx, err = AddImageToIndex(idx, manifest)
if err != nil {
return nil, fmt.Errorf("failed to add image to index: %w", err)
}
}
return idx, nil
}

View File

@@ -97,7 +97,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
Replace: true,
SkipSubject: tc.skipSubject,
}
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
require.NoError(t, err)
indexName := fmt.Sprintf("%s/repo:root", u.Host)
@@ -108,15 +108,18 @@ func TestAttestationReferenceTypes(t *testing.T) {
require.NoError(t, err)
repo := fmt.Sprintf("%s/referrers", ru.Host)
tc.referrersRepo = repo
images, err := attest.SignedAttestationImages(ctx, attIdx.Index, signer, opts)
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
for _, img := range images {
err = mirror.PushImageToRegistry(img.Image, fmt.Sprintf("%s:tag-does-not-matter", repo))
for _, img := range signedManifests {
err = mirror.PushImageToRegistry(img.Attestation.Image, fmt.Sprintf("%s:tag-does-not-matter", repo))
require.NoError(t, err)
}
} else {
signedIndex, err := attest.Sign(ctx, attIdx.Index, signer, opts)
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
signedIndex := attIdx.Index
signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests)
require.NoError(t, err)
err = mirror.PushIndexToRegistry(signedIndex, indexName)
require.NoError(t, err)
@@ -215,20 +218,20 @@ func TestReferencesInDifferentRepo(t *testing.T) {
Replace: true,
SkipTL: true,
}
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
require.NoError(t, err)
indexName := fmt.Sprintf("%s/%s:latest", serverUrl.Host, repoName)
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
require.NoError(t, err)
signedImages, err := attest.SignedAttestationImages(ctx, attIdx.Index, signer, opts)
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
// push signed attestation image to the ref server
for _, img := range signedImages {
for _, img := range signedManifests {
// push references using subject-digest.att convention
err = mirror.PushImageToRegistry(img.Image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerUrl.Host, repoName))
err = mirror.PushImageToRegistry(img.Attestation.Image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerUrl.Host, repoName))
require.NoError(t, err)
}
mfs2, err := attIdx.Index.IndexManifest()

View File

@@ -12,12 +12,15 @@ import (
)
const (
DockerReferenceType = "vnd.docker.reference.type"
AttestationManifestType = "attestation-manifest"
DockerReferenceDigest = "vnd.docker.reference.digest"
DockerDsseExtKind = "application/vnd.docker.attestation-verification.v1+json"
RekorTlExtKind = "Rekor"
OCIDescriptorDSSEMediaType = ociv1.MediaTypeDescriptor + "+dsse"
DockerReferenceType = "vnd.docker.reference.type"
AttestationManifestType = "attestation-manifest"
InTotoPredicateType = "in-toto.io/predicate-type"
DockerReferenceDigest = "vnd.docker.reference.digest"
DockerDsseExtKind = "application/vnd.docker.attestation-verification.v1+json"
RekorTlExtKind = "Rekor"
OCIDescriptorDSSEMediaType = ociv1.MediaTypeDescriptor + "+dsse"
InTotoReferenceLifecycleStage = "vnd.docker.lifecycle-stage"
LifecycleStageExperimental = "experimental"
)
var base64Encoding = base64.StdEncoding.Strict()
@@ -30,19 +33,19 @@ type AttestationLayer struct {
}
type AttestationImage struct {
Layers []AttestationLayer
Layers []*AttestationLayer
Image v1.Image
}
type SignedAttestationImage struct {
Image v1.Image
Descriptor *v1.Descriptor
AttestationManifest AttestationManifest
AttestationManifest *AttestationManifest
}
type AttestationManifest struct {
Descriptor v1.Descriptor
Attestation AttestationImage
Descriptor *v1.Descriptor
Attestation *AttestationImage
MediaType types.MediaType
Annotations map[string]string
Digest v1.Hash

View File

@@ -14,7 +14,7 @@ import (
func TestRegistryAuth(t *testing.T) {
UnsignedTestImage := filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
attIdx, err := oci.SubjectIndexFromPath(UnsignedTestImage)
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
require.NoError(t, err)
// test cases for ecr, gcr and dockerhub
testCases := []struct {
@@ -27,7 +27,7 @@ func TestRegistryAuth(t *testing.T) {
t.Run(tc.Image, func(t *testing.T) {
err := mirror.PushIndexToRegistry(attIdx.Index, tc.Image)
require.NoError(t, err)
_, err = oci.SubjectIndexFromRemote(tc.Image)
_, err = oci.IndexFromRemote(tc.Image)
require.NoError(t, err)
})
}

View File

@@ -6,6 +6,7 @@ import (
"fmt"
"strings"
"github.com/docker/attest/pkg/attestation"
att "github.com/docker/attest/pkg/attestation"
v1 "github.com/google/go-containerregistry/pkg/v1"
"github.com/google/go-containerregistry/pkg/v1/layout"
@@ -50,7 +51,7 @@ func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType stri
var envs []*att.Envelope
manifest := r.AttestationManifest.Manifest
for i, l := range manifest.Layers {
if l.Annotations[InTotoPredicateType] != predicateType {
if l.Annotations[attestation.InTotoPredicateType] != predicateType {
continue
}
layer := layers[i]
@@ -121,7 +122,7 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*Atte
}
}
for _, mf := range mfs2.Manifests {
if mf.Annotations[att.DockerReferenceType] != AttestationManifestType {
if mf.Annotations[att.DockerReferenceType] != attestation.AttestationManifestType {
continue
}

View File

@@ -57,7 +57,7 @@ func ExtractEnvelopes(ia *AttestationManifest, predicateType string) ([]*att.Env
for i, l := range manifest.Layers {
if (strings.HasPrefix(string(l.MediaType), "application/vnd.in-toto.")) &&
strings.HasSuffix(string(l.MediaType), "+dsse") &&
l.Annotations[InTotoPredicateType] == predicateType {
l.Annotations[att.InTotoPredicateType] == predicateType {
reader, err := layers[i].Uncompressed()
if err != nil {
return nil, fmt.Errorf("failed to get layer contents: %w", err)

View File

@@ -79,7 +79,7 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context) error {
if err != nil {
return fmt.Errorf("failed to get manifest: %w", err)
}
if manifest.Annotations[att.DockerReferenceType] != AttestationManifestType {
if manifest.Annotations[att.DockerReferenceType] != att.AttestationManifestType {
continue
}
if manifest.Annotations[att.DockerReferenceDigest] != subjectDigest {

View File

@@ -29,9 +29,12 @@ func TestRegistry(t *testing.T) {
Replace: true,
SkipSubject: true,
}
attIdx, err := oci.SubjectIndexFromPath(oci.UnsignedTestImage)
attIdx, err := oci.IndexFromPath(oci.UnsignedTestImage)
require.NoError(t, err)
signedIndex, err := attest.Sign(ctx, attIdx.Index, signer, opts)
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
require.NoError(t, err)
signedIndex := attIdx.Index
signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests)
require.NoError(t, err)
indexName := fmt.Sprintf("%s/repo:root", u.Host)

View File

@@ -11,17 +11,15 @@ import (
)
const (
AttestationManifestType = "attestation-manifest"
InTotoPredicateType = "in-toto.io/predicate-type"
OciReferenceTarget = "org.opencontainers.image.ref.name"
LocalPrefix = "oci://"
RegistryPrefix = "docker://"
OCI SourceType = "OCI"
Docker SourceType = "Docker"
OciReferenceTarget = "org.opencontainers.image.ref.name"
LocalPrefix = "oci://"
RegistryPrefix = "docker://"
OCI SourceType = "OCI"
Docker SourceType = "Docker"
)
type SourceType string
type SubjectIndex struct {
type NamedIndex struct {
Index v1.ImageIndex
Name string
}
@@ -42,7 +40,7 @@ type ImageSpec struct {
Platform *v1.Platform
}
func SubjectIndexFromPath(path string) (*SubjectIndex, error) {
func IndexFromPath(path string) (*NamedIndex, error) {
wrapperIdx, err := layout.ImageIndexFromPath(path)
if err != nil {
return nil, fmt.Errorf("failed to load image index: %w", err)
@@ -59,13 +57,13 @@ func SubjectIndexFromPath(path string) (*SubjectIndex, error) {
if err != nil {
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
}
return &SubjectIndex{
return &NamedIndex{
Index: idx,
Name: imageName,
}, nil
}
func SubjectIndexFromRemote(image string) (*SubjectIndex, error) {
func IndexFromRemote(image string) (*NamedIndex, error) {
ref, err := name.ParseReference(image)
if err != nil {
return nil, fmt.Errorf("failed to parse image reference %s: %w", image, err)
@@ -76,17 +74,17 @@ func SubjectIndexFromRemote(image string) (*SubjectIndex, error) {
if err != nil {
return nil, fmt.Errorf("failed to pull image %s: %w", image, err)
}
return &SubjectIndex{
return &NamedIndex{
Index: idx,
Name: image,
}, nil
}
func LoadSubjectIndex(input *ImageSpec) (*SubjectIndex, error) {
func LoadIndex(input *ImageSpec) (*NamedIndex, error) {
if input.Type == OCI {
return SubjectIndexFromPath(input.Identifier)
return IndexFromPath(input.Identifier)
} else {
return SubjectIndexFromRemote(input.Identifier)
return IndexFromRemote(input.Identifier)
}
}