Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10dab6ed25 | ||
|
|
cd964d4287 | ||
|
|
6e1ff664a3 | ||
|
|
728f1611e4 | ||
|
|
d9a23a08a4 | ||
|
|
20f4403d44 | ||
|
|
549c89e841 | ||
|
|
5faf0801ee | ||
|
|
116c668183 | ||
|
|
db98f597f2 | ||
|
|
34bc47fcec | ||
|
|
9c822317aa | ||
|
|
a605278749 | ||
|
|
c3ece3f02d |
3
go.mod
3
go.mod
@@ -31,6 +31,9 @@ require (
|
|||||||
sigs.k8s.io/yaml v1.4.0
|
sigs.k8s.io/yaml v1.4.0
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// fork of a fork (in case it goes away) with changes to support ArtifactType (https://github.com/google/go-containerregistry/pull/1931)
|
||||||
|
replace github.com/google/go-containerregistry v0.20.0 => github.com/kipz/go-containerregistry v0.0.0-20240423201245-bf57eace21f2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
cloud.google.com/go v0.115.0 // indirect
|
cloud.google.com/go v0.115.0 // indirect
|
||||||
cloud.google.com/go/auth v0.7.0 // indirect
|
cloud.google.com/go/auth v0.7.0 // indirect
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -344,8 +344,6 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
|
|||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/go-containerregistry v0.20.0 h1:wRqHpOeVh3DnenOrPy9xDOLdnLatiGuuNRVelR2gSbg=
|
|
||||||
github.com/google/go-containerregistry v0.20.0/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
|
|
||||||
github.com/google/go-github/v55 v55.0.0 h1:4pp/1tNMB9X/LuAhs5i0KQAE40NmiR/y6prLNb9x9cg=
|
github.com/google/go-github/v55 v55.0.0 h1:4pp/1tNMB9X/LuAhs5i0KQAE40NmiR/y6prLNb9x9cg=
|
||||||
github.com/google/go-github/v55 v55.0.0/go.mod h1:JLahOTA1DnXzhxEymmFF5PP2tSS9JVNj68mSZNDwskA=
|
github.com/google/go-github/v55 v55.0.0/go.mod h1:JLahOTA1DnXzhxEymmFF5PP2tSS9JVNj68mSZNDwskA=
|
||||||
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
|
||||||
@@ -417,6 +415,8 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm
|
|||||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/kipz/go-containerregistry v0.0.0-20240423201245-bf57eace21f2 h1:Q8a+lW1mDc5ta1kelfIVqXl/DC+KQg6PG/F33kCC9TA=
|
||||||
|
github.com/kipz/go-containerregistry v0.0.0-20240423201245-bf57eace21f2/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI=
|
||||||
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
|
||||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||||
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU=
|
||||||
|
|||||||
64
internal/test/mocks.go
Normal file
64
internal/test/mocks.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/docker/attest/pkg/attestation"
|
||||||
|
"github.com/docker/attest/pkg/oci"
|
||||||
|
"github.com/docker/attest/pkg/signerverifier"
|
||||||
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
|
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MockResolver struct {
|
||||||
|
Envs []*attestation.Envelope
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MockResolver) Attestations(ctx context.Context, mediaType string) ([]*attestation.Envelope, error) {
|
||||||
|
return r.Envs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MockResolver) ImageName(ctx context.Context) (string, error) {
|
||||||
|
return "library/alpine:latest", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MockResolver) ImageDescriptor(ctx context.Context) (*v1.Descriptor, error) {
|
||||||
|
digest, err := v1.NewHash("sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &v1.Descriptor{
|
||||||
|
Digest: digest,
|
||||||
|
Size: 1234,
|
||||||
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||||
|
}, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r MockResolver) ImagePlatform(ctx context.Context) (*v1.Platform, error) {
|
||||||
|
return oci.ParsePlatform("linux/amd64")
|
||||||
|
}
|
||||||
|
|
||||||
|
type MockRegistryResolver struct {
|
||||||
|
Subject *v1.Descriptor
|
||||||
|
ImageNameStr string
|
||||||
|
*MockResolver
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MockRegistryResolver) ImageDescriptor(ctx context.Context) (*v1.Descriptor, error) {
|
||||||
|
return r.Subject, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *MockRegistryResolver) ImageName(ctx context.Context) (string, error) {
|
||||||
|
return r.ImageNameStr, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetMockSigner(ctx context.Context) (dsse.SignerVerifier, error) {
|
||||||
|
priv, err := os.ReadFile(filepath.Join("..", "..", "test", "testdata", "test-signing-key.pem"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return signerverifier.LoadKeyPair(priv)
|
||||||
|
}
|
||||||
@@ -6,7 +6,6 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -82,14 +81,6 @@ func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) {
|
|||||||
return ctx, signer
|
return ctx, signer
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetMockSigner(ctx context.Context) (dsse.SignerVerifier, error) {
|
|
||||||
priv, err := os.ReadFile(filepath.Join("..", "..", "test", "testdata", "test-signing-key.pem"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return signerverifier.LoadKeyPair(priv)
|
|
||||||
}
|
|
||||||
|
|
||||||
type AnnotatedStatement struct {
|
type AnnotatedStatement struct {
|
||||||
OCIDescriptor *v1.Descriptor
|
OCIDescriptor *v1.Descriptor
|
||||||
InTotoStatement *intoto.Statement
|
InTotoStatement *intoto.Statement
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func ExampleSign_remote() {
|
|||||||
|
|
||||||
// configure signing options
|
// configure signing options
|
||||||
opts := &attestation.SigningOptions{
|
opts := &attestation.SigningOptions{
|
||||||
Replace: true, // replace unsigned intoto statements with signed intoto attestations, otherwise leave in place
|
SkipTL: true, // skip trust logging to a transparency log
|
||||||
}
|
}
|
||||||
|
|
||||||
// load image index with unsigned attestation-manifests
|
// load image index with unsigned attestation-manifests
|
||||||
@@ -49,7 +49,7 @@ func ExampleSign_remote() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
signedIndex := attIdx.Index
|
signedIndex := attIdx.Index
|
||||||
signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests)
|
signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,8 +18,11 @@ func SignStatements(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVe
|
|||||||
}
|
}
|
||||||
// sign every attestation layer in each manifest
|
// sign every attestation layer in each manifest
|
||||||
for _, manifest := range attestationManifests {
|
for _, manifest := range attestationManifests {
|
||||||
for _, layer := range manifest.Attestation.Layers {
|
for _, layer := range manifest.OriginalLayers {
|
||||||
manifest.AddAttestation(ctx, signer, layer.Statement, opts)
|
err = manifest.AddAttestation(ctx, signer, layer.Statement, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to sign attestation layer %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return attestationManifests, nil
|
return attestationManifests, nil
|
||||||
|
|||||||
@@ -2,13 +2,18 @@ package attest
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/docker/attest/internal/test"
|
"github.com/docker/attest/internal/test"
|
||||||
"github.com/docker/attest/pkg/attestation"
|
"github.com/docker/attest/pkg/attestation"
|
||||||
|
"github.com/docker/attest/pkg/mirror"
|
||||||
"github.com/docker/attest/pkg/oci"
|
"github.com/docker/attest/pkg/oci"
|
||||||
"github.com/docker/attest/pkg/policy"
|
"github.com/docker/attest/pkg/policy"
|
||||||
|
"github.com/google/go-containerregistry/pkg/registry"
|
||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||||
@@ -53,15 +58,13 @@ func TestSignVerifyOCILayout(t *testing.T) {
|
|||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
||||||
opts := &attestation.SigningOptions{
|
opts := &attestation.SigningOptions{}
|
||||||
Replace: tc.replace,
|
|
||||||
}
|
|
||||||
attIdx, err := oci.IndexFromPath(tc.TestImage)
|
attIdx, err := oci.IndexFromPath(tc.TestImage)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
|
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
signedIndex := attIdx.Index
|
signedIndex := attIdx.Index
|
||||||
signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests)
|
signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests, attestation.WithReplacedLayers(tc.replace))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
// output signed attestations
|
// output signed attestations
|
||||||
idx := v1.ImageIndex(empty.Index)
|
idx := v1.ImageIndex(empty.Index)
|
||||||
@@ -102,6 +105,7 @@ func TestSignVerifyOCILayout(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAddSignedLayerAnnotations(t *testing.T) {
|
func TestAddSignedLayerAnnotations(t *testing.T) {
|
||||||
|
ctx, signer := test.Setup(t)
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
replace bool
|
replace bool
|
||||||
@@ -115,27 +119,30 @@ func TestAddSignedLayerAnnotations(t *testing.T) {
|
|||||||
data := []byte("signed")
|
data := []byte("signed")
|
||||||
testLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType))
|
testLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType))
|
||||||
mediaType := types.OCIManifestSchema1
|
mediaType := types.OCIManifestSchema1
|
||||||
opts := &attestation.SigningOptions{
|
opts := &attestation.SigningOptions{}
|
||||||
Replace: tc.replace,
|
|
||||||
}
|
|
||||||
originalLayer := &attestation.AttestationLayer{
|
originalLayer := &attestation.AttestationLayer{
|
||||||
Layer: testLayer,
|
Layer: testLayer,
|
||||||
Statement: &intoto.Statement{},
|
Statement: &intoto.Statement{
|
||||||
|
StatementHeader: intoto.StatementHeader{
|
||||||
|
PredicateType: attestation.VSAPredicateType,
|
||||||
|
},
|
||||||
|
},
|
||||||
Annotations: map[string]string{"test": "test"},
|
Annotations: map[string]string{"test": "test"},
|
||||||
}
|
}
|
||||||
|
|
||||||
manifest := &attestation.AttestationManifest{
|
manifest := &attestation.AttestationManifest{
|
||||||
MediaType: mediaType,
|
OriginalDescriptor: &v1.Descriptor{
|
||||||
Attestation: &attestation.AttestationImage{
|
MediaType: mediaType,
|
||||||
Image: empty.Image,
|
},
|
||||||
Layers: []*attestation.AttestationLayer{
|
OriginalLayers: []*attestation.AttestationLayer{
|
||||||
originalLayer,
|
originalLayer,
|
||||||
},
|
|
||||||
},
|
},
|
||||||
SubjectDescriptor: &v1.Descriptor{},
|
SubjectDescriptor: &v1.Descriptor{},
|
||||||
}
|
}
|
||||||
err := manifest.AddOrReplaceLayer(originalLayer, opts)
|
err := manifest.AddAttestation(ctx, signer, originalLayer.Statement, opts)
|
||||||
newImg := manifest.Attestation.Image
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
newImg, err := manifest.BuildAttestationImage(attestation.WithReplacedLayers(tc.replace))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
mf, _ := newImg.RawManifest()
|
mf, _ := newImg.RawManifest()
|
||||||
type Annotations struct {
|
type Annotations struct {
|
||||||
@@ -152,3 +159,87 @@ func TestAddSignedLayerAnnotations(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSimpleStatementSigning(t *testing.T) {
|
||||||
|
ctx, signer := test.Setup(t)
|
||||||
|
empty := types.MediaType("application/vnd.oci.empty.v1+json")
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
replace bool
|
||||||
|
}{
|
||||||
|
{"replaced", true},
|
||||||
|
{"not replaced", false},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
opts := &attestation.SigningOptions{}
|
||||||
|
statement := &intoto.Statement{
|
||||||
|
StatementHeader: intoto.StatementHeader{
|
||||||
|
PredicateType: attestation.VSAPredicateType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
statement2 := &intoto.Statement{
|
||||||
|
StatementHeader: intoto.StatementHeader{
|
||||||
|
PredicateType: attestation.VSAPredicateType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
digest, err := v1.NewHash("sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620")
|
||||||
|
require.NoError(t, err)
|
||||||
|
subject := &v1.Descriptor{
|
||||||
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||||
|
Digest: digest,
|
||||||
|
}
|
||||||
|
manifest, err := NewAttestationManifest(subject)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = manifest.AddAttestation(ctx, signer, statement, opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = manifest.AddAttestation(ctx, signer, statement2, opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// fake that the manfifest was loaded from a real image
|
||||||
|
manifest.OriginalLayers = manifest.SignedLayers
|
||||||
|
envelopes, err := oci.ExtractEnvelopes(manifest, attestation.VSAPredicateType)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, envelopes, 2)
|
||||||
|
|
||||||
|
newImg, err := manifest.BuildAttestationImage(attestation.WithReplacedLayers(tc.replace))
|
||||||
|
require.NoError(t, err)
|
||||||
|
layers, err := newImg.Layers()
|
||||||
|
require.NoError(t, err)
|
||||||
|
if tc.replace {
|
||||||
|
assert.Len(t, layers, 2)
|
||||||
|
} else {
|
||||||
|
assert.Len(t, layers, 4)
|
||||||
|
}
|
||||||
|
|
||||||
|
newImgs, err := manifest.BuildReferringArtifacts()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, newImgs, 2)
|
||||||
|
for _, img := range newImgs {
|
||||||
|
mf, err := img.Manifest()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, "application/vnd.in-toto+json", mf.ArtifactType)
|
||||||
|
assert.Equal(t, subject.MediaType, mf.MediaType)
|
||||||
|
assert.Equal(t, empty, mf.Config.MediaType)
|
||||||
|
assert.Equal(t, int64(2), mf.Config.Size)
|
||||||
|
assert.Equal(t, "{}", string(mf.Config.Data))
|
||||||
|
layers, err := img.Layers()
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Len(t, layers, 1)
|
||||||
|
}
|
||||||
|
server := httptest.NewServer(registry.New(registry.WithReferrersSupport(true)))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
u, err := url.Parse(server.URL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||||
|
output, err := oci.ParseImageSpecs(indexName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = mirror.SaveReferrers(manifest, output)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"github.com/docker/attest/pkg/policy"
|
"github.com/docker/attest/pkg/policy"
|
||||||
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,9 +28,10 @@ func (o Outcome) StringForVSA() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type VerificationResult struct {
|
type VerificationResult struct {
|
||||||
Outcome Outcome
|
Outcome Outcome
|
||||||
Policy *policy.Policy
|
Policy *policy.Policy
|
||||||
Input *policy.PolicyInput
|
Input *policy.PolicyInput
|
||||||
VSA *intoto.Statement
|
VSA *intoto.Statement
|
||||||
Violations []policy.Violation
|
Violations []policy.Violation
|
||||||
|
SubjectDescriptor *v1.Descriptor
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/docker/attest/pkg/config"
|
"github.com/docker/attest/pkg/config"
|
||||||
"github.com/docker/attest/pkg/oci"
|
"github.com/docker/attest/pkg/oci"
|
||||||
"github.com/docker/attest/pkg/policy"
|
"github.com/docker/attest/pkg/policy"
|
||||||
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -60,7 +61,7 @@ func Verify(ctx context.Context, src *oci.ImageSpec, opts *policy.PolicyOptions)
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func ToPolicyResult(p *policy.Policy, input *policy.PolicyInput, result *policy.Result) (*VerificationResult, error) {
|
func toVerificationResult(p *policy.Policy, input *policy.PolicyInput, result *policy.Result) (*VerificationResult, error) {
|
||||||
dgst, err := oci.SplitDigest(input.Digest)
|
dgst, err := oci.SplitDigest(input.Digest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to split digest: %w", err)
|
return nil, fmt.Errorf("failed to split digest: %w", err)
|
||||||
@@ -112,10 +113,11 @@ func ToPolicyResult(p *policy.Policy, input *policy.PolicyInput, result *policy.
|
|||||||
}
|
}
|
||||||
|
|
||||||
func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, pctx *policy.Policy) (*VerificationResult, error) {
|
func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, pctx *policy.Policy) (*VerificationResult, error) {
|
||||||
digest, err := resolver.ImageDigest(ctx)
|
desc, err := resolver.ImageDescriptor(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get image digest: %w", err)
|
return nil, fmt.Errorf("failed to get image descriptor: %w", err)
|
||||||
}
|
}
|
||||||
|
digest := desc.Digest.String()
|
||||||
name, err := resolver.ImageName(ctx)
|
name, err := resolver.ImageName(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get image name: %w", err)
|
return nil, fmt.Errorf("failed to get image name: %w", err)
|
||||||
@@ -155,5 +157,20 @@ func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, p
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("policy evaluation failed: %w", err)
|
return nil, fmt.Errorf("policy evaluation failed: %w", err)
|
||||||
}
|
}
|
||||||
return ToPolicyResult(pctx, input, result)
|
verificationResult, err := toVerificationResult(pctx, input, result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to convert to policy result: %w", err)
|
||||||
|
}
|
||||||
|
verificationResult.SubjectDescriptor = desc
|
||||||
|
return verificationResult, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAttestationManifest(subject *v1.Descriptor) (*attestation.AttestationManifest, error) {
|
||||||
|
return &attestation.AttestationManifest{
|
||||||
|
OriginalDescriptor: &v1.Descriptor{
|
||||||
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||||
|
},
|
||||||
|
OriginalLayers: []*attestation.AttestationLayer{},
|
||||||
|
SubjectDescriptor: subject,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func TestVerifyAttestations(t *testing.T) {
|
|||||||
var env = new(attestation.Envelope)
|
var env = new(attestation.Envelope)
|
||||||
err = json.Unmarshal(ex, env)
|
err = json.Unmarshal(ex, env)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
resolver := &oci.MockResolver{
|
resolver := &test.MockResolver{
|
||||||
Envs: []*attestation.Envelope{env},
|
Envs: []*attestation.Envelope{env},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,15 +77,13 @@ func TestVSA(t *testing.T) {
|
|||||||
// setup an image with signed attestations
|
// setup an image with signed attestations
|
||||||
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
||||||
|
|
||||||
opts := &attestation.SigningOptions{
|
opts := &attestation.SigningOptions{}
|
||||||
Replace: true,
|
|
||||||
}
|
|
||||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
|
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
signedIndex := attIdx.Index
|
signedIndex := attIdx.Index
|
||||||
signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests)
|
signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// output signed attestations
|
// output signed attestations
|
||||||
@@ -136,15 +134,13 @@ func TestVerificationFailure(t *testing.T) {
|
|||||||
// setup an image with signed attestations
|
// setup an image with signed attestations
|
||||||
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
||||||
|
|
||||||
opts := &attestation.SigningOptions{
|
opts := &attestation.SigningOptions{}
|
||||||
Replace: true,
|
|
||||||
}
|
|
||||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
|
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
signedIndex := attIdx.Index
|
signedIndex := attIdx.Index
|
||||||
signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests)
|
signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests, attestation.WithReplacedLayers(true))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// output signed attestations
|
// output signed attestations
|
||||||
@@ -215,14 +211,13 @@ func TestSignVerify(t *testing.T) {
|
|||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
opts := &attestation.SigningOptions{
|
opts := &attestation.SigningOptions{
|
||||||
Replace: true,
|
SkipTL: tc.signTL,
|
||||||
SkipTL: tc.signTL,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
|
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
signedIndex := attIdx.Index
|
signedIndex := attIdx.Index
|
||||||
signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests)
|
signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests, attestation.WithReplacedLayers(true))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
imageName := tc.imageName
|
imageName := tc.imageName
|
||||||
|
|||||||
@@ -29,15 +29,15 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*AttestationManife
|
|||||||
}
|
}
|
||||||
|
|
||||||
var attestationManifests []*AttestationManifest
|
var attestationManifests []*AttestationManifest
|
||||||
for _, manifest := range idx.Manifests {
|
for _, desc := range idx.Manifests {
|
||||||
if manifest.Annotations[DockerReferenceType] == AttestationManifestType {
|
if desc.Annotations[DockerReferenceType] == AttestationManifestType {
|
||||||
subject := subjects[manifest.Annotations[DockerReferenceDigest]]
|
subject := subjects[desc.Annotations[DockerReferenceDigest]]
|
||||||
if subject == nil {
|
if subject == nil {
|
||||||
return nil, fmt.Errorf("failed to find subject for attestation manifest: %w", err)
|
return nil, fmt.Errorf("failed to find subject for attestation manifest: %w", err)
|
||||||
}
|
}
|
||||||
attestationImage, err := index.Image(manifest.Digest)
|
attestationImage, err := index.Image(desc.Digest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", manifest.Digest.String(), err)
|
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", desc.Digest.String(), err)
|
||||||
}
|
}
|
||||||
attestationLayers, err := GetAttestationsFromImage(attestationImage)
|
attestationLayers, err := GetAttestationsFromImage(attestationImage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -45,14 +45,9 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*AttestationManife
|
|||||||
}
|
}
|
||||||
attestationManifests = append(attestationManifests,
|
attestationManifests = append(attestationManifests,
|
||||||
&AttestationManifest{
|
&AttestationManifest{
|
||||||
Descriptor: &manifest,
|
OriginalDescriptor: &desc,
|
||||||
SubjectDescriptor: subject,
|
SubjectDescriptor: subject,
|
||||||
Attestation: &AttestationImage{
|
OriginalLayers: attestationLayers})
|
||||||
Layers: attestationLayers,
|
|
||||||
Image: attestationImage},
|
|
||||||
MediaType: manifest.MediaType,
|
|
||||||
Annotations: manifest.Annotations,
|
|
||||||
Digest: manifest.Digest})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return attestationManifests, nil
|
return attestationManifests, nil
|
||||||
@@ -90,7 +85,7 @@ func GetAttestationsFromImage(image v1.Image) ([]*AttestationLayer, error) {
|
|||||||
return nil, fmt.Errorf("failed to decode statement layer contents: %w", err)
|
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, Statement: stmt, Annotations: ann})
|
||||||
}
|
}
|
||||||
return attestationLayers, nil
|
return attestationLayers, nil
|
||||||
}
|
}
|
||||||
@@ -100,17 +95,11 @@ func (manifest *AttestationManifest) AddAttestation(ctx context.Context, signer
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to create signed layer: %w", err)
|
return fmt.Errorf("failed to create signed layer: %w", err)
|
||||||
}
|
}
|
||||||
newImg, newDesc, err := addLayerToImage(manifest, layer, opts)
|
manifest.SignedLayers = append(manifest.SignedLayers, layer)
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to add signed layers to image: %w", err)
|
|
||||||
}
|
|
||||||
manifest.Attestation.Image = newImg
|
|
||||||
manifest.Descriptor = newDesc
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*AttestationLayer, error) {
|
func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*AttestationLayer, error) {
|
||||||
|
|
||||||
// sign the statement
|
// sign the statement
|
||||||
env, err := SignInTotoStatement(ctx, statement, signer, opts)
|
env, err := SignInTotoStatement(ctx, statement, signer, opts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -127,7 +116,6 @@ func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, si
|
|||||||
}
|
}
|
||||||
return &AttestationLayer{
|
return &AttestationLayer{
|
||||||
Statement: statement,
|
Statement: statement,
|
||||||
MediaType: types.MediaType(intoto.PayloadType),
|
|
||||||
Annotations: map[string]string{
|
Annotations: map[string]string{
|
||||||
InTotoPredicateType: statement.PredicateType,
|
InTotoPredicateType: statement.PredicateType,
|
||||||
InTotoReferenceLifecycleStage: LifecycleStageExperimental,
|
InTotoReferenceLifecycleStage: LifecycleStageExperimental,
|
||||||
@@ -148,104 +136,182 @@ func SignInTotoStatement(ctx context.Context, statement *intoto.Statement, signe
|
|||||||
return env, nil
|
return env, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func addLayerToImage(
|
func UpdateIndexImage(
|
||||||
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,
|
idx v1.ImageIndex,
|
||||||
manifest *AttestationManifest,
|
manifest *AttestationManifest,
|
||||||
) (v1.ImageIndex, error) {
|
options ...func(*AttestationManifestImageOptions) error) (v1.ImageIndex, error) {
|
||||||
idx = mutate.RemoveManifests(idx, match.Digests(manifest.Digest))
|
image, err := manifest.BuildAttestationImage(options...)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to build image: %w", err)
|
||||||
|
}
|
||||||
|
newDesc, err := partial.Descriptor(image)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get descriptor: %w", err)
|
||||||
|
}
|
||||||
|
newDesc.Platform = &v1.Platform{
|
||||||
|
Architecture: "unknown",
|
||||||
|
OS: "unknown",
|
||||||
|
}
|
||||||
|
newDesc.MediaType = manifest.OriginalDescriptor.MediaType
|
||||||
|
newDesc.Annotations = manifest.OriginalDescriptor.Annotations
|
||||||
|
idx = mutate.RemoveManifests(idx, match.Digests(manifest.OriginalDescriptor.Digest))
|
||||||
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
|
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
|
||||||
Add: manifest.Attestation.Image,
|
Add: image,
|
||||||
Descriptor: *manifest.Descriptor,
|
Descriptor: *newDesc,
|
||||||
})
|
})
|
||||||
return idx, nil
|
return idx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddImagesToIndex(
|
func UpdateIndexImages(idx v1.ImageIndex, manifest []*AttestationManifest, options ...func(*AttestationManifestImageOptions) error) (v1.ImageIndex, error) {
|
||||||
idx v1.ImageIndex,
|
var err error
|
||||||
manifests []*AttestationManifest,
|
for _, m := range manifest {
|
||||||
) (v1.ImageIndex, error) {
|
idx, err = UpdateIndexImage(idx, m, options...)
|
||||||
for _, manifest := range manifests {
|
|
||||||
var err error
|
|
||||||
idx, err = AddImageToIndex(idx, manifest)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to add image to index: %w", err)
|
return nil, fmt.Errorf("failed to add image to index: %w", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return idx, nil
|
return idx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newOptions(options ...func(*AttestationManifestImageOptions) error) (*AttestationManifestImageOptions, error) {
|
||||||
|
opts := &AttestationManifestImageOptions{}
|
||||||
|
for _, opt := range options {
|
||||||
|
err := opt(opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return opts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithoutSubject(skipSubject bool) func(*AttestationManifestImageOptions) error {
|
||||||
|
return func(r *AttestationManifestImageOptions) error {
|
||||||
|
r.skipSubject = skipSubject
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func WithReplacedLayers(replaceLayers bool) func(*AttestationManifestImageOptions) error {
|
||||||
|
return func(r *AttestationManifestImageOptions) error {
|
||||||
|
r.replaceLayers = replaceLayers
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// build an image with signed attestations, optionally replacing existing layers with signed layers
|
||||||
|
func (manifest *AttestationManifest) BuildAttestationImage(options ...func(*AttestationManifestImageOptions) error) (v1.Image, error) {
|
||||||
|
opts, err := newOptions(options...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create options: %w", err)
|
||||||
|
}
|
||||||
|
resultLayers := manifest.SignedLayers
|
||||||
|
for _, existingLayer := range manifest.OriginalLayers {
|
||||||
|
var found bool
|
||||||
|
for _, signedLayer := range manifest.SignedLayers {
|
||||||
|
if existingLayer.Statement == signedLayer.Statement {
|
||||||
|
found = true
|
||||||
|
// copy over original annotations
|
||||||
|
for k, v := range existingLayer.Annotations {
|
||||||
|
signedLayer.Annotations[k] = v
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//add existing layers if they've not been signed or we're not replacing them
|
||||||
|
if !found || !opts.replaceLayers {
|
||||||
|
resultLayers = append(resultLayers, existingLayer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// so taht we attach all attestations to a single attestations image - as per current buildkit
|
||||||
|
opts.laxReferrers = true
|
||||||
|
newImg, err := buildImage(resultLayers, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to build image: %w", err)
|
||||||
|
}
|
||||||
|
return newImg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// build an image per attestation (layer) suitable for use as Referrers
|
||||||
|
func (manifest *AttestationManifest) BuildReferringArtifacts() ([]v1.Image, error) {
|
||||||
|
var images []v1.Image
|
||||||
|
for _, layer := range manifest.SignedLayers {
|
||||||
|
opts := &AttestationManifestImageOptions{}
|
||||||
|
newImg, err := buildImage([]*AttestationLayer{layer}, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to build image: %w", err)
|
||||||
|
}
|
||||||
|
images = append(images, newImg)
|
||||||
|
}
|
||||||
|
return images, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// build and image containing only layers
|
||||||
|
func buildImage(layers []*AttestationLayer, manifest *v1.Descriptor, subject *v1.Descriptor, opts *AttestationManifestImageOptions) (v1.Image, error) {
|
||||||
|
newImg := empty.Image
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// NB: if we add the subject before the layers, it does not end up being computed/serialised in the output for some reason
|
||||||
|
//TODO - recreate this bug and push upstream
|
||||||
|
for _, layer := range layers {
|
||||||
|
add := mutate.Addendum{
|
||||||
|
Layer: layer.Layer,
|
||||||
|
Annotations: layer.Annotations,
|
||||||
|
}
|
||||||
|
newImg, err = mutate.Append(newImg, add)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to add layer to image: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// this is for attaching attestations to an attestation image in the index
|
||||||
|
if opts.laxReferrers {
|
||||||
|
newImg = mutate.ConfigMediaType(newImg, "application/vnd.oci.image.config.v1+json")
|
||||||
|
} else {
|
||||||
|
newImg = mutate.ArtifactType(newImg, intoto.PayloadType)
|
||||||
|
newImg = mutate.ConfigMediaType(newImg, "application/vnd.oci.empty.v1+json")
|
||||||
|
}
|
||||||
|
// we need to set this even when we set the artifact type otherwise things break (even the go-container-registry client)
|
||||||
|
// even though it's allowed to be empty by spec when setting artifact type
|
||||||
|
newImg = mutate.MediaType(newImg, manifest.MediaType)
|
||||||
|
|
||||||
|
// see note above - must be added after the layers!
|
||||||
|
if !opts.skipSubject {
|
||||||
|
newImg = mutate.Subject(newImg, *subject).(v1.Image)
|
||||||
|
}
|
||||||
|
if !opts.laxReferrers {
|
||||||
|
// as per https://github.com/opencontainers/image-spec/blob/main/manifest.md#guidance-for-an-empty-descriptor
|
||||||
|
newImg = &EmptyConfigImage{newImg}
|
||||||
|
}
|
||||||
|
return newImg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type EmptyConfigImage struct {
|
||||||
|
v1.Image
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *EmptyConfigImage) RawConfigFile() ([]byte, error) {
|
||||||
|
return []byte("{}"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *EmptyConfigImage) Manifest() (*v1.Manifest, error) {
|
||||||
|
mf, err := i.Image.Manifest()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get manifest: %w", err)
|
||||||
|
}
|
||||||
|
mf.Config = v1.Descriptor{
|
||||||
|
MediaType: "application/vnd.oci.empty.v1+json",
|
||||||
|
Size: 2,
|
||||||
|
Digest: v1.Hash{Algorithm: "sha256", Hex: "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"},
|
||||||
|
Data: []byte("{}"),
|
||||||
|
}
|
||||||
|
return mf, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *EmptyConfigImage) RawManifest() ([]byte, error) {
|
||||||
|
mf, err := i.Manifest()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get manifest: %w", err)
|
||||||
|
}
|
||||||
|
return json.Marshal(mf)
|
||||||
|
}
|
||||||
|
|||||||
@@ -102,8 +102,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
opts := &attestation.SigningOptions{
|
opts := &attestation.SigningOptions{
|
||||||
Replace: true,
|
SkipTL: true,
|
||||||
SkipSubject: tc.skipSubject,
|
|
||||||
}
|
}
|
||||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -120,15 +119,17 @@ func TestAttestationReferenceTypes(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
|
err = mirror.PushIndexToRegistry(attIdx.Index, indexName)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
for _, img := range signedManifests {
|
for _, signedManifest := range signedManifests {
|
||||||
err = mirror.PushImageToRegistry(img.Attestation.Image, fmt.Sprintf("%s:tag-does-not-matter", repo))
|
image, err := signedManifest.BuildAttestationImage(attestation.WithoutSubject(tc.skipSubject), attestation.WithReplacedLayers(true))
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = mirror.PushImageToRegistry(image, fmt.Sprintf("%s:tag-does-not-matter", repo))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
signedIndex := attIdx.Index
|
signedIndex := attIdx.Index
|
||||||
signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests)
|
signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests, attestation.WithReplacedLayers(true), attestation.WithoutSubject(tc.skipSubject))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
err = mirror.PushIndexToRegistry(signedIndex, indexName)
|
err = mirror.PushIndexToRegistry(signedIndex, indexName)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -213,10 +214,35 @@ func TestReferencesInDifferentRepo(t *testing.T) {
|
|||||||
refServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
refServer: httptest.NewServer(registry.New(registry.WithReferrersSupport(true))),
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
server := tc.server
|
||||||
server := tc.server
|
defer server.Close()
|
||||||
defer server.Close()
|
serverUrl, err := url.Parse(server.URL)
|
||||||
serverUrl, err := url.Parse(server.URL)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
refServer := tc.refServer
|
||||||
|
defer refServer.Close()
|
||||||
|
refServerUrl, err := url.Parse(refServer.URL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
opts := &attestation.SigningOptions{
|
||||||
|
SkipTL: true,
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
|
||||||
|
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// push signed attestation image to the ref server
|
||||||
|
for _, signedManifest := range signedManifests {
|
||||||
|
// push references using subject-digest.att convention
|
||||||
|
image, err := signedManifest.BuildAttestationImage()
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = mirror.PushImageToRegistry(image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerUrl.Host, repoName))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
refServer := tc.refServer
|
refServer := tc.refServer
|
||||||
@@ -225,8 +251,7 @@ func TestReferencesInDifferentRepo(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
opts := &attestation.SigningOptions{
|
opts := &attestation.SigningOptions{
|
||||||
Replace: true,
|
SkipTL: true,
|
||||||
SkipTL: true,
|
|
||||||
}
|
}
|
||||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -239,10 +264,14 @@ func TestReferencesInDifferentRepo(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// push signed attestation image to the ref server
|
// push signed attestation image to the ref server
|
||||||
for _, img := range signedManifests {
|
for _, mf := range signedManifests {
|
||||||
// push references using subject-digest.att convention
|
// push references using subject-digest.att convention
|
||||||
err = mirror.PushImageToRegistry(img.Attestation.Image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerUrl.Host, repoName))
|
imgs, err := mf.BuildReferringArtifacts()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
for _, img := range imgs {
|
||||||
|
err = mirror.PushImageToRegistry(img, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerUrl.Host, repoName))
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
mfs2, err := attIdx.Index.IndexManifest()
|
mfs2, err := attIdx.Index.IndexManifest()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@@ -262,6 +291,6 @@ func TestReferencesInDifferentRepo(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
|
assert.Equal(t, attest.OutcomeSuccess, results.Outcome)
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
|
||||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||||
v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
|
v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
|
||||||
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
ociv1 "github.com/opencontainers/image-spec/specs-go/v1"
|
||||||
@@ -28,30 +27,27 @@ var base64Encoding = base64.StdEncoding.Strict()
|
|||||||
type AttestationLayer struct {
|
type AttestationLayer struct {
|
||||||
Statement *intoto.Statement
|
Statement *intoto.Statement
|
||||||
Layer v1.Layer
|
Layer v1.Layer
|
||||||
MediaType types.MediaType
|
|
||||||
Annotations map[string]string
|
Annotations map[string]string
|
||||||
}
|
}
|
||||||
|
|
||||||
type AttestationImage struct {
|
|
||||||
Layers []*AttestationLayer
|
|
||||||
Image v1.Image
|
|
||||||
}
|
|
||||||
|
|
||||||
type SignedAttestationImage struct {
|
|
||||||
Image v1.Image
|
|
||||||
Descriptor *v1.Descriptor
|
|
||||||
AttestationManifest *AttestationManifest
|
|
||||||
}
|
|
||||||
|
|
||||||
type AttestationManifest struct {
|
type AttestationManifest struct {
|
||||||
Descriptor *v1.Descriptor
|
OriginalDescriptor *v1.Descriptor
|
||||||
Attestation *AttestationImage
|
OriginalLayers []*AttestationLayer
|
||||||
MediaType types.MediaType
|
|
||||||
Annotations map[string]string
|
// accumulated during signing
|
||||||
Digest v1.Hash
|
SignedLayers []*AttestationLayer
|
||||||
|
// details of subect image
|
||||||
|
SubjectName string
|
||||||
SubjectDescriptor *v1.Descriptor
|
SubjectDescriptor *v1.Descriptor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AttestationManifestImageOptions struct {
|
||||||
|
// how to output the image
|
||||||
|
skipSubject bool
|
||||||
|
replaceLayers bool
|
||||||
|
laxReferrers bool
|
||||||
|
}
|
||||||
|
|
||||||
// the following types are needed until https://github.com/secure-systems-lab/dsse/pull/61 is merged
|
// the following types are needed until https://github.com/secure-systems-lab/dsse/pull/61 is merged
|
||||||
type Envelope struct {
|
type Envelope struct {
|
||||||
PayloadType string `json:"payloadType"`
|
PayloadType string `json:"payloadType"`
|
||||||
@@ -83,12 +79,8 @@ type VerifyOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type SigningOptions struct {
|
type SigningOptions struct {
|
||||||
// replace unsigned statements with signed attestations
|
|
||||||
Replace bool
|
|
||||||
// don't log to the configured transparency log
|
// don't log to the configured transparency log
|
||||||
SkipTL bool
|
SkipTL bool
|
||||||
// don't add OCI subject field to attestation image
|
|
||||||
SkipSubject bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func DSSEMediaType(predicateType string) (string, error) {
|
func DSSEMediaType(predicateType string) (string, error) {
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/docker/attest/internal/embed"
|
"github.com/docker/attest/internal/embed"
|
||||||
|
"github.com/docker/attest/pkg/attestation"
|
||||||
"github.com/docker/attest/pkg/oci"
|
"github.com/docker/attest/pkg/oci"
|
||||||
"github.com/docker/attest/pkg/tuf"
|
"github.com/docker/attest/pkg/tuf"
|
||||||
"github.com/google/go-containerregistry/pkg/name"
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,7 +37,7 @@ func PushImageToRegistry(image v1.Image, imageName string) error {
|
|||||||
return remote.Write(ref, image, oci.MultiKeychainOption())
|
return remote.Write(ref, image, oci.MultiKeychainOption())
|
||||||
}
|
}
|
||||||
|
|
||||||
func PushIndexToRegistry(image v1.ImageIndex, imageName string) error {
|
func PushIndexToRegistry(index v1.ImageIndex, imageName string) error {
|
||||||
// Parse the index name
|
// Parse the index name
|
||||||
ref, err := name.ParseReference(imageName)
|
ref, err := name.ParseReference(imageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -43,7 +45,7 @@ func PushIndexToRegistry(image v1.ImageIndex, imageName string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Push the index to the registry
|
// Push the index to the registry
|
||||||
return remote.WriteIndex(ref, image, oci.MultiKeychainOption())
|
return remote.WriteIndex(ref, index, oci.MultiKeychainOption())
|
||||||
}
|
}
|
||||||
|
|
||||||
func SaveImageAsOCILayout(image v1.Image, path string) error {
|
func SaveImageAsOCILayout(image v1.Image, path string) error {
|
||||||
@@ -73,3 +75,83 @@ func SaveIndexAsOCILayout(image v1.ImageIndex, path string) error {
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func SaveIndex(outputs []*oci.ImageSpec, index v1.ImageIndex, indexName string) error {
|
||||||
|
// split output by comma and write or push each one
|
||||||
|
for _, output := range outputs {
|
||||||
|
if output.Type == oci.OCI {
|
||||||
|
idx := v1.ImageIndex(empty.Index)
|
||||||
|
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
|
||||||
|
Add: index,
|
||||||
|
Descriptor: v1.Descriptor{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
oci.OciReferenceTarget: indexName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
err := SaveIndexAsOCILayout(idx, output.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write signed image: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := PushIndexToRegistry(index, output.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to push signed image: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveImage(output *oci.ImageSpec, image v1.Image, imageName string) error {
|
||||||
|
if output.Type == oci.OCI {
|
||||||
|
idx := v1.ImageIndex(empty.Index)
|
||||||
|
idx = mutate.AppendManifests(idx, mutate.IndexAddendum{
|
||||||
|
Add: image,
|
||||||
|
Descriptor: v1.Descriptor{
|
||||||
|
Annotations: map[string]string{
|
||||||
|
oci.OciReferenceTarget: imageName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
err := SaveIndexAsOCILayout(idx, output.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to write signed image: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err := PushImageToRegistry(image, output.Identifier)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to push signed image: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveReferrers(manifest *attestation.AttestationManifest, outputs []*oci.ImageSpec) error {
|
||||||
|
for _, output := range outputs {
|
||||||
|
if output.Type == oci.OCI {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// so that we use the same tag each time to reduce number of tags (tags aren't needed for referrers but we must push one)
|
||||||
|
attOut, err := oci.ReplaceTagInSpec(output, manifest.SubjectDescriptor.Digest)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
//otherwise we end up with the detected platform, though I'm not sure it matters
|
||||||
|
attOut.Platform = &v1.Platform{
|
||||||
|
OS: "unknown",
|
||||||
|
Architecture: "unknown",
|
||||||
|
}
|
||||||
|
images, err := manifest.BuildReferringArtifacts()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to build image: %w", err)
|
||||||
|
}
|
||||||
|
for _, image := range images {
|
||||||
|
err = SaveImage(attOut, image, "")
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to push image: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
111
pkg/mirror/mirror_test.go
Normal file
111
pkg/mirror/mirror_test.go
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
package mirror
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/docker/attest/internal/test"
|
||||||
|
"github.com/docker/attest/pkg/attest"
|
||||||
|
"github.com/docker/attest/pkg/attestation"
|
||||||
|
"github.com/docker/attest/pkg/oci"
|
||||||
|
"github.com/google/go-containerregistry/pkg/registry"
|
||||||
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
|
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||||
|
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestSavingIndex(t *testing.T) {
|
||||||
|
UnsignedTestImage := filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||||
|
outputLayout := test.CreateTempDir(t, "", "mirror-test")
|
||||||
|
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
server := httptest.NewServer(registry.New())
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
u, err := url.Parse(server.URL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||||
|
output, err := oci.ParseImageSpecs(indexName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = SaveIndex(output, attIdx.Index, indexName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ociOutput, err := oci.ParseImageSpecs("oci://" + outputLayout)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = SaveIndex(ociOutput, attIdx.Index, indexName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSavingImage(t *testing.T) {
|
||||||
|
|
||||||
|
outputLayout := test.CreateTempDir(t, "", "mirror-test")
|
||||||
|
|
||||||
|
img := empty.Image
|
||||||
|
|
||||||
|
server := httptest.NewServer(registry.New())
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
u, err := url.Parse(server.URL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||||
|
output, err := oci.ParseImageSpec(indexName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = SaveImage(output, img, indexName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
ociOutput, err := oci.ParseImageSpec("oci://" + outputLayout)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = SaveImage(ociOutput, img, indexName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSavingReferrers(t *testing.T) {
|
||||||
|
ctx, signer := test.Setup(t)
|
||||||
|
opts := &attestation.SigningOptions{}
|
||||||
|
statement := &intoto.Statement{
|
||||||
|
StatementHeader: intoto.StatementHeader{
|
||||||
|
PredicateType: attestation.VSAPredicateType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
digest, err := v1.NewHash("sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620")
|
||||||
|
require.NoError(t, err)
|
||||||
|
subject := &v1.Descriptor{
|
||||||
|
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||||
|
Digest: digest,
|
||||||
|
}
|
||||||
|
manifest, err := attest.NewAttestationManifest(subject)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = manifest.AddAttestation(ctx, signer, statement, opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
server := httptest.NewServer(registry.New(registry.WithReferrersSupport(true)))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
u, err := url.Parse(server.URL)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||||
|
output, err := oci.ParseImageSpecs(indexName)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = SaveReferrers(manifest, output)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
reg := &test.MockRegistryResolver{
|
||||||
|
Subject: subject,
|
||||||
|
MockResolver: &test.MockResolver{},
|
||||||
|
ImageNameStr: indexName,
|
||||||
|
}
|
||||||
|
require.NoError(t, err)
|
||||||
|
refResolver, err := oci.NewReferrersAttestationResolver(reg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
attestations, err := refResolver.Attestations(ctx, attestation.VSAPredicateType)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, attestations, 1)
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
|
|
||||||
// implementation of AttestationResolver that closes over attestations from an oci layout
|
// implementation of AttestationResolver that closes over attestations from an oci layout
|
||||||
type OCILayoutResolver struct {
|
type OCILayoutResolver struct {
|
||||||
*AttestationManifest
|
*attestation.AttestationManifest
|
||||||
*ImageSpec
|
*ImageSpec
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ func NewOCILayoutAttestationResolver(src *ImageSpec) (*OCILayoutResolver, error)
|
|||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *OCILayoutResolver) fetchAttestationManifest() (*AttestationManifest, error) {
|
func (r *OCILayoutResolver) fetchAttestationManifest() (*attestation.AttestationManifest, error) {
|
||||||
if r.AttestationManifest == nil {
|
if r.AttestationManifest == nil {
|
||||||
m, err := attestationManifestFromOCILayout(r.Identifier, r.ImageSpec.Platform)
|
m, err := attestationManifestFromOCILayout(r.Identifier, r.ImageSpec.Platform)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -43,19 +43,13 @@ func (r *OCILayoutResolver) fetchAttestationManifest() (*AttestationManifest, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
||||||
attestationImage := r.AttestationManifest.Image
|
|
||||||
layers, err := attestationImage.Layers()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err)
|
|
||||||
}
|
|
||||||
var envs []*att.Envelope
|
var envs []*att.Envelope
|
||||||
manifest := r.AttestationManifest.Manifest
|
for _, attestationLayer := range r.AttestationManifest.OriginalLayers {
|
||||||
for i, l := range manifest.Layers {
|
if attestationLayer.Annotations[attestation.InTotoPredicateType] != predicateType {
|
||||||
if l.Annotations[attestation.InTotoPredicateType] != predicateType {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
layer := layers[i]
|
|
||||||
mt, err := layer.MediaType()
|
mt, err := attestationLayer.Layer.MediaType()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
||||||
}
|
}
|
||||||
@@ -65,7 +59,7 @@ func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType stri
|
|||||||
}
|
}
|
||||||
var env = new(att.Envelope)
|
var env = new(att.Envelope)
|
||||||
// parse layer blob as json
|
// parse layer blob as json
|
||||||
r, err := layer.Uncompressed()
|
r, err := attestationLayer.Layer.Uncompressed()
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
||||||
@@ -81,18 +75,18 @@ func (r *OCILayoutResolver) Attestations(ctx context.Context, predicateType stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *OCILayoutResolver) ImageName(ctx context.Context) (string, error) {
|
func (r *OCILayoutResolver) ImageName(ctx context.Context) (string, error) {
|
||||||
return r.Name, nil
|
return r.SubjectName, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *OCILayoutResolver) ImageDigest(ctx context.Context) (string, error) {
|
func (r *OCILayoutResolver) ImageDescriptor(ctx context.Context) (*v1.Descriptor, error) {
|
||||||
return r.Digest, nil
|
return r.SubjectDescriptor, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *OCILayoutResolver) ImagePlatform(ctx context.Context) (*v1.Platform, error) {
|
func (r *OCILayoutResolver) ImagePlatform(ctx context.Context) (*v1.Platform, error) {
|
||||||
return r.ImageSpec.Platform, nil
|
return r.ImageSpec.Platform, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*AttestationManifest, error) {
|
func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*attestation.AttestationManifest, error) {
|
||||||
idx, err := layout.ImageIndexFromPath(path)
|
idx, err := layout.ImageIndexFromPath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -115,10 +109,11 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*Atte
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
||||||
}
|
}
|
||||||
var imageDigest string
|
var subjectDescriptor *v1.Descriptor
|
||||||
for _, mf := range mfs2.Manifests {
|
for _, mf := range mfs2.Manifests {
|
||||||
if mf.Platform.Equals(*platform) {
|
if mf.Platform.Equals(*platform) {
|
||||||
imageDigest = mf.Digest.String()
|
subjectDescriptor = &mf
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, mf := range mfs2.Manifests {
|
for _, mf := range mfs2.Manifests {
|
||||||
@@ -126,7 +121,7 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*Atte
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if mf.Annotations[att.DockerReferenceDigest] != imageDigest {
|
if mf.Annotations[att.DockerReferenceDigest] != subjectDescriptor.Digest.String() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -134,17 +129,15 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*Atte
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
|
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
|
||||||
}
|
}
|
||||||
manifest, err := attestationImage.Manifest()
|
layers, err := attestation.GetAttestationsFromImage(attestationImage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get manifest: %w", err)
|
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
|
||||||
}
|
}
|
||||||
attest := &AttestationManifest{
|
attest := &attestation.AttestationManifest{
|
||||||
Name: name,
|
OriginalLayers: layers,
|
||||||
Image: attestationImage,
|
OriginalDescriptor: &mf,
|
||||||
Manifest: manifest,
|
SubjectName: name,
|
||||||
Descriptor: &mf,
|
SubjectDescriptor: subjectDescriptor,
|
||||||
Digest: imageDigest,
|
|
||||||
Platform: platform,
|
|
||||||
}
|
}
|
||||||
return attest, nil
|
return attest, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/containerd/containerd/platforms"
|
"github.com/containerd/containerd/platforms"
|
||||||
"github.com/distribution/reference"
|
"github.com/distribution/reference"
|
||||||
|
"github.com/docker/attest/pkg/attestation"
|
||||||
att "github.com/docker/attest/pkg/attestation"
|
att "github.com/docker/attest/pkg/attestation"
|
||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
@@ -46,19 +47,17 @@ func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
|
|||||||
return options
|
return options
|
||||||
}
|
}
|
||||||
|
|
||||||
func ExtractEnvelopes(ia *AttestationManifest, predicateType string) ([]*att.Envelope, error) {
|
func ExtractEnvelopes(manifest *attestation.AttestationManifest, predicateType string) ([]*att.Envelope, error) {
|
||||||
manifest := ia.Manifest
|
|
||||||
image := ia.Image
|
|
||||||
var envs []*att.Envelope
|
var envs []*att.Envelope
|
||||||
layers, err := image.Layers()
|
for _, attestationLayer := range manifest.OriginalLayers {
|
||||||
if err != nil {
|
mt, err := attestationLayer.Layer.MediaType()
|
||||||
return nil, fmt.Errorf("failed to get layers: %w", err)
|
if err != nil {
|
||||||
}
|
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
||||||
for i, l := range manifest.Layers {
|
}
|
||||||
if (strings.HasPrefix(string(l.MediaType), "application/vnd.in-toto.")) &&
|
if (strings.HasPrefix(string(mt), "application/vnd.in-toto.")) &&
|
||||||
strings.HasSuffix(string(l.MediaType), "+dsse") &&
|
strings.HasSuffix(string(mt), "+dsse") &&
|
||||||
l.Annotations[att.InTotoPredicateType] == predicateType {
|
attestationLayer.Annotations[att.InTotoPredicateType] == predicateType {
|
||||||
reader, err := layers[i].Uncompressed()
|
reader, err := attestationLayer.Layer.Uncompressed()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
||||||
}
|
}
|
||||||
@@ -75,13 +74,13 @@ func ExtractEnvelopes(ia *AttestationManifest, predicateType string) ([]*att.Env
|
|||||||
return envs, nil
|
return envs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageDigestForPlatform(ix *v1.IndexManifest, platform *v1.Platform) (string, error) {
|
func imageDescriptor(ix *v1.IndexManifest, platform *v1.Platform) (*v1.Descriptor, error) {
|
||||||
for _, m := range ix.Manifests {
|
for _, m := range ix.Manifests {
|
||||||
if (m.MediaType == ocispec.MediaTypeImageManifest || m.MediaType == "application/vnd.docker.distribution.manifest.v2+json") && m.Platform.Equals(*platform) {
|
if (m.MediaType == ocispec.MediaTypeImageManifest || m.MediaType == "application/vnd.docker.distribution.manifest.v2+json") && m.Platform.Equals(*platform) {
|
||||||
return m.Digest.String(), nil
|
return &m, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "", errors.New(fmt.Sprintf("no image found for platform %v", platform))
|
return nil, errors.New(fmt.Sprintf("no image found for platform %v", platform))
|
||||||
}
|
}
|
||||||
|
|
||||||
func attestationDigestForDigest(ix *v1.IndexManifest, imageDigest string, attestType string) (string, error) {
|
func attestationDigestForDigest(ix *v1.IndexManifest, imageDigest string, attestType string) (string, error) {
|
||||||
@@ -147,3 +146,27 @@ func SplitDigest(digest string) (common.DigestSet, error) {
|
|||||||
parts[0]: parts[1],
|
parts[0]: parts[1],
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ReplaceTagInSpec(src *ImageSpec, digest v1.Hash) (*ImageSpec, error) {
|
||||||
|
newName, err := replaceTag(src.Identifier, digest)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse repo name: %w", err)
|
||||||
|
}
|
||||||
|
return &ImageSpec{
|
||||||
|
Identifier: newName,
|
||||||
|
Type: src.Type,
|
||||||
|
Platform: src.Platform,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// so that the index tag is replaced with a tag unique to the image digest and doesn't overwrite it
|
||||||
|
func replaceTag(image string, digest v1.Hash) (string, error) {
|
||||||
|
if strings.HasPrefix(image, LocalPrefix) {
|
||||||
|
return image, nil
|
||||||
|
}
|
||||||
|
notag, err := WithoutTag(image)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%s:%s-%s.att", notag, digest.Algorithm, digest.Hex), nil
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@@ -75,14 +76,16 @@ func TestImageDigestForPlatform(t *testing.T) {
|
|||||||
|
|
||||||
p, err := ParsePlatform("linux/amd64")
|
p, err := ParsePlatform("linux/amd64")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
digest, err := imageDigestForPlatform(mfs2, p)
|
desc, err := imageDescriptor(mfs2, p)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
digest := desc.Digest.String()
|
||||||
assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", digest)
|
assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", digest)
|
||||||
|
|
||||||
p, err = ParsePlatform("linux/arm64")
|
p, err = ParsePlatform("linux/arm64")
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
digest, err = imageDigestForPlatform(mfs2, p)
|
desc, err = imageDescriptor(mfs2, p)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
digest = desc.Digest.String()
|
||||||
assert.Equal(t, "sha256:7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e", digest)
|
assert.Equal(t, "sha256:7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e", digest)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,3 +109,31 @@ func TestWithoutTag(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReplaceTag(t *testing.T) {
|
||||||
|
tc := []struct {
|
||||||
|
name string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{name: "image:tag", expected: "index.docker.io/library/image:sha256-digest.att"},
|
||||||
|
{name: "image", expected: "index.docker.io/library/image:sha256-digest.att"},
|
||||||
|
{name: "image:sha256-digest.att", expected: "index.docker.io/library/image:sha256-digest.att"},
|
||||||
|
{name: "docker://image:tag", expected: "docker://index.docker.io/library/image:sha256-digest.att"},
|
||||||
|
{name: "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "index.docker.io/library/image:sha256-digest.att"},
|
||||||
|
{name: "oci://foobar", expected: "oci://foobar"},
|
||||||
|
{name: "docker://image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "docker://index.docker.io/library/image:sha256-digest.att"},
|
||||||
|
{name: "docker://127.0.0.1:36555/repo:latest", expected: "docker://127.0.0.1:36555/repo:sha256-digest.att"},
|
||||||
|
}
|
||||||
|
|
||||||
|
digest := v1.Hash{
|
||||||
|
Algorithm: "sha256",
|
||||||
|
Hex: "digest",
|
||||||
|
}
|
||||||
|
for _, c := range tc {
|
||||||
|
t.Run(c.name, func(t *testing.T) {
|
||||||
|
replaced, err := replaceTag(c.name, digest)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, c.expected, replaced)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/attest/pkg/attestation"
|
||||||
att "github.com/docker/attest/pkg/attestation"
|
att "github.com/docker/attest/pkg/attestation"
|
||||||
"github.com/google/go-containerregistry/pkg/name"
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||||
@@ -13,13 +14,13 @@ import (
|
|||||||
type ReferrersResolver struct {
|
type ReferrersResolver struct {
|
||||||
digest string
|
digest string
|
||||||
referrersRepo string
|
referrersRepo string
|
||||||
manifests []*AttestationManifest
|
manifests []*attestation.AttestationManifest
|
||||||
*RegistryImageDetailsResolver
|
ImageDetailsResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewReferrersAttestationResolver(src *RegistryImageDetailsResolver, options ...func(*ReferrersResolver) error) (*ReferrersResolver, error) {
|
func NewReferrersAttestationResolver(src ImageDetailsResolver, options ...func(*ReferrersResolver) error) (*ReferrersResolver, error) {
|
||||||
res := &ReferrersResolver{
|
res := &ReferrersResolver{
|
||||||
RegistryImageDetailsResolver: src,
|
ImageDetailsResolver: src,
|
||||||
}
|
}
|
||||||
for _, opt := range options {
|
for _, opt := range options {
|
||||||
err := opt(res)
|
err := opt(res)
|
||||||
@@ -39,11 +40,19 @@ func WithReferrersRepo(repo string) func(*ReferrersResolver) error {
|
|||||||
|
|
||||||
func (r *ReferrersResolver) resolveAttestations(ctx context.Context) error {
|
func (r *ReferrersResolver) resolveAttestations(ctx context.Context) error {
|
||||||
if r.manifests == nil {
|
if r.manifests == nil {
|
||||||
subjectRef, err := name.ParseReference(r.Identifier)
|
imageName, err := r.ImageName(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get image name: %w", err)
|
||||||
|
}
|
||||||
|
subjectRef, err := name.ParseReference(imageName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to parse reference: %w", err)
|
return fmt.Errorf("failed to parse reference: %w", err)
|
||||||
}
|
}
|
||||||
subjectDigest, err := r.ImageDigest(ctx)
|
desc, err := r.ImageDescriptor(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get descriptor: %w", err)
|
||||||
|
}
|
||||||
|
subjectDigest := desc.Digest.String()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get digest: %w", err)
|
return fmt.Errorf("failed to get digest: %w", err)
|
||||||
}
|
}
|
||||||
@@ -56,6 +65,7 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context) error {
|
|||||||
} else {
|
} else {
|
||||||
referrersSubjectRef = subjectRef.Context().Digest(subjectDigest)
|
referrersSubjectRef = subjectRef.Context().Digest(subjectDigest)
|
||||||
}
|
}
|
||||||
|
// TODO - search for in-toto artifact type
|
||||||
referrersIndex, err := remote.Referrers(referrersSubjectRef)
|
referrersIndex, err := remote.Referrers(referrersSubjectRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get referrers: %w", err)
|
return fmt.Errorf("failed to get referrers: %w", err)
|
||||||
@@ -67,31 +77,22 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context) error {
|
|||||||
if len(referrersIndexManifest.Manifests) == 0 {
|
if len(referrersIndexManifest.Manifests) == 0 {
|
||||||
return errors.New("no referrers found")
|
return errors.New("no referrers found")
|
||||||
}
|
}
|
||||||
aManifests := make([]*AttestationManifest, 0)
|
aManifests := make([]*attestation.AttestationManifest, 0)
|
||||||
for _, m := range referrersIndexManifest.Manifests {
|
for _, m := range referrersIndexManifest.Manifests {
|
||||||
|
|
||||||
remoteRef := referrersSubjectRef.Context().Digest(m.Digest.String())
|
remoteRef := referrersSubjectRef.Context().Digest(m.Digest.String())
|
||||||
attestationImage, err := remote.Image(remoteRef)
|
attestationImage, err := remote.Image(remoteRef)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get referred image: %w", err)
|
return fmt.Errorf("failed to get referred image: %w", err)
|
||||||
}
|
}
|
||||||
manifest, err := attestationImage.Manifest()
|
layers, err := attestation.GetAttestationsFromImage(attestationImage)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get manifest: %w", err)
|
return fmt.Errorf("failed to get attestations from image: %w", err)
|
||||||
}
|
}
|
||||||
if manifest.Annotations[att.DockerReferenceType] != att.AttestationManifestType {
|
attest := &attestation.AttestationManifest{
|
||||||
continue
|
SubjectName: imageName,
|
||||||
}
|
OriginalLayers: layers,
|
||||||
if manifest.Annotations[att.DockerReferenceDigest] != subjectDigest {
|
OriginalDescriptor: &m,
|
||||||
continue
|
SubjectDescriptor: desc,
|
||||||
}
|
|
||||||
attest := &AttestationManifest{
|
|
||||||
Name: r.Identifier,
|
|
||||||
Image: attestationImage,
|
|
||||||
Manifest: manifest,
|
|
||||||
Descriptor: &m,
|
|
||||||
Digest: subjectDigest,
|
|
||||||
Platform: r.Platform,
|
|
||||||
}
|
}
|
||||||
aManifests = append(aManifests, attest)
|
aManifests = append(aManifests, attest)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package oci
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/docker/attest/pkg/attestation"
|
||||||
att "github.com/docker/attest/pkg/attestation"
|
att "github.com/docker/attest/pkg/attestation"
|
||||||
"github.com/google/go-containerregistry/pkg/name"
|
"github.com/google/go-containerregistry/pkg/name"
|
||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
@@ -13,12 +13,12 @@ import (
|
|||||||
|
|
||||||
type RegistryResolver struct {
|
type RegistryResolver struct {
|
||||||
*RegistryImageDetailsResolver
|
*RegistryImageDetailsResolver
|
||||||
*AttestationManifest
|
*attestation.AttestationManifest
|
||||||
}
|
}
|
||||||
|
|
||||||
type RegistryImageDetailsResolver struct {
|
type RegistryImageDetailsResolver struct {
|
||||||
*ImageSpec
|
*ImageSpec
|
||||||
digest string
|
descriptor *v1.Descriptor
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRegistryImageDetailsResolver(src *ImageSpec) (*RegistryImageDetailsResolver, error) {
|
func NewRegistryImageDetailsResolver(src *ImageSpec) (*RegistryImageDetailsResolver, error) {
|
||||||
@@ -41,24 +41,36 @@ func (r *RegistryImageDetailsResolver) ImagePlatform(ctx context.Context) (*v1.P
|
|||||||
return r.Platform, nil
|
return r.Platform, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RegistryImageDetailsResolver) ImageDigest(ctx context.Context) (string, error) {
|
func (r *RegistryImageDetailsResolver) ImageDescriptor(ctx context.Context) (*v1.Descriptor, error) {
|
||||||
if r.digest == "" {
|
if r.descriptor == nil {
|
||||||
subjectRef, err := name.ParseReference(r.Identifier)
|
subjectRef, err := name.ParseReference(r.Identifier)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to parse reference: %w", err)
|
return nil, fmt.Errorf("failed to parse reference: %w", err)
|
||||||
}
|
}
|
||||||
options := WithOptions(ctx, r.Platform)
|
options := WithOptions(ctx, r.Platform)
|
||||||
desc, err := remote.Image(subjectRef, options...)
|
image, err := remote.Image(subjectRef, options...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get image manifest: %w", err)
|
return nil, fmt.Errorf("failed to get image manifest: %w", err)
|
||||||
}
|
}
|
||||||
subjectDigest, err := desc.Digest()
|
digest, err := image.Digest()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("failed to get image digest: %w", err)
|
return nil, fmt.Errorf("failed to get image digest: %w", err)
|
||||||
|
}
|
||||||
|
size, err := image.Size()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get image size: %w", err)
|
||||||
|
}
|
||||||
|
mediaType, err := image.MediaType()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get image media type: %w", err)
|
||||||
|
}
|
||||||
|
r.descriptor = &v1.Descriptor{
|
||||||
|
Digest: digest,
|
||||||
|
Size: size,
|
||||||
|
MediaType: mediaType,
|
||||||
}
|
}
|
||||||
r.digest = subjectDigest.String()
|
|
||||||
}
|
}
|
||||||
return r.digest, nil
|
return r.descriptor, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RegistryResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
func (r *RegistryResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
||||||
@@ -72,7 +84,7 @@ func (r *RegistryResolver) Attestations(ctx context.Context, predicateType strin
|
|||||||
return ExtractEnvelopes(r.AttestationManifest, predicateType)
|
return ExtractEnvelopes(r.AttestationManifest, predicateType)
|
||||||
}
|
}
|
||||||
|
|
||||||
func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Platform) (*AttestationManifest, error) {
|
func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Platform) (*attestation.AttestationManifest, error) {
|
||||||
// we want to get to the image index, so ignoring platform for now
|
// we want to get to the image index, so ignoring platform for now
|
||||||
options := WithOptions(ctx, nil)
|
options := WithOptions(ctx, nil)
|
||||||
ref, err := name.ParseReference(image)
|
ref, err := name.ParseReference(image)
|
||||||
@@ -87,10 +99,12 @@ func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Pl
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get index manifest: %w", err)
|
return nil, fmt.Errorf("failed to get index manifest: %w", err)
|
||||||
}
|
}
|
||||||
digest, err := imageDigestForPlatform(indexManifest, platform)
|
subjectDescriptor, err := imageDescriptor(indexManifest, platform)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to obtain image for platform: %w", err)
|
return nil, fmt.Errorf("failed to obtain image for platform: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
digest := subjectDescriptor.Digest.String()
|
||||||
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), digest))
|
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), digest))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
||||||
@@ -108,22 +122,20 @@ func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Pl
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get attestation: %w", err)
|
return nil, fmt.Errorf("failed to get attestation: %w", err)
|
||||||
}
|
}
|
||||||
manifest := new(v1.Manifest)
|
|
||||||
err = json.Unmarshal(remoteDescriptor.Manifest, manifest)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to unmarshal attestation: %w", err)
|
|
||||||
}
|
|
||||||
attestationImage, err := remoteDescriptor.Image()
|
attestationImage, err := remoteDescriptor.Image()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to get attestation image: %w", err)
|
return nil, fmt.Errorf("failed to get attestation image: %w", err)
|
||||||
}
|
}
|
||||||
attest := &AttestationManifest{
|
|
||||||
Name: image,
|
layers, err := attestation.GetAttestationsFromImage(attestationImage)
|
||||||
Image: attestationImage,
|
if err != nil {
|
||||||
Manifest: manifest,
|
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
|
||||||
Descriptor: &remoteDescriptor.Descriptor,
|
}
|
||||||
Digest: digest,
|
attest := &attestation.AttestationManifest{
|
||||||
Platform: platform,
|
OriginalLayers: layers,
|
||||||
|
OriginalDescriptor: &remoteDescriptor.Descriptor,
|
||||||
|
SubjectName: image,
|
||||||
|
SubjectDescriptor: subjectDescriptor,
|
||||||
}
|
}
|
||||||
return attest, nil
|
return attest, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,16 +25,13 @@ func TestRegistry(t *testing.T) {
|
|||||||
u, err := url.Parse(server.URL)
|
u, err := url.Parse(server.URL)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
opts := &attestation.SigningOptions{
|
opts := &attestation.SigningOptions{}
|
||||||
Replace: true,
|
|
||||||
SkipSubject: true,
|
|
||||||
}
|
|
||||||
attIdx, err := oci.IndexFromPath(oci.UnsignedTestImage)
|
attIdx, err := oci.IndexFromPath(oci.UnsignedTestImage)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
signedIndex := attIdx.Index
|
signedIndex := attIdx.Index
|
||||||
signedIndex, err = attestation.AddImagesToIndex(signedIndex, signedManifests)
|
signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||||
@@ -47,7 +44,8 @@ func TestRegistry(t *testing.T) {
|
|||||||
|
|
||||||
resolver, err := policy.CreateImageDetailsResolver(spec)
|
resolver, err := policy.CreateImageDetailsResolver(spec)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
digest, err := resolver.ImageDigest(ctx)
|
desc, err := resolver.ImageDescriptor(ctx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
digest := desc.Digest.String()
|
||||||
assert.True(t, strings.Contains(digest, "sha256:"))
|
assert.True(t, strings.Contains(digest, "sha256:"))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,21 +7,6 @@ import (
|
|||||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||||
)
|
)
|
||||||
|
|
||||||
type AttestationManifests struct {
|
|
||||||
Manifests []*AttestationManifest
|
|
||||||
}
|
|
||||||
|
|
||||||
type AttestationManifest struct {
|
|
||||||
// attestation image details
|
|
||||||
Image v1.Image
|
|
||||||
Manifest *v1.Manifest
|
|
||||||
Descriptor *v1.Descriptor
|
|
||||||
// details of subect image
|
|
||||||
Name string
|
|
||||||
Digest string
|
|
||||||
Platform *v1.Platform
|
|
||||||
}
|
|
||||||
|
|
||||||
type AttestationResolver interface {
|
type AttestationResolver interface {
|
||||||
ImageDetailsResolver
|
ImageDetailsResolver
|
||||||
Attestations(ctx context.Context, mediaType string) ([]*att.Envelope, error)
|
Attestations(ctx context.Context, mediaType string) ([]*att.Envelope, error)
|
||||||
@@ -30,25 +15,5 @@ type AttestationResolver interface {
|
|||||||
type ImageDetailsResolver interface {
|
type ImageDetailsResolver interface {
|
||||||
ImageName(ctx context.Context) (string, error)
|
ImageName(ctx context.Context) (string, error)
|
||||||
ImagePlatform(ctx context.Context) (*v1.Platform, error)
|
ImagePlatform(ctx context.Context) (*v1.Platform, error)
|
||||||
ImageDigest(ctx context.Context) (string, error)
|
ImageDescriptor(ctx context.Context) (*v1.Descriptor, error)
|
||||||
}
|
|
||||||
|
|
||||||
type MockResolver struct {
|
|
||||||
Envs []*att.Envelope
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r MockResolver) Attestations(ctx context.Context, mediaType string) ([]*att.Envelope, error) {
|
|
||||||
return r.Envs, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r MockResolver) ImageName(ctx context.Context) (string, error) {
|
|
||||||
return "library/alpine:latest", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r MockResolver) ImageDigest(ctx context.Context) (string, error) {
|
|
||||||
return "sha256:test-digest", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r MockResolver) ImagePlatform(ctx context.Context) (*v1.Platform, error) {
|
|
||||||
return ParsePlatform("linux/amd64")
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
|
|||||||
|
|
||||||
re := policy.NewRegoEvaluator(true)
|
re := policy.NewRegoEvaluator(true)
|
||||||
|
|
||||||
defaultResolver := oci.MockResolver{
|
defaultResolver := test.MockResolver{
|
||||||
Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation)},
|
Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation)},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ k2s4SO3XbQ2GG2alm289SUUpmBAuVxvT8muYQ8HC/QzixzyTACTXsBDjQg==
|
|||||||
|
|
||||||
func TestGCPKMS_Signer(t *testing.T) {
|
func TestGCPKMS_Signer(t *testing.T) {
|
||||||
// create a new signer
|
// create a new signer
|
||||||
ctx := context.Background()
|
ctx := context.TODO()
|
||||||
ref := "projects/attest-kms-test/locations/us-west1/keyRings/attest-kms-test/cryptoKeys/test-signing-key/cryptoKeyVersions/1"
|
ref := "projects/attest-kms-test/locations/us-west1/keyRings/attest-kms-test/cryptoKeys/test-signing-key/cryptoKeyVersions/1"
|
||||||
signer, err := GetGCPSigner(ctx, ref)
|
signer, err := GetGCPSigner(ctx, ref)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user