Merge pull request #122 from docker/feat-mirror-empty-config-image
feat: mirror empty config image
This commit is contained in:
@@ -2,22 +2,13 @@ package test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/policy"
|
||||
"github.com/docker/attest/pkg/signerverifier"
|
||||
"github.com/docker/attest/pkg/tlog"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||
)
|
||||
|
||||
@@ -30,6 +21,8 @@ const (
|
||||
AWSKMSKeyARN = "arn:aws:kms:us-east-1:175142243308:alias/doi-signing" // sandbox
|
||||
)
|
||||
|
||||
var UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||
|
||||
func CreateTempDir(t *testing.T, dir, pattern string) string {
|
||||
// Create a temporary directory for output oci layout
|
||||
tempDir, err := os.MkdirTemp(dir, pattern)
|
||||
@@ -89,108 +82,3 @@ func Setup(t *testing.T) (context.Context, dsse.SignerVerifier) {
|
||||
|
||||
return ctx, signer
|
||||
}
|
||||
|
||||
type AnnotatedStatement struct {
|
||||
OCIDescriptor *v1.Descriptor
|
||||
InTotoStatement *intoto.Statement
|
||||
Annotations map[string]string
|
||||
}
|
||||
|
||||
func ExtractStatementsFromIndex(idx v1.ImageIndex, mediaType string) ([]*AnnotatedStatement, error) {
|
||||
mfs2, err := idx.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
||||
}
|
||||
|
||||
var statements []*AnnotatedStatement
|
||||
|
||||
for i := range mfs2.Manifests {
|
||||
mf := &mfs2.Manifests[i]
|
||||
if mf.Annotations[attestation.DockerReferenceType] != "attestation-manifest" {
|
||||
continue
|
||||
}
|
||||
|
||||
attestationImage, err := idx.Image(mf.Digest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
|
||||
}
|
||||
layers, err := attestationImage.Layers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err)
|
||||
}
|
||||
|
||||
for _, layer := range layers {
|
||||
// parse layer blob as json
|
||||
mt, err := layer.MediaType()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
||||
}
|
||||
|
||||
if string(mt) != mediaType {
|
||||
continue
|
||||
}
|
||||
r, err := layer.Uncompressed()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
||||
}
|
||||
defer r.Close()
|
||||
inTotoStatement := new(intoto.Statement)
|
||||
var desc *v1.Descriptor
|
||||
if strings.HasSuffix(string(mt), "+dsse") {
|
||||
env := new(attestation.Envelope)
|
||||
err = json.NewDecoder(r).Decode(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode env: %w", err)
|
||||
}
|
||||
payload, err := base64.StdEncoding.Strict().DecodeString(env.Payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode payload: %w", err)
|
||||
}
|
||||
err = json.Unmarshal([]byte(payload), inTotoStatement)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode %s statement: %w", mediaType, err)
|
||||
}
|
||||
} else {
|
||||
desc := new(v1.Descriptor)
|
||||
err = json.NewDecoder(r).Decode(desc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode statement: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
layerDesc, err := partial.Descriptor(layer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get descriptor for layer: %w", err)
|
||||
}
|
||||
annotations := make(map[string]string)
|
||||
for k, v := range layerDesc.Annotations {
|
||||
annotations[k] = v
|
||||
}
|
||||
statements = append(statements, &AnnotatedStatement{
|
||||
OCIDescriptor: desc,
|
||||
InTotoStatement: inTotoStatement,
|
||||
Annotations: annotations,
|
||||
})
|
||||
}
|
||||
}
|
||||
return statements, nil
|
||||
}
|
||||
|
||||
func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStatement, error) {
|
||||
idx, err := layout.ImageIndexFromPath(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load image index: %w", err)
|
||||
}
|
||||
|
||||
idxm, err := idx.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get digest: %w", err)
|
||||
}
|
||||
idxDigest := idxm.Manifests[0].Digest
|
||||
|
||||
mfs, err := idx.ImageIndex(idxDigest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
|
||||
}
|
||||
return ExtractStatementsFromIndex(mfs, mediaType)
|
||||
}
|
||||
|
||||
2
pkg/attest/README.md
Normal file
2
pkg/attest/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## attest
|
||||
This package implements the top-level signing and verification methods.
|
||||
@@ -12,14 +12,14 @@ import (
|
||||
// this is only relevant if there are (unsigned) in-toto statements.
|
||||
func SignStatements(ctx context.Context, idx v1.ImageIndex, signer dsse.SignerVerifier, opts *attestation.SigningOptions) ([]*attestation.Manifest, error) {
|
||||
// extract attestation manifests from index
|
||||
attestationManifests, err := attestation.GetAttestationManifestsFromIndex(idx)
|
||||
attestationManifests, err := attestation.ManifestsFromIndex(idx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load attestation manifests from index: %w", err)
|
||||
}
|
||||
// sign every attestation layer in each manifest
|
||||
for _, manifest := range attestationManifests {
|
||||
for _, layer := range manifest.OriginalLayers {
|
||||
err = manifest.AddAttestation(ctx, signer, layer.Statement, opts)
|
||||
err = manifest.Add(ctx, signer, layer.Statement, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign attestation layer %w", err)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
package attest
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
@@ -12,10 +8,6 @@ import (
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/policy"
|
||||
"github.com/google/go-containerregistry/pkg/registry"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/static"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
v02 "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/v0.2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
@@ -23,7 +15,6 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
|
||||
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
|
||||
PassMirrorPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-mirror")
|
||||
@@ -42,8 +33,8 @@ func TestSignVerifyOCILayout(t *testing.T) {
|
||||
expectedAttestations int
|
||||
replace bool
|
||||
}{
|
||||
{"signed replaced", UnsignedTestImage, 0, 4, true},
|
||||
{"without replace", UnsignedTestImage, 4, 4, false},
|
||||
{"signed replaced", test.UnsignedTestImage, 0, 4, true},
|
||||
{"without replace", test.UnsignedTestImage, 4, 4, false},
|
||||
// image without provenance doesn't fail
|
||||
{"no provenance (replace)", NoProvenanceImage, 0, 2, true},
|
||||
{"no provenance (no replace)", NoProvenanceImage, 2, 2, false},
|
||||
@@ -70,10 +61,10 @@ func TestSignVerifyOCILayout(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
assert.Equalf(t, OutcomeSuccess, policy.Outcome, "Policy should have been found")
|
||||
|
||||
var allEnvelopes []*test.AnnotatedStatement
|
||||
var allEnvelopes []*attestation.AnnotatedStatement
|
||||
for _, predicate := range []string{intoto.PredicateSPDX, v02.PredicateSLSAProvenance, attestation.VSAPredicateType} {
|
||||
mt, _ := attestation.DSSEMediaType(predicate)
|
||||
statements, err := test.ExtractAnnotatedStatements(outputLayout, mt)
|
||||
statements, err := attestation.ExtractAnnotatedStatements(outputLayout, mt)
|
||||
require.NoError(t, err)
|
||||
allEnvelopes = append(allEnvelopes, statements...)
|
||||
|
||||
@@ -83,150 +74,9 @@ func TestSignVerifyOCILayout(t *testing.T) {
|
||||
}
|
||||
}
|
||||
assert.Equalf(t, tc.expectedAttestations, len(allEnvelopes), "expected %d attestations, got %d", tc.expectedAttestations, len(allEnvelopes))
|
||||
statements, err := test.ExtractAnnotatedStatements(outputLayout, intoto.PayloadType)
|
||||
statements, err := attestation.ExtractAnnotatedStatements(outputLayout, intoto.PayloadType)
|
||||
require.NoError(t, err)
|
||||
assert.Equalf(t, tc.expectedStatements, len(statements), "expected %d statement, got %d", tc.expectedStatements, len(statements))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddSignedLayerAnnotations(t *testing.T) {
|
||||
ctx, signer := test.Setup(t)
|
||||
testCases := []struct {
|
||||
name string
|
||||
replace bool
|
||||
}{
|
||||
{"replaced", true},
|
||||
{"not replaced", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
data := []byte("signed")
|
||||
testLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType))
|
||||
mediaType := types.OCIManifestSchema1
|
||||
opts := &attestation.SigningOptions{}
|
||||
originalLayer := &attestation.Layer{
|
||||
Layer: testLayer,
|
||||
Statement: &intoto.Statement{
|
||||
StatementHeader: intoto.StatementHeader{
|
||||
PredicateType: attestation.VSAPredicateType,
|
||||
},
|
||||
},
|
||||
Annotations: map[string]string{"test": "test"},
|
||||
}
|
||||
|
||||
manifest := &attestation.Manifest{
|
||||
OriginalDescriptor: &v1.Descriptor{
|
||||
MediaType: mediaType,
|
||||
},
|
||||
OriginalLayers: []*attestation.Layer{
|
||||
originalLayer,
|
||||
},
|
||||
SubjectDescriptor: &v1.Descriptor{},
|
||||
}
|
||||
err := manifest.AddAttestation(ctx, signer, originalLayer.Statement, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
newImg, err := manifest.BuildAttestationImage(attestation.WithReplacedLayers(tc.replace))
|
||||
require.NoError(t, err)
|
||||
mf, _ := newImg.RawManifest()
|
||||
type Annotations struct {
|
||||
Annotations map[string]string `json:"annotations"`
|
||||
}
|
||||
type Layers struct {
|
||||
Layers []Annotations `json:"layers"`
|
||||
}
|
||||
l := &Layers{}
|
||||
err = json.Unmarshal(mf, l)
|
||||
require.NoError(t, err)
|
||||
_, ok := l.Layers[0].Annotations["test"]
|
||||
assert.Truef(t, ok, "missing annotations")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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.Contains(t, mf.ArtifactType, "application/vnd.in-toto")
|
||||
assert.Contains(t, mf.ArtifactType, "+dsse")
|
||||
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 = oci.SaveReferrers(manifest, output)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"github.com/docker/attest/pkg/config"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/policy"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
)
|
||||
|
||||
@@ -112,7 +111,7 @@ func toVerificationResult(p *policy.Policy, input *policy.Input, result *policy.
|
||||
}, nil
|
||||
}
|
||||
|
||||
func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, pctx *policy.Policy) (*VerificationResult, error) {
|
||||
func VerifyAttestations(ctx context.Context, resolver attestation.Resolver, pctx *policy.Policy) (*VerificationResult, error) {
|
||||
desc, err := resolver.ImageDescriptor(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get image descriptor: %w", err)
|
||||
@@ -164,13 +163,3 @@ func VerifyAttestations(ctx context.Context, resolver oci.AttestationResolver, p
|
||||
verificationResult.SubjectDescriptor = desc
|
||||
return verificationResult, nil
|
||||
}
|
||||
|
||||
func NewAttestationManifest(subject *v1.Descriptor) (*attestation.Manifest, error) {
|
||||
return &attestation.Manifest{
|
||||
OriginalDescriptor: &v1.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
},
|
||||
OriginalLayers: []*attestation.Layer{},
|
||||
SubjectDescriptor: subject,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ func TestVerifyAttestations(t *testing.T) {
|
||||
env := new(attestation.Envelope)
|
||||
err = json.Unmarshal(ex, env)
|
||||
assert.NoError(t, err)
|
||||
resolver := &oci.MockResolver{
|
||||
resolver := &attestation.MockResolver{
|
||||
Envs: []*attestation.Envelope{env},
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func TestVerifyAttestations(t *testing.T) {
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
mockPE := policy.MockPolicyEvaluator{
|
||||
EvaluateFunc: func(_ context.Context, _ oci.AttestationResolver, _ *policy.Policy, _ *policy.Input) (*policy.Result, error) {
|
||||
EvaluateFunc: func(_ context.Context, _ attestation.Resolver, _ *policy.Policy, _ *policy.Input) (*policy.Result, error) {
|
||||
return policy.AllowedResult(), tc.policyEvaluationError
|
||||
},
|
||||
}
|
||||
@@ -72,7 +72,7 @@ func TestVSA(t *testing.T) {
|
||||
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
||||
|
||||
opts := &attestation.SigningOptions{}
|
||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
assert.NoError(t, err)
|
||||
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
|
||||
require.NoError(t, err)
|
||||
@@ -122,7 +122,7 @@ func TestVerificationFailure(t *testing.T) {
|
||||
outputLayout := test.CreateTempDir(t, "", TestTempDir)
|
||||
|
||||
opts := &attestation.SigningOptions{}
|
||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
assert.NoError(t, err)
|
||||
signedManifests, err := SignStatements(ctx, attIdx.Index, signer, opts)
|
||||
require.NoError(t, err)
|
||||
@@ -185,7 +185,7 @@ func TestSignVerify(t *testing.T) {
|
||||
{name: "mirror no match", signTL: true, policyDir: PassMirrorPolicyDir, imageName: "incorrect.org/library/test-image:test", expectError: true},
|
||||
}
|
||||
|
||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
assert.NoError(t, err)
|
||||
|
||||
for _, tc := range testCases {
|
||||
|
||||
4
pkg/attestation/README.md
Normal file
4
pkg/attestation/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
## attestations
|
||||
This package is for components that deal with the creation, storage, and retrieval of signed attestions using OCI.
|
||||
|
||||
For more generic OCI components see the `oci` package.
|
||||
@@ -1,14 +1,17 @@
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||
"github.com/google/go-containerregistry/pkg/v1/match"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
"github.com/google/go-containerregistry/pkg/v1/partial"
|
||||
@@ -18,8 +21,19 @@ import (
|
||||
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||
)
|
||||
|
||||
// GetAttestationManifestsFromIndex extracts all attestation manifests from an index.
|
||||
func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*Manifest, error) {
|
||||
// NewManifest creates a new attestation manifest from a descriptor.
|
||||
func NewManifest(subject *v1.Descriptor) (*Manifest, error) {
|
||||
return &Manifest{
|
||||
OriginalDescriptor: &v1.Descriptor{
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
},
|
||||
OriginalLayers: []*Layer{},
|
||||
SubjectDescriptor: subject,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ManifestsFromIndex extracts all attestation manifests from an index.
|
||||
func ManifestsFromIndex(index v1.ImageIndex) ([]*Manifest, error) {
|
||||
idx, err := index.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
||||
@@ -42,7 +56,7 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*Manifest, error)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", desc.Digest.String(), err)
|
||||
}
|
||||
attestationLayers, err := GetAttestationsFromImage(attestationImage)
|
||||
attestationLayers, err := layersFromImage(attestationImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
|
||||
}
|
||||
@@ -57,8 +71,8 @@ func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*Manifest, error)
|
||||
return attestationManifests, nil
|
||||
}
|
||||
|
||||
// GetAttestationsFromImage extracts all attestation layers from an image.
|
||||
func GetAttestationsFromImage(image v1.Image) ([]*Layer, error) {
|
||||
// LayersFromImage extracts all attestation layers from an image.
|
||||
func layersFromImage(image v1.Image) ([]*Layer, error) {
|
||||
layers, err := image.Layers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract layers from image: %w", err)
|
||||
@@ -94,7 +108,7 @@ func GetAttestationsFromImage(image v1.Image) ([]*Layer, error) {
|
||||
return attestationLayers, nil
|
||||
}
|
||||
|
||||
func (manifest *Manifest) AddAttestation(ctx context.Context, signer dsse.SignerVerifier, statement *intoto.Statement, opts *SigningOptions) error {
|
||||
func (manifest *Manifest) Add(ctx context.Context, signer dsse.SignerVerifier, statement *intoto.Statement, opts *SigningOptions) error {
|
||||
layer, err := createSignedImageLayer(ctx, statement, signer, opts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create signed layer: %w", err)
|
||||
@@ -105,7 +119,7 @@ func (manifest *Manifest) AddAttestation(ctx context.Context, signer dsse.Signer
|
||||
|
||||
func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*Layer, error) {
|
||||
// sign the statement
|
||||
env, err := SignInTotoStatement(ctx, statement, signer, opts)
|
||||
env, err := signInTotoStatement(ctx, statement, signer, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign statement: %w", err)
|
||||
}
|
||||
@@ -128,7 +142,7 @@ func createSignedImageLayer(ctx context.Context, statement *intoto.Statement, si
|
||||
}, nil
|
||||
}
|
||||
|
||||
func SignInTotoStatement(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*Envelope, error) {
|
||||
func signInTotoStatement(ctx context.Context, statement *intoto.Statement, signer dsse.SignerVerifier, opts *SigningOptions) (*Envelope, error) {
|
||||
payload, err := json.Marshal(statement)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal statement: %w", err)
|
||||
@@ -140,12 +154,12 @@ func SignInTotoStatement(ctx context.Context, statement *intoto.Statement, signe
|
||||
return env, nil
|
||||
}
|
||||
|
||||
func UpdateIndexImage(
|
||||
func updateImageIndex(
|
||||
idx v1.ImageIndex,
|
||||
manifest *Manifest,
|
||||
options ...func(*ManifestImageOptions) error,
|
||||
) (v1.ImageIndex, error) {
|
||||
image, err := manifest.BuildAttestationImage(options...)
|
||||
image, err := manifest.BuildImage(options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build image: %w", err)
|
||||
}
|
||||
@@ -170,7 +184,7 @@ func UpdateIndexImage(
|
||||
func UpdateIndexImages(idx v1.ImageIndex, manifest []*Manifest, options ...func(*ManifestImageOptions) error) (v1.ImageIndex, error) {
|
||||
var err error
|
||||
for _, m := range manifest {
|
||||
idx, err = UpdateIndexImage(idx, m, options...)
|
||||
idx, err = updateImageIndex(idx, m, options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to add image to index: %w", err)
|
||||
}
|
||||
@@ -204,7 +218,7 @@ func WithReplacedLayers(replaceLayers bool) func(*ManifestImageOptions) error {
|
||||
}
|
||||
|
||||
// build an image with signed attestations, optionally replacing existing layers with signed layers.
|
||||
func (manifest *Manifest) BuildAttestationImage(options ...func(*ManifestImageOptions) error) (v1.Image, error) {
|
||||
func (manifest *Manifest) BuildImage(options ...func(*ManifestImageOptions) error) (v1.Image, error) {
|
||||
opts, err := newOptions(options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create options: %w", err)
|
||||
@@ -229,7 +243,7 @@ func (manifest *Manifest) BuildAttestationImage(options ...func(*ManifestImageOp
|
||||
}
|
||||
// so that 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)
|
||||
newImg, err := buildImageFromLayers(resultLayers, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build image: %w", err)
|
||||
}
|
||||
@@ -241,7 +255,7 @@ func (manifest *Manifest) BuildReferringArtifacts() ([]v1.Image, error) {
|
||||
var images []v1.Image
|
||||
for _, layer := range manifest.SignedLayers {
|
||||
opts := &ManifestImageOptions{}
|
||||
newImg, err := buildImage([]*Layer{layer}, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts)
|
||||
newImg, err := buildImageFromLayers([]*Layer{layer}, manifest.OriginalDescriptor, manifest.SubjectDescriptor, opts)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build image: %w", err)
|
||||
}
|
||||
@@ -250,8 +264,8 @@ func (manifest *Manifest) BuildReferringArtifacts() ([]v1.Image, error) {
|
||||
return images, nil
|
||||
}
|
||||
|
||||
// build an image containing only layers.
|
||||
func buildImage(layers []*Layer, manifest *v1.Descriptor, subject *v1.Descriptor, opts *ManifestImageOptions) (v1.Image, error) {
|
||||
// build an image containing only layers provided.
|
||||
func buildImageFromLayers(layers []*Layer, manifest *v1.Descriptor, subject *v1.Descriptor, opts *ManifestImageOptions) (v1.Image, error) {
|
||||
newImg := empty.Image
|
||||
var err error
|
||||
if len(layers) == 0 {
|
||||
@@ -296,46 +310,135 @@ func buildImage(layers []*Layer, manifest *v1.Descriptor, subject *v1.Descriptor
|
||||
}
|
||||
if !opts.laxReferrers {
|
||||
// as per https://github.com/opencontainers/image-spec/blob/main/manifest.md#guidance-for-an-empty-descriptor
|
||||
newImg = &EmptyConfigImage{newImg}
|
||||
newImg = &oci.EmptyConfigImage{Image: 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()
|
||||
func ExtractEnvelopes(manifest *Manifest, predicateType string) ([]*Envelope, error) {
|
||||
var envs []*Envelope
|
||||
dsseMediaType, err := DSSEMediaType(predicateType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get manifest: %w", err)
|
||||
return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err)
|
||||
}
|
||||
mf.Config = v1.Descriptor{
|
||||
MediaType: "application/vnd.oci.empty.v1+json",
|
||||
Size: 2,
|
||||
Digest: v1.Hash{Algorithm: "sha256", Hex: "44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a"},
|
||||
Data: []byte("{}"),
|
||||
for _, attestationLayer := range manifest.OriginalLayers {
|
||||
mt, err := attestationLayer.Layer.MediaType()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
||||
}
|
||||
if string(mt) == dsseMediaType {
|
||||
reader, err := attestationLayer.Layer.Uncompressed()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
env := new(Envelope)
|
||||
err = json.NewDecoder(reader).Decode(&env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode envelope: %w", err)
|
||||
}
|
||||
envs = append(envs, env)
|
||||
}
|
||||
}
|
||||
return mf, nil
|
||||
|
||||
return envs, nil
|
||||
}
|
||||
|
||||
func (i *EmptyConfigImage) RawManifest() ([]byte, error) {
|
||||
mf, err := i.Manifest()
|
||||
func ExtractStatementsFromIndex(idx v1.ImageIndex, mediaType string) ([]*AnnotatedStatement, error) {
|
||||
mfs2, err := idx.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get manifest: %w", err)
|
||||
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
||||
}
|
||||
return json.Marshal(mf)
|
||||
|
||||
var statements []*AnnotatedStatement
|
||||
|
||||
for i := range mfs2.Manifests {
|
||||
mf := &mfs2.Manifests[i]
|
||||
if mf.Annotations[DockerReferenceType] != "attestation-manifest" {
|
||||
continue
|
||||
}
|
||||
|
||||
attestationImage, err := idx.Image(mf.Digest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
|
||||
}
|
||||
layers, err := attestationImage.Layers()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract layers from attestation image: %w", err)
|
||||
}
|
||||
|
||||
for _, layer := range layers {
|
||||
// parse layer blob as json
|
||||
mt, err := layer.MediaType()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
||||
}
|
||||
|
||||
if string(mt) != mediaType {
|
||||
continue
|
||||
}
|
||||
r, err := layer.Uncompressed()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
||||
}
|
||||
defer r.Close()
|
||||
inTotoStatement := new(intoto.Statement)
|
||||
var desc *v1.Descriptor
|
||||
if strings.HasSuffix(string(mt), "+dsse") {
|
||||
env := new(Envelope)
|
||||
err = json.NewDecoder(r).Decode(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode env: %w", err)
|
||||
}
|
||||
payload, err := base64.StdEncoding.Strict().DecodeString(env.Payload)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode payload: %w", err)
|
||||
}
|
||||
err = json.Unmarshal([]byte(payload), inTotoStatement)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode %s statement: %w", mediaType, err)
|
||||
}
|
||||
} else {
|
||||
desc := new(v1.Descriptor)
|
||||
err = json.NewDecoder(r).Decode(desc)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode statement: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
layerDesc, err := partial.Descriptor(layer)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get descriptor for layer: %w", err)
|
||||
}
|
||||
annotations := make(map[string]string)
|
||||
for k, v := range layerDesc.Annotations {
|
||||
annotations[k] = v
|
||||
}
|
||||
statements = append(statements, &AnnotatedStatement{
|
||||
OCIDescriptor: desc,
|
||||
InTotoStatement: inTotoStatement,
|
||||
Annotations: annotations,
|
||||
})
|
||||
}
|
||||
}
|
||||
return statements, nil
|
||||
}
|
||||
|
||||
func (i *EmptyConfigImage) Digest() (v1.Hash, error) {
|
||||
mb, err := i.RawManifest()
|
||||
func ExtractAnnotatedStatements(path string, mediaType string) ([]*AnnotatedStatement, error) {
|
||||
idx, err := layout.ImageIndexFromPath(path)
|
||||
if err != nil {
|
||||
return v1.Hash{}, err
|
||||
return nil, fmt.Errorf("failed to load image index: %w", err)
|
||||
}
|
||||
digest, _, err := v1.SHA256(bytes.NewReader(mb))
|
||||
return digest, err
|
||||
|
||||
idxm, err := idx.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get digest: %w", err)
|
||||
}
|
||||
idxDigest := idxm.Manifests[0].Digest
|
||||
|
||||
mfs, err := idx.ImageIndex(idxDigest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
|
||||
}
|
||||
return ExtractStatementsFromIndex(mfs, mediaType)
|
||||
}
|
||||
|
||||
@@ -1,21 +1,18 @@
|
||||
package test
|
||||
package attestation_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
var UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||
|
||||
const (
|
||||
ExpectedStatements = 4
|
||||
)
|
||||
const ExpectedStatements = 4
|
||||
|
||||
func TestExtractAnnotatedStatements(t *testing.T) {
|
||||
statements, err := ExtractAnnotatedStatements(UnsignedTestImage, intoto.PayloadType)
|
||||
statements, err := attestation.ExtractAnnotatedStatements(test.UnsignedTestImage, intoto.PayloadType)
|
||||
assert.NoError(t, err)
|
||||
assert.Equalf(t, len(statements), ExpectedStatements, "expected %d statement, got %d", ExpectedStatements, len(statements))
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/docker/attest/pkg/attest"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/signerverifier"
|
||||
@@ -62,13 +61,13 @@ func ExampleManifest() {
|
||||
}
|
||||
|
||||
// create a new manifest to hold the attestation
|
||||
manifest, err := attest.NewAttestationManifest(desc)
|
||||
manifest, err := attestation.NewManifest(desc)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// sign and add the attestation to the manifest
|
||||
err = manifest.AddAttestation(context.Background(), signer, statement, opts)
|
||||
err = manifest.Add(context.Background(), signer, statement, opts)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -79,7 +78,11 @@ func ExampleManifest() {
|
||||
}
|
||||
|
||||
// save the manifest to the registry as a referrers artifact
|
||||
err = oci.SaveReferrers(manifest, output)
|
||||
artifacts, err := manifest.BuildReferringArtifacts()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
err = oci.SaveImagesNoTag(artifacts, output)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -1,36 +1,35 @@
|
||||
package oci
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
att "github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||
)
|
||||
|
||||
// implementation of AttestationResolver that closes over attestations from an oci layout.
|
||||
// implementation of Resolver that closes over attestations from an oci layout.
|
||||
type LayoutResolver struct {
|
||||
*attestation.Manifest
|
||||
*ImageSpec
|
||||
*Manifest
|
||||
*oci.ImageSpec
|
||||
}
|
||||
|
||||
func NewOCILayoutAttestationResolver(src *ImageSpec) (*LayoutResolver, error) {
|
||||
func NewOCILayoutResolver(src *oci.ImageSpec) (*LayoutResolver, error) {
|
||||
r := &LayoutResolver{
|
||||
ImageSpec: src,
|
||||
}
|
||||
_, err := r.fetchAttestationManifest()
|
||||
_, err := r.fetchManifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *LayoutResolver) fetchAttestationManifest() (*attestation.Manifest, error) {
|
||||
func (r *LayoutResolver) fetchManifest() (*Manifest, error) {
|
||||
if r.Manifest == nil {
|
||||
m, err := attestationManifestFromOCILayout(r.Identifier, r.ImageSpec.Platform)
|
||||
m, err := manifestFromOCILayout(r.Identifier, r.ImageSpec.Platform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -40,9 +39,9 @@ func (r *LayoutResolver) fetchAttestationManifest() (*attestation.Manifest, erro
|
||||
return r.Manifest, nil
|
||||
}
|
||||
|
||||
func (r *LayoutResolver) Attestations(_ context.Context, predicateType string) ([]*att.Envelope, error) {
|
||||
var envs []*att.Envelope
|
||||
dsseMediaType, err := attestation.DSSEMediaType(predicateType)
|
||||
func (r *LayoutResolver) Attestations(_ context.Context, predicateType string) ([]*Envelope, error) {
|
||||
var envs []*Envelope
|
||||
dsseMediaType, err := DSSEMediaType(predicateType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err)
|
||||
}
|
||||
@@ -55,7 +54,7 @@ func (r *LayoutResolver) Attestations(_ context.Context, predicateType string) (
|
||||
if mts != dsseMediaType {
|
||||
continue
|
||||
}
|
||||
env := new(att.Envelope)
|
||||
env := new(Envelope)
|
||||
// parse layer blob as json
|
||||
r, err := attestationLayer.Layer.Uncompressed()
|
||||
if err != nil {
|
||||
@@ -83,7 +82,7 @@ func (r *LayoutResolver) ImagePlatform(_ context.Context) (*v1.Platform, error)
|
||||
return r.ImageSpec.Platform, nil
|
||||
}
|
||||
|
||||
func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*attestation.Manifest, error) {
|
||||
func manifestFromOCILayout(path string, platform *v1.Platform) (*Manifest, error) {
|
||||
idx, err := layout.ImageIndexFromPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -120,11 +119,11 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*atte
|
||||
}
|
||||
for i := range mfs2.Manifests {
|
||||
mf := &mfs2.Manifests[i]
|
||||
if mf.Annotations[att.DockerReferenceType] != attestation.AttestationManifestType {
|
||||
if mf.Annotations[DockerReferenceType] != AttestationManifestType {
|
||||
continue
|
||||
}
|
||||
|
||||
if mf.Annotations[att.DockerReferenceDigest] != subjectDescriptor.Digest.String() {
|
||||
if mf.Annotations[DockerReferenceDigest] != subjectDescriptor.Digest.String() {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -132,11 +131,11 @@ func attestationManifestFromOCILayout(path string, platform *v1.Platform) (*atte
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract attestation image with digest %s: %w", mf.Digest.String(), err)
|
||||
}
|
||||
layers, err := attestation.GetAttestationsFromImage(attestationImage)
|
||||
layers, err := layersFromImage(attestationImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
|
||||
}
|
||||
attest := &attestation.Manifest{
|
||||
attest := &Manifest{
|
||||
OriginalLayers: layers,
|
||||
OriginalDescriptor: mf,
|
||||
SubjectName: idxDescriptor.Annotations["org.opencontainers.image.ref.name"],
|
||||
@@ -1,4 +1,4 @@
|
||||
package oci_test
|
||||
package attestation_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -24,7 +24,7 @@ func TestAttestationFromOCILayout(t *testing.T) {
|
||||
}
|
||||
|
||||
opts := &attestation.SigningOptions{}
|
||||
attIdx, err := oci.IndexFromPath(oci.UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
require.NoError(t, err)
|
||||
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
||||
require.NoError(t, err)
|
||||
@@ -1,17 +1,17 @@
|
||||
package oci
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
)
|
||||
|
||||
type MockResolver struct {
|
||||
Envs []*attestation.Envelope
|
||||
Envs []*Envelope
|
||||
}
|
||||
|
||||
func (r MockResolver) Attestations(_ context.Context, _ string) ([]*attestation.Envelope, error) {
|
||||
func (r MockResolver) Attestations(_ context.Context, _ string) ([]*Envelope, error) {
|
||||
return r.Envs, nil
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ func (r MockResolver) ImageDescriptor(_ context.Context) (*v1.Descriptor, error)
|
||||
}
|
||||
|
||||
func (r MockResolver) ImagePlatform(_ context.Context) (*v1.Platform, error) {
|
||||
return ParsePlatform("linux/amd64")
|
||||
return oci.ParsePlatform("linux/amd64")
|
||||
}
|
||||
|
||||
type MockRegistryResolver struct {
|
||||
@@ -1,22 +1,21 @@
|
||||
package oci
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
att "github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
)
|
||||
|
||||
type ReferrersResolver struct {
|
||||
referrersRepo string
|
||||
ImageDetailsResolver
|
||||
oci.ImageDetailsResolver
|
||||
}
|
||||
|
||||
func NewReferrersAttestationResolver(src ImageDetailsResolver, options ...func(*ReferrersResolver) error) (*ReferrersResolver, error) {
|
||||
func NewReferrersResolver(src oci.ImageDetailsResolver, options ...func(*ReferrersResolver) error) (*ReferrersResolver, error) {
|
||||
res := &ReferrersResolver{
|
||||
ImageDetailsResolver: src,
|
||||
}
|
||||
@@ -36,8 +35,8 @@ func WithReferrersRepo(repo string) func(*ReferrersResolver) error {
|
||||
}
|
||||
}
|
||||
|
||||
func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateType string) ([]*attestation.Manifest, error) {
|
||||
dsseMediaType, err := attestation.DSSEMediaType(predicateType)
|
||||
func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateType string) ([]*Manifest, error) {
|
||||
dsseMediaType, err := DSSEMediaType(predicateType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err)
|
||||
}
|
||||
@@ -54,19 +53,16 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateTy
|
||||
return nil, fmt.Errorf("failed to get descriptor: %w", err)
|
||||
}
|
||||
subjectDigest := desc.Digest.String()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get digest: %w", err)
|
||||
}
|
||||
var referrersSubjectRef name.Digest
|
||||
if r.referrersRepo != "" {
|
||||
referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", strings.TrimPrefix(r.referrersRepo, RegistryPrefix), subjectDigest))
|
||||
referrersSubjectRef, err = name.NewDigest(fmt.Sprintf("%s@%s", strings.TrimPrefix(r.referrersRepo, oci.RegistryPrefix), subjectDigest))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create referrers reference: %w", err)
|
||||
}
|
||||
} else {
|
||||
referrersSubjectRef = subjectRef.Context().Digest(subjectDigest)
|
||||
}
|
||||
options := WithOptions(ctx, nil)
|
||||
options := oci.WithOptions(ctx, nil)
|
||||
options = append(options, remote.WithFilter("artifactType", dsseMediaType))
|
||||
referrersIndex, err := remote.Referrers(referrersSubjectRef, options...)
|
||||
if err != nil {
|
||||
@@ -76,16 +72,16 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateTy
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get index manifest: %w", err)
|
||||
}
|
||||
aManifests := make([]*attestation.Manifest, 0)
|
||||
aManifests := make([]*Manifest, 0)
|
||||
for i := range referrersIndexManifest.Manifests {
|
||||
m := referrersIndexManifest.Manifests[i]
|
||||
remoteRef := referrersSubjectRef.Context().Digest(m.Digest.String())
|
||||
options = WithOptions(ctx, nil)
|
||||
options = oci.WithOptions(ctx, nil)
|
||||
attestationImage, err := remote.Image(remoteRef, options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get referred image: %w", err)
|
||||
}
|
||||
layers, err := attestation.GetAttestationsFromImage(attestationImage)
|
||||
layers, err := layersFromImage(attestationImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
|
||||
}
|
||||
@@ -99,7 +95,7 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateTy
|
||||
if string(mt) != dsseMediaType {
|
||||
return nil, fmt.Errorf("expected layer media type %s, got %s", dsseMediaType, mt)
|
||||
}
|
||||
attest := &attestation.Manifest{
|
||||
attest := &Manifest{
|
||||
SubjectName: imageName,
|
||||
OriginalLayers: layers,
|
||||
OriginalDescriptor: &m,
|
||||
@@ -110,12 +106,12 @@ func (r *ReferrersResolver) resolveAttestations(ctx context.Context, predicateTy
|
||||
return aManifests, nil
|
||||
}
|
||||
|
||||
func (r *ReferrersResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
||||
func (r *ReferrersResolver) Attestations(ctx context.Context, predicateType string) ([]*Envelope, error) {
|
||||
manifests, err := r.resolveAttestations(ctx, predicateType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to resolve attestations: %w", err)
|
||||
}
|
||||
var envs []*att.Envelope
|
||||
var envs []*Envelope
|
||||
for _, attest := range manifests {
|
||||
es, err := ExtractEnvelopes(attest, predicateType)
|
||||
if err != nil {
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/internal/util"
|
||||
"github.com/docker/attest/pkg/attest"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/config"
|
||||
@@ -16,14 +15,12 @@ import (
|
||||
"github.com/docker/attest/pkg/policy"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
"github.com/google/go-containerregistry/pkg/registry"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var (
|
||||
UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||
NoProvenanceImage = filepath.Join("..", "..", "test", "testdata", "no-provenance-image")
|
||||
PassPolicyDir = filepath.Join("..", "..", "test", "testdata", "local-policy-pass")
|
||||
LocalPolicy = filepath.Join("..", "..", "test", "testdata", "local-policy")
|
||||
@@ -94,7 +91,7 @@ func TestAttestationReferenceTypes(t *testing.T) {
|
||||
opts := &attestation.SigningOptions{
|
||||
SkipTL: true,
|
||||
}
|
||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
require.NoError(t, err)
|
||||
|
||||
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||
@@ -120,7 +117,9 @@ func TestAttestationReferenceTypes(t *testing.T) {
|
||||
output, err := oci.ParseImageSpec(outputRepo)
|
||||
require.NoError(t, err)
|
||||
for _, attIdx := range signedManifests {
|
||||
err = oci.SaveReferrers(attIdx, []*oci.ImageSpec{output})
|
||||
images, err := attIdx.BuildReferringArtifacts()
|
||||
require.NoError(t, err)
|
||||
err = oci.SaveImagesNoTag(images, []*oci.ImageSpec{output})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
|
||||
@@ -213,7 +212,7 @@ func TestReferencesInDifferentRepo(t *testing.T) {
|
||||
opts := &attestation.SigningOptions{
|
||||
SkipTL: true,
|
||||
}
|
||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
require.NoError(t, err)
|
||||
|
||||
indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName)
|
||||
@@ -226,7 +225,7 @@ func TestReferencesInDifferentRepo(t *testing.T) {
|
||||
// push signed attestation image to the ref server
|
||||
for _, signedManifest := range signedManifests {
|
||||
// push references using subject-digest.att convention
|
||||
image, err := signedManifest.BuildAttestationImage()
|
||||
image, err := signedManifest.BuildImage()
|
||||
require.NoError(t, err)
|
||||
err = oci.PushImageToRegistry(image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerURL.Host, repoName))
|
||||
require.NoError(t, err)
|
||||
@@ -239,7 +238,7 @@ func TestReferencesInDifferentRepo(t *testing.T) {
|
||||
opts := &attestation.SigningOptions{
|
||||
SkipTL: true,
|
||||
}
|
||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
require.NoError(t, err)
|
||||
|
||||
indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName)
|
||||
@@ -294,7 +293,7 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) {
|
||||
opts := &attestation.SigningOptions{
|
||||
SkipTL: true,
|
||||
}
|
||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
require.NoError(t, err)
|
||||
|
||||
indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName)
|
||||
@@ -327,14 +326,3 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmptyConfigImageDigest(t *testing.T) {
|
||||
empty := empty.Image
|
||||
img := attestation.EmptyConfigImage{empty}
|
||||
mf, err := img.RawManifest()
|
||||
require.NoError(t, err)
|
||||
hash := util.SHA256Hex(mf)
|
||||
digest, err := img.Digest()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, digest.Hex, hash)
|
||||
}
|
||||
|
||||
101
pkg/attestation/registry.go
Normal file
101
pkg/attestation/registry.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
)
|
||||
|
||||
type RegistryResolver struct {
|
||||
*oci.RegistryImageDetailsResolver
|
||||
*Manifest
|
||||
}
|
||||
|
||||
func NewRegistryResolver(src *oci.RegistryImageDetailsResolver) (*RegistryResolver, error) {
|
||||
return &RegistryResolver{
|
||||
RegistryImageDetailsResolver: src,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *RegistryResolver) Attestations(ctx context.Context, predicateType string) ([]*Envelope, error) {
|
||||
if r.Manifest == nil {
|
||||
attest, err := FetchManifest(ctx, r.Identifier, r.ImageSpec.Platform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Manifest = attest
|
||||
}
|
||||
return ExtractEnvelopes(r.Manifest, predicateType)
|
||||
}
|
||||
|
||||
func attestationDigestForImage(ix *v1.IndexManifest, imageDigest string, attestType string) (string, error) {
|
||||
for i := range ix.Manifests {
|
||||
m := &ix.Manifests[i]
|
||||
if v, ok := m.Annotations[DockerReferenceType]; ok && v == attestType {
|
||||
if d, ok := m.Annotations[DockerReferenceDigest]; ok && d == imageDigest {
|
||||
return m.Digest.String(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no attestation found for image %s", imageDigest)
|
||||
}
|
||||
|
||||
func FetchManifest(ctx context.Context, image string, platform *v1.Platform) (*Manifest, error) {
|
||||
// we want to get to the image index, so ignoring platform for now
|
||||
options := oci.WithOptions(ctx, nil)
|
||||
ref, err := name.ParseReference(image)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse reference: %w", err)
|
||||
}
|
||||
index, err := remote.Index(ref, options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get index: %w", err)
|
||||
}
|
||||
indexManifest, err := index.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get index manifest: %w", err)
|
||||
}
|
||||
subjectDescriptor, err := oci.ImageDescriptor(indexManifest, platform)
|
||||
if err != nil {
|
||||
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))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
||||
}
|
||||
|
||||
attestationDigest, err := attestationDigestForImage(indexManifest, digest, "attestation-manifest")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to obtain attestation for image: %w", err)
|
||||
}
|
||||
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), attestationDigest))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
||||
}
|
||||
remoteDescriptor, err := remote.Get(ref, options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestation: %w", err)
|
||||
}
|
||||
attestationImage, err := remoteDescriptor.Image()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestation image: %w", err)
|
||||
}
|
||||
|
||||
layers, err := layersFromImage(attestationImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
|
||||
}
|
||||
attest := &Manifest{
|
||||
OriginalLayers: layers,
|
||||
OriginalDescriptor: &remoteDescriptor.Descriptor,
|
||||
SubjectName: image,
|
||||
SubjectDescriptor: subjectDescriptor,
|
||||
}
|
||||
return attest, nil
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package oci_test
|
||||
package attestation_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@@ -25,7 +25,7 @@ func TestRegistry(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
opts := &attestation.SigningOptions{}
|
||||
attIdx, err := oci.IndexFromPath(oci.UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
require.NoError(t, err)
|
||||
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
||||
require.NoError(t, err)
|
||||
12
pkg/attestation/resolver.go
Normal file
12
pkg/attestation/resolver.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
)
|
||||
|
||||
type Resolver interface {
|
||||
oci.ImageDetailsResolver
|
||||
Attestations(ctx context.Context, mediaType string) ([]*Envelope, error)
|
||||
}
|
||||
@@ -6,12 +6,19 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/signerverifier"
|
||||
"github.com/google/go-containerregistry/pkg/registry"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/static"
|
||||
"github.com/google/go-containerregistry/pkg/v1/types"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
@@ -149,3 +156,146 @@ func TestSignVerifyAttestation(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddSignedLayerAnnotations(t *testing.T) {
|
||||
ctx, signer := test.Setup(t)
|
||||
testCases := []struct {
|
||||
name string
|
||||
replace bool
|
||||
}{
|
||||
{"replaced", true},
|
||||
{"not replaced", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
data := []byte("signed")
|
||||
testLayer := static.NewLayer(data, types.MediaType(intoto.PayloadType))
|
||||
mediaType := types.OCIManifestSchema1
|
||||
opts := &attestation.SigningOptions{}
|
||||
originalLayer := &attestation.Layer{
|
||||
Layer: testLayer,
|
||||
Statement: &intoto.Statement{
|
||||
StatementHeader: intoto.StatementHeader{
|
||||
PredicateType: attestation.VSAPredicateType,
|
||||
},
|
||||
},
|
||||
Annotations: map[string]string{"test": "test"},
|
||||
}
|
||||
|
||||
manifest := &attestation.Manifest{
|
||||
OriginalDescriptor: &v1.Descriptor{
|
||||
MediaType: mediaType,
|
||||
},
|
||||
OriginalLayers: []*attestation.Layer{
|
||||
originalLayer,
|
||||
},
|
||||
SubjectDescriptor: &v1.Descriptor{},
|
||||
}
|
||||
err := manifest.Add(ctx, signer, originalLayer.Statement, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
newImg, err := manifest.BuildImage(attestation.WithReplacedLayers(tc.replace))
|
||||
require.NoError(t, err)
|
||||
mf, _ := newImg.RawManifest()
|
||||
type Annotations struct {
|
||||
Annotations map[string]string `json:"annotations"`
|
||||
}
|
||||
type Layers struct {
|
||||
Layers []Annotations `json:"layers"`
|
||||
}
|
||||
l := &Layers{}
|
||||
err = json.Unmarshal(mf, l)
|
||||
require.NoError(t, err)
|
||||
_, ok := l.Layers[0].Annotations["test"]
|
||||
assert.Truef(t, ok, "missing annotations")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
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 := attestation.NewManifest(subject)
|
||||
require.NoError(t, err)
|
||||
err = manifest.Add(ctx, signer, statement, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = manifest.Add(ctx, signer, statement2, opts)
|
||||
require.NoError(t, err)
|
||||
|
||||
// fake that the manfifest was loaded from a real image
|
||||
manifest.OriginalLayers = manifest.SignedLayers
|
||||
envelopes, err := attestation.ExtractEnvelopes(manifest, attestation.VSAPredicateType)
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, envelopes, 2)
|
||||
|
||||
newImg, err := manifest.BuildImage(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.Contains(t, mf.ArtifactType, "application/vnd.in-toto")
|
||||
assert.Contains(t, mf.ArtifactType, "+dsse")
|
||||
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)
|
||||
artifacts, err := manifest.BuildReferringArtifacts()
|
||||
require.NoError(t, err)
|
||||
err = oci.SaveImagesNoTag(artifacts, output)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,12 @@ type Extension struct {
|
||||
Ext *DockerDSSEExtension `json:"ext"`
|
||||
}
|
||||
|
||||
type AnnotatedStatement struct {
|
||||
OCIDescriptor *v1.Descriptor
|
||||
InTotoStatement *intoto.Statement
|
||||
Annotations map[string]string
|
||||
}
|
||||
|
||||
type DockerDSSEExtension struct {
|
||||
TL *DockerTLExtension `json:"tl"`
|
||||
}
|
||||
@@ -83,6 +89,12 @@ type SigningOptions struct {
|
||||
SkipTL bool
|
||||
}
|
||||
|
||||
type Options struct {
|
||||
NoReferrers bool
|
||||
Attach bool
|
||||
ReferrersRepo string
|
||||
}
|
||||
|
||||
func DSSEMediaType(predicateType string) (string, error) {
|
||||
var predicateName string
|
||||
switch predicateType {
|
||||
|
||||
2
pkg/mirror/README.md
Normal file
2
pkg/mirror/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## mirror
|
||||
This package contains components to mirror TUF metadata and targets to OCI.
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
@@ -17,7 +18,7 @@ import (
|
||||
// -----------------
|
||||
|
||||
// GetMetadataManifest returns an image with TUF root metadata as layers.
|
||||
func (m *TUFMirror) GetMetadataManifest(metadataURL string) (v1.Image, error) {
|
||||
func (m *TUFMirror) GetMetadataManifest(metadataURL string) (*oci.EmptyConfigImage, error) {
|
||||
metadata, err := m.getMetadataMirror(metadataURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get metadata: %w", err)
|
||||
@@ -26,7 +27,7 @@ func (m *TUFMirror) GetMetadataManifest(metadataURL string) (v1.Image, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to build metadata manifest: %w", err)
|
||||
}
|
||||
return manifest, nil
|
||||
return &oci.EmptyConfigImage{Image: manifest}, nil
|
||||
}
|
||||
|
||||
// getMetadataMirror returns a TufMetadata struct with TUF metadata as map of file names to bytes.
|
||||
@@ -183,7 +184,7 @@ func (m *TUFMirror) buildDelegatedMetadataManifests(delegated []DelegatedTargetM
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to append delegated targets layer to image: %w", err)
|
||||
}
|
||||
manifests = append(manifests, &Image{Image: img, Tag: role.Name})
|
||||
manifests = append(manifests, &Image{Image: &oci.EmptyConfigImage{Image: img}, Tag: role.Name})
|
||||
}
|
||||
return manifests, nil
|
||||
}
|
||||
|
||||
@@ -16,15 +16,20 @@ import (
|
||||
"github.com/theupdateframework/go-tuf/v2/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
metadataPath = "/metadata"
|
||||
targetsPath = "/targets"
|
||||
)
|
||||
|
||||
func TestGetTufMetadataMirror(t *testing.T) {
|
||||
server := httptest.NewServer(http.FileServer(http.Dir(filepath.Join("..", "..", "test", "testdata", "tuf", "test-repo"))))
|
||||
defer server.Close()
|
||||
|
||||
path := test.CreateTempDir(t, "", "tuf_temp")
|
||||
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker())
|
||||
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
|
||||
assert.NoError(t, err)
|
||||
|
||||
tufMetadata, err := m.getMetadataMirror(server.URL + "/metadata")
|
||||
tufMetadata, err := m.getMetadataMirror(server.URL + metadataPath)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// check that all roles are not empty
|
||||
@@ -39,10 +44,10 @@ func TestGetMetadataManifest(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
path := test.CreateTempDir(t, "", "tuf_temp")
|
||||
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker())
|
||||
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
|
||||
assert.NoError(t, err)
|
||||
|
||||
img, err := m.GetMetadataManifest(server.URL + "/metadata")
|
||||
img, err := m.GetMetadataManifest(server.URL + metadataPath)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, img)
|
||||
|
||||
@@ -78,7 +83,7 @@ func TestGetDelegatedMetadataMirrors(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
path := test.CreateTempDir(t, "", "tuf_temp")
|
||||
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker())
|
||||
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
|
||||
assert.NoError(t, err)
|
||||
|
||||
delegations, err := m.GetDelegatedMetadataMirrors()
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
"github.com/google/go-containerregistry/pkg/v1/mutate"
|
||||
@@ -42,7 +43,7 @@ func (m *TUFMirror) GetTUFTargetMirrors() ([]*Image, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to append role layer to image: %w", err)
|
||||
}
|
||||
targetMirrors = append(targetMirrors, &Image{Image: img, Tag: name})
|
||||
targetMirrors = append(targetMirrors, &Image{Image: &oci.EmptyConfigImage{Image: img}, Tag: name})
|
||||
}
|
||||
return targetMirrors, nil
|
||||
}
|
||||
@@ -93,9 +94,10 @@ func (m *TUFMirror) GetDelegatedTargetMirrors() ([]*Index, error) {
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to append role layer to image: %w", err)
|
||||
}
|
||||
emptyConfigImage := &oci.EmptyConfigImage{Image: img}
|
||||
// append image to index with annotation
|
||||
index = mutate.AppendManifests(index, mutate.IndexAddendum{
|
||||
Add: img,
|
||||
Add: emptyConfigImage,
|
||||
Descriptor: v1.Descriptor{
|
||||
Annotations: map[string]string{
|
||||
tufFileAnnotation: fmt.Sprintf("%s/%s", subdir, name),
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestGetTufTargetsMirror(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
path := test.CreateTempDir(t, "", "tuf_temp")
|
||||
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker())
|
||||
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
|
||||
assert.NoError(t, err)
|
||||
|
||||
targets, err := m.GetTUFTargetMirrors()
|
||||
@@ -61,7 +61,7 @@ func TestTargetDelegationMetadata(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
path := test.CreateTempDir(t, "", "tuf_temp")
|
||||
tm, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker())
|
||||
tm, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
|
||||
assert.NoError(t, err)
|
||||
|
||||
targets, err := tm.TUFClient.LoadDelegatedTargets("test-role", "targets")
|
||||
@@ -74,7 +74,7 @@ func TestGetDelegatedTargetMirrors(t *testing.T) {
|
||||
defer server.Close()
|
||||
|
||||
path := test.CreateTempDir(t, "", "tuf_temp")
|
||||
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+"/metadata", server.URL+"/targets", tuf.NewMockVersionChecker())
|
||||
m, err := NewTUFMirror(embed.RootDev.Data, path, server.URL+metadataPath, server.URL+targetsPath, tuf.NewMockVersionChecker())
|
||||
assert.NoError(t, err)
|
||||
|
||||
mirrors, err := m.GetDelegatedTargetMirrors()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package mirror
|
||||
|
||||
import (
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/tuf"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/theupdateframework/go-tuf/v2/metadata"
|
||||
@@ -32,7 +33,7 @@ type DelegatedTargetMetadata struct {
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
Image v1.Image
|
||||
Image *oci.EmptyConfigImage
|
||||
Tag string
|
||||
}
|
||||
|
||||
|
||||
2
pkg/oci/README.md
Normal file
2
pkg/oci/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## oci
|
||||
This package is for generic OCI components. For attestation specific components see the `attestation` package.
|
||||
@@ -1,19 +1,17 @@
|
||||
//go:build e2e
|
||||
|
||||
package mirror_test
|
||||
package oci_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRegistryAuth(t *testing.T) {
|
||||
UnsignedTestImage := filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||
|
||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
require.NoError(t, err)
|
||||
// test cases for ecr, gcr and dockerhub
|
||||
testCases := []struct {
|
||||
@@ -2,14 +2,11 @@ package oci
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/containerd/platforms"
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
att "github.com/docker/attest/pkg/attestation"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
"github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
|
||||
@@ -45,36 +42,7 @@ func WithOptions(ctx context.Context, platform *v1.Platform) []remote.Option {
|
||||
return options
|
||||
}
|
||||
|
||||
func ExtractEnvelopes(manifest *attestation.Manifest, predicateType string) ([]*att.Envelope, error) {
|
||||
var envs []*att.Envelope
|
||||
dsseMediaType, err := attestation.DSSEMediaType(predicateType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get DSSE media type for predicate '%s': %w", predicateType, err)
|
||||
}
|
||||
for _, attestationLayer := range manifest.OriginalLayers {
|
||||
mt, err := attestationLayer.Layer.MediaType()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
||||
}
|
||||
if string(mt) == dsseMediaType {
|
||||
reader, err := attestationLayer.Layer.Uncompressed()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
||||
}
|
||||
defer reader.Close()
|
||||
env := new(att.Envelope)
|
||||
err = json.NewDecoder(reader).Decode(&env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode envelope: %w", err)
|
||||
}
|
||||
envs = append(envs, env)
|
||||
}
|
||||
}
|
||||
|
||||
return envs, nil
|
||||
}
|
||||
|
||||
func imageDescriptor(ix *v1.IndexManifest, platform *v1.Platform) (*v1.Descriptor, error) {
|
||||
func ImageDescriptor(ix *v1.IndexManifest, platform *v1.Platform) (*v1.Descriptor, error) {
|
||||
for i := range ix.Manifests {
|
||||
m := &ix.Manifests[i]
|
||||
if (m.MediaType == ocispec.MediaTypeImageManifest || m.MediaType == "application/vnd.docker.distribution.manifest.v2+json") && m.Platform.Equals(*platform) {
|
||||
@@ -84,18 +52,6 @@ func imageDescriptor(ix *v1.IndexManifest, platform *v1.Platform) (*v1.Descripto
|
||||
return nil, fmt.Errorf("no image found for platform %v", platform)
|
||||
}
|
||||
|
||||
func attestationDigestForDigest(ix *v1.IndexManifest, imageDigest string, attestType string) (string, error) {
|
||||
for i := range ix.Manifests {
|
||||
m := &ix.Manifests[i]
|
||||
if v, ok := m.Annotations[att.DockerReferenceType]; ok && v == attestType {
|
||||
if d, ok := m.Annotations[att.DockerReferenceDigest]; ok && d == imageDigest {
|
||||
return m.Digest.String(), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("no attestation found for image %s", imageDigest)
|
||||
}
|
||||
|
||||
func RefToPURL(ref string, platform *v1.Platform) (string, bool, error) {
|
||||
var isCanonical bool
|
||||
named, err := reference.ParseNormalizedNamed(ref)
|
||||
@@ -150,7 +106,7 @@ func SplitDigest(digest string) (common.DigestSet, error) {
|
||||
}
|
||||
|
||||
func ReplaceTagInSpec(src *ImageSpec, digest v1.Hash) (*ImageSpec, error) {
|
||||
newName, err := replaceTag(src.Identifier, digest)
|
||||
newName, err := ReplaceTag(src.Identifier, digest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse repo name: %w", err)
|
||||
}
|
||||
@@ -162,7 +118,7 @@ func ReplaceTagInSpec(src *ImageSpec, digest v1.Hash) (*ImageSpec, error) {
|
||||
}
|
||||
|
||||
// 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) {
|
||||
func ReplaceTag(image string, digest v1.Hash) (string, error) {
|
||||
if strings.HasPrefix(image, LocalPrefix) {
|
||||
return image, nil
|
||||
}
|
||||
|
||||
@@ -1,56 +1,55 @@
|
||||
package oci
|
||||
package oci_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/internal/test"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
var UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||
|
||||
func TestRefToPurl(t *testing.T) {
|
||||
arm, err := ParsePlatform("arm64/linux")
|
||||
arm, err := oci.ParsePlatform("arm64/linux")
|
||||
require.NoError(t, err)
|
||||
purl, canonical, err := RefToPURL("alpine", arm)
|
||||
purl, canonical, err := oci.RefToPURL("alpine", arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/alpine@latest?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
|
||||
purl, canonical, err = RefToPURL("alpine:123", arm)
|
||||
purl, canonical, err = oci.RefToPURL("alpine:123", arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
|
||||
purl, canonical, err = RefToPURL("google/alpine:123", arm)
|
||||
purl, canonical, err = oci.RefToPURL("google/alpine:123", arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/google/alpine@123?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
|
||||
purl, canonical, err = RefToPURL("library/alpine:123", arm)
|
||||
purl, canonical, err = oci.RefToPURL("library/alpine:123", arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
|
||||
purl, canonical, err = RefToPURL("docker.io/library/alpine:123", arm)
|
||||
purl, canonical, err = oci.RefToPURL("docker.io/library/alpine:123", arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/alpine@123?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
|
||||
purl, canonical, err = RefToPURL("localhost:5001/library/alpine:123", arm)
|
||||
purl, canonical, err = oci.RefToPURL("localhost:5001/library/alpine:123", arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/localhost%3A5001/library/alpine@123?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
|
||||
purl, canonical, err = RefToPURL("localhost:5001/alpine:123", arm)
|
||||
purl, canonical, err = oci.RefToPURL("localhost:5001/alpine:123", arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/localhost%3A5001/alpine@123?platform=arm64%2Flinux", purl)
|
||||
assert.False(t, canonical)
|
||||
|
||||
purl, canonical, err = RefToPURL("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b", arm)
|
||||
purl, canonical, err = oci.RefToPURL("localhost:5001/alpine@sha256:c5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b", arm)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "pkg:docker/localhost%3A5001/alpine?digest=sha256%3Ac5b1261d6d3e43071626931fc004f70149baeba2c8ec672bd4f27761f8e1ad6b&platform=arm64%2Flinux", purl)
|
||||
assert.True(t, canonical)
|
||||
@@ -58,7 +57,7 @@ func TestRefToPurl(t *testing.T) {
|
||||
|
||||
// Test fix for https://github.com/docker/secure-artifacts-team-issues/issues/202
|
||||
func TestImageDigestForPlatform(t *testing.T) {
|
||||
idx, err := layout.ImageIndexFromPath(UnsignedTestImage)
|
||||
idx, err := layout.ImageIndexFromPath(test.UnsignedTestImage)
|
||||
assert.NoError(t, err)
|
||||
|
||||
idxm, err := idx.IndexManifest()
|
||||
@@ -72,16 +71,16 @@ func TestImageDigestForPlatform(t *testing.T) {
|
||||
mfs2, err := mfs.IndexManifest()
|
||||
assert.NoError(t, err)
|
||||
|
||||
p, err := ParsePlatform("linux/amd64")
|
||||
p, err := oci.ParsePlatform("linux/amd64")
|
||||
assert.NoError(t, err)
|
||||
desc, err := imageDescriptor(mfs2, p)
|
||||
desc, err := oci.ImageDescriptor(mfs2, p)
|
||||
assert.NoError(t, err)
|
||||
digest := desc.Digest.String()
|
||||
assert.Equal(t, "sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620", digest)
|
||||
|
||||
p, err = ParsePlatform("linux/arm64")
|
||||
p, err = oci.ParsePlatform("linux/arm64")
|
||||
assert.NoError(t, err)
|
||||
desc, err = imageDescriptor(mfs2, p)
|
||||
desc, err = oci.ImageDescriptor(mfs2, p)
|
||||
assert.NoError(t, err)
|
||||
digest = desc.Digest.String()
|
||||
assert.Equal(t, "sha256:7a76cec943853f9f7105b1976afa1bf7cd5bb6afc4e9d5852dd8da7cf81ae86e", digest)
|
||||
@@ -95,14 +94,14 @@ func TestWithoutTag(t *testing.T) {
|
||||
{name: "image:tag", expected: "index.docker.io/library/image"},
|
||||
{name: "image", expected: "index.docker.io/library/image"},
|
||||
{name: "image:sha256-digest.att", expected: "index.docker.io/library/image"},
|
||||
{name: RegistryPrefix + "image:tag", expected: RegistryPrefix + "index.docker.io/library/image"},
|
||||
{name: oci.RegistryPrefix + "image:tag", expected: oci.RegistryPrefix + "index.docker.io/library/image"},
|
||||
{name: "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "index.docker.io/library/image"},
|
||||
{name: RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: RegistryPrefix + "index.docker.io/library/image"},
|
||||
{name: RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: RegistryPrefix + "127.0.0.1:36555/repo"},
|
||||
{name: oci.RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: oci.RegistryPrefix + "index.docker.io/library/image"},
|
||||
{name: oci.RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: oci.RegistryPrefix + "127.0.0.1:36555/repo"},
|
||||
}
|
||||
for _, c := range tc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
notag, _ := WithoutTag(c.name)
|
||||
notag, _ := oci.WithoutTag(c.name)
|
||||
assert.Equal(t, c.expected, notag)
|
||||
})
|
||||
}
|
||||
@@ -116,11 +115,11 @@ func TestReplaceTag(t *testing.T) {
|
||||
{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: RegistryPrefix + "image:tag", expected: RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: oci.RegistryPrefix + "image:tag", expected: oci.RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: LocalPrefix + "foobar", expected: LocalPrefix + "foobar"},
|
||||
{name: RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: RegistryPrefix + "127.0.0.1:36555/repo:sha256-digest.att"},
|
||||
{name: oci.LocalPrefix + "foobar", expected: oci.LocalPrefix + "foobar"},
|
||||
{name: oci.RegistryPrefix + "image@sha256:166710df254975d4a6c4c407c315951c22753dcaa829e020a3fd5d18fff70dd2", expected: oci.RegistryPrefix + "index.docker.io/library/image:sha256-digest.att"},
|
||||
{name: oci.RegistryPrefix + "127.0.0.1:36555/repo:latest", expected: oci.RegistryPrefix + "127.0.0.1:36555/repo:sha256-digest.att"},
|
||||
}
|
||||
|
||||
digest := v1.Hash{
|
||||
@@ -129,7 +128,7 @@ func TestReplaceTag(t *testing.T) {
|
||||
}
|
||||
for _, c := range tc {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
replaced, err := replaceTag(c.name, digest)
|
||||
replaced, err := oci.ReplaceTag(c.name, digest)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, c.expected, replaced)
|
||||
})
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
@@ -13,6 +12,7 @@ import (
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
)
|
||||
|
||||
// PushImageToRegistry pushes an image to the registry with the specified name.
|
||||
func PushImageToRegistry(image v1.Image, imageName string) error {
|
||||
ref, err := name.ParseReference(imageName)
|
||||
if err != nil {
|
||||
@@ -23,6 +23,7 @@ func PushImageToRegistry(image v1.Image, imageName string) error {
|
||||
return remote.Write(ref, image, MultiKeychainOption())
|
||||
}
|
||||
|
||||
// PushIndexToRegistry pushes an index to the registry with the specified name.
|
||||
func PushIndexToRegistry(index v1.ImageIndex, imageName string) error {
|
||||
// Parse the index name
|
||||
ref, err := name.ParseReference(imageName)
|
||||
@@ -34,6 +35,7 @@ func PushIndexToRegistry(index v1.ImageIndex, imageName string) error {
|
||||
return remote.WriteIndex(ref, index, MultiKeychainOption())
|
||||
}
|
||||
|
||||
// SaveIndexAsOCILayout saves an image as an OCI layout to the specified path.
|
||||
func SaveImageAsOCILayout(image v1.Image, path string) error {
|
||||
// Save the image to the local filesystem
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
@@ -48,6 +50,7 @@ func SaveImageAsOCILayout(image v1.Image, path string) error {
|
||||
return l.AppendImage(image)
|
||||
}
|
||||
|
||||
// SaveIndexAsOCILayout saves an index as an OCI layout to the specified path.
|
||||
func SaveIndexAsOCILayout(image v1.ImageIndex, path string) error {
|
||||
// Save the index to the local filesystem
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
@@ -62,6 +65,7 @@ func SaveIndexAsOCILayout(image v1.ImageIndex, path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveIndex saves an index to the specified outputs.
|
||||
func SaveIndex(outputs []*ImageSpec, index v1.ImageIndex, indexName string) error {
|
||||
// split output by comma and write or push each one
|
||||
for _, output := range outputs {
|
||||
@@ -89,6 +93,7 @@ func SaveIndex(outputs []*ImageSpec, index v1.ImageIndex, indexName string) erro
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveImage saves an image to the specified output.
|
||||
func SaveImage(output *ImageSpec, image v1.Image, imageName string) error {
|
||||
if output.Type == OCI {
|
||||
idx := v1.ImageIndex(empty.Index)
|
||||
@@ -113,26 +118,23 @@ func SaveImage(output *ImageSpec, image v1.Image, imageName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func SaveReferrers(manifest *attestation.Manifest, outputs []*ImageSpec) error {
|
||||
// SaveImagesNoTag saves a list of images by digest to the specified outputs.
|
||||
func SaveImagesNoTag(images []v1.Image, outputs []*ImageSpec) error {
|
||||
for _, output := range outputs {
|
||||
// OCI layout output for referrers not supported
|
||||
// OCI layout output not supported
|
||||
if output.Type == OCI {
|
||||
continue
|
||||
}
|
||||
images, err := manifest.BuildReferringArtifacts()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to build image: %w", err)
|
||||
}
|
||||
for _, image := range images {
|
||||
digest, err := image.Digest()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get attestation image digest: %w", err)
|
||||
return fmt.Errorf("failed to get image digest: %w", err)
|
||||
}
|
||||
attOut, err := ReplaceDigestInSpec(output, digest)
|
||||
spec, err := ReplaceDigestInSpec(output, digest)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create attestation image spec: %w", err)
|
||||
return fmt.Errorf("failed to create image spec: %w", err)
|
||||
}
|
||||
err = PushImageToRegistry(image, attOut.Identifier)
|
||||
err = PushImageToRegistry(image, spec.Identifier)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to push image: %w", err)
|
||||
}
|
||||
|
||||
@@ -4,11 +4,9 @@ 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"
|
||||
@@ -19,9 +17,8 @@ import (
|
||||
)
|
||||
|
||||
func TestSavingIndex(t *testing.T) {
|
||||
UnsignedTestImage := filepath.Join("..", "..", "test", "testdata", "unsigned-test-image")
|
||||
outputLayout := test.CreateTempDir(t, "", "mirror-test")
|
||||
attIdx, err := oci.IndexFromPath(UnsignedTestImage)
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
require.NoError(t, err)
|
||||
|
||||
server := httptest.NewServer(registry.New())
|
||||
@@ -80,9 +77,9 @@ func TestSavingReferrers(t *testing.T) {
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
Digest: digest,
|
||||
}
|
||||
manifest, err := attest.NewAttestationManifest(subject)
|
||||
manifest, err := attestation.NewManifest(subject)
|
||||
require.NoError(t, err)
|
||||
err = manifest.AddAttestation(ctx, signer, statement, opts)
|
||||
err = manifest.Add(ctx, signer, statement, opts)
|
||||
require.NoError(t, err)
|
||||
server := httptest.NewServer(registry.New(registry.WithReferrersSupport(true)))
|
||||
defer server.Close()
|
||||
@@ -93,16 +90,18 @@ func TestSavingReferrers(t *testing.T) {
|
||||
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||
output, err := oci.ParseImageSpecs(indexName)
|
||||
require.NoError(t, err)
|
||||
err = oci.SaveReferrers(manifest, output)
|
||||
artifacts, err := manifest.BuildReferringArtifacts()
|
||||
require.NoError(t, err)
|
||||
err = oci.SaveImagesNoTag(artifacts, output)
|
||||
require.NoError(t, err)
|
||||
|
||||
reg := &oci.MockRegistryResolver{
|
||||
reg := &attestation.MockRegistryResolver{
|
||||
Subject: subject,
|
||||
MockResolver: &oci.MockResolver{},
|
||||
MockResolver: &attestation.MockResolver{},
|
||||
ImageNameStr: indexName,
|
||||
}
|
||||
require.NoError(t, err)
|
||||
refResolver, err := oci.NewReferrersAttestationResolver(reg)
|
||||
refResolver, err := attestation.NewReferrersResolver(reg)
|
||||
require.NoError(t, err)
|
||||
attestations, err := refResolver.Attestations(ctx, attestation.VSAPredicateType)
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -4,18 +4,11 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
att "github.com/docker/attest/pkg/attestation"
|
||||
"github.com/google/go-containerregistry/pkg/name"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/remote"
|
||||
)
|
||||
|
||||
type RegistryResolver struct {
|
||||
*RegistryImageDetailsResolver
|
||||
*attestation.Manifest
|
||||
}
|
||||
|
||||
type RegistryImageDetailsResolver struct {
|
||||
*ImageSpec
|
||||
descriptor *v1.Descriptor
|
||||
@@ -27,12 +20,6 @@ func NewRegistryImageDetailsResolver(src *ImageSpec) (*RegistryImageDetailsResol
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewRegistryAttestationResolver(src *RegistryImageDetailsResolver) (*RegistryResolver, error) {
|
||||
return &RegistryResolver{
|
||||
RegistryImageDetailsResolver: src,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *RegistryImageDetailsResolver) ImageName(_ context.Context) (string, error) {
|
||||
return r.Identifier, nil
|
||||
}
|
||||
@@ -72,70 +59,3 @@ func (r *RegistryImageDetailsResolver) ImageDescriptor(ctx context.Context) (*v1
|
||||
}
|
||||
return r.descriptor, nil
|
||||
}
|
||||
|
||||
func (r *RegistryResolver) Attestations(ctx context.Context, predicateType string) ([]*att.Envelope, error) {
|
||||
if r.Manifest == nil {
|
||||
attest, err := FetchAttestationManifest(ctx, r.Identifier, r.ImageSpec.Platform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Manifest = attest
|
||||
}
|
||||
return ExtractEnvelopes(r.Manifest, predicateType)
|
||||
}
|
||||
|
||||
func FetchAttestationManifest(ctx context.Context, image string, platform *v1.Platform) (*attestation.Manifest, error) {
|
||||
// we want to get to the image index, so ignoring platform for now
|
||||
options := WithOptions(ctx, nil)
|
||||
ref, err := name.ParseReference(image)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse reference: %w", err)
|
||||
}
|
||||
index, err := remote.Index(ref, options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get index: %w", err)
|
||||
}
|
||||
indexManifest, err := index.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get index manifest: %w", err)
|
||||
}
|
||||
subjectDescriptor, err := imageDescriptor(indexManifest, platform)
|
||||
if err != nil {
|
||||
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))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
||||
}
|
||||
|
||||
attestationDigest, err := attestationDigestForDigest(indexManifest, digest, "attestation-manifest")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to obtain attestation for image: %w", err)
|
||||
}
|
||||
ref, err = name.ParseReference(fmt.Sprintf("%s@%s", ref.Context().Name(), attestationDigest))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse attestation reference: %w", err)
|
||||
}
|
||||
remoteDescriptor, err := remote.Get(ref, options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestation: %w", err)
|
||||
}
|
||||
attestationImage, err := remoteDescriptor.Image()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestation image: %w", err)
|
||||
}
|
||||
|
||||
layers, err := attestation.GetAttestationsFromImage(attestationImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
|
||||
}
|
||||
attest := &attestation.Manifest{
|
||||
OriginalLayers: layers,
|
||||
OriginalDescriptor: &remoteDescriptor.Descriptor,
|
||||
SubjectName: image,
|
||||
SubjectDescriptor: subjectDescriptor,
|
||||
}
|
||||
return attest, nil
|
||||
}
|
||||
|
||||
@@ -3,15 +3,9 @@ package oci
|
||||
import (
|
||||
"context"
|
||||
|
||||
att "github.com/docker/attest/pkg/attestation"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
)
|
||||
|
||||
type AttestationResolver interface {
|
||||
ImageDetailsResolver
|
||||
Attestations(ctx context.Context, mediaType string) ([]*att.Envelope, error)
|
||||
}
|
||||
|
||||
type ImageDetailsResolver interface {
|
||||
ImageName(ctx context.Context) (string, error)
|
||||
ImagePlatform(ctx context.Context) (*v1.Platform, error)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
@@ -26,12 +28,6 @@ type (
|
||||
}
|
||||
)
|
||||
|
||||
type AttestationOptions struct {
|
||||
NoReferrers bool
|
||||
Attach bool
|
||||
ReferrersRepo string
|
||||
}
|
||||
|
||||
type ImageSpecOption func(*ImageSpec) error
|
||||
|
||||
type ImageSpec struct {
|
||||
@@ -180,3 +176,42 @@ func WithoutTag(image string) (string, error) {
|
||||
repo := ref.Context().Name()
|
||||
return prefix + repo, 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)
|
||||
}
|
||||
|
||||
func (i *EmptyConfigImage) Digest() (v1.Hash, error) {
|
||||
mb, err := i.RawManifest()
|
||||
if err != nil {
|
||||
return v1.Hash{}, err
|
||||
}
|
||||
digest, _, err := v1.SHA256(bytes.NewReader(mb))
|
||||
return digest, err
|
||||
}
|
||||
|
||||
21
pkg/oci/types_test.go
Normal file
21
pkg/oci/types_test.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package oci
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/docker/attest/internal/util"
|
||||
"github.com/google/go-containerregistry/pkg/v1/empty"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestEmptyConfigImageDigest(t *testing.T) {
|
||||
empty := empty.Image
|
||||
img := EmptyConfigImage{Image: empty}
|
||||
mf, err := img.RawManifest()
|
||||
require.NoError(t, err)
|
||||
hash := util.SHA256Hex(mf)
|
||||
digest, err := img.Digest()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, digest.Hex, hash)
|
||||
}
|
||||
2
pkg/policy/README.md
Normal file
2
pkg/policy/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## policy
|
||||
This package is for attestation policy mapping and evaluation.
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
)
|
||||
|
||||
type policyEvaluatorCtxKeyType struct{}
|
||||
@@ -26,5 +26,5 @@ func GetPolicyEvaluator(ctx context.Context) (Evaluator, error) {
|
||||
}
|
||||
|
||||
type Evaluator interface {
|
||||
Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *Input) (*Result, error)
|
||||
Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error)
|
||||
}
|
||||
|
||||
@@ -3,14 +3,14 @@ package policy
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
)
|
||||
|
||||
type MockPolicyEvaluator struct {
|
||||
EvaluateFunc func(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *Input) (*Result, error)
|
||||
EvaluateFunc func(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error)
|
||||
}
|
||||
|
||||
func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *Input) (*Result, error) {
|
||||
func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error) {
|
||||
if pe.EvaluateFunc != nil {
|
||||
return pe.EvaluateFunc(ctx, resolver, pctx, input)
|
||||
}
|
||||
@@ -19,7 +19,7 @@ func (pe *MockPolicyEvaluator) Evaluate(ctx context.Context, resolver oci.Attest
|
||||
|
||||
func GetMockPolicy() Evaluator {
|
||||
return &MockPolicyEvaluator{
|
||||
EvaluateFunc: func(_ context.Context, _ oci.AttestationResolver, _ *Policy, _ *Input) (*Result, error) {
|
||||
EvaluateFunc: func(_ context.Context, _ attestation.Resolver, _ *Policy, _ *Input) (*Result, error) {
|
||||
return AllowedResult(), nil
|
||||
},
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/distribution/reference"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/config"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
)
|
||||
@@ -211,28 +212,28 @@ func normalizeImageName(imageName string) (string, error) {
|
||||
func CreateImageDetailsResolver(imageSource *oci.ImageSpec) (oci.ImageDetailsResolver, error) {
|
||||
switch imageSource.Type {
|
||||
case oci.OCI:
|
||||
return oci.NewOCILayoutAttestationResolver(imageSource)
|
||||
return attestation.NewOCILayoutResolver(imageSource)
|
||||
case oci.Docker:
|
||||
return oci.NewRegistryImageDetailsResolver(imageSource)
|
||||
}
|
||||
return nil, fmt.Errorf("unsupported image source type: %s", imageSource.Type)
|
||||
}
|
||||
|
||||
func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *config.PolicyMapping) (oci.AttestationResolver, error) {
|
||||
func CreateAttestationResolver(resolver oci.ImageDetailsResolver, mapping *config.PolicyMapping) (attestation.Resolver, error) {
|
||||
if mapping.Attestations != nil {
|
||||
if mapping.Attestations.Style == config.AttestationStyleAttached {
|
||||
switch resolver := resolver.(type) {
|
||||
case *oci.RegistryImageDetailsResolver:
|
||||
return oci.NewRegistryAttestationResolver(resolver)
|
||||
case *oci.LayoutResolver:
|
||||
return attestation.NewRegistryResolver(resolver)
|
||||
case *attestation.LayoutResolver:
|
||||
return resolver, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported image details resolver type: %T", resolver)
|
||||
}
|
||||
}
|
||||
if mapping.Attestations.Repo != "" {
|
||||
return oci.NewReferrersAttestationResolver(resolver, oci.WithReferrersRepo(mapping.Attestations.Repo))
|
||||
return attestation.NewReferrersResolver(resolver, attestation.WithReferrersRepo(mapping.Attestations.Repo))
|
||||
}
|
||||
}
|
||||
return oci.NewReferrersAttestationResolver(resolver)
|
||||
return attestation.NewReferrersResolver(resolver)
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
|
||||
|
||||
re := policy.NewRegoEvaluator(true)
|
||||
|
||||
defaultResolver := oci.MockResolver{
|
||||
defaultResolver := attestation.MockResolver{
|
||||
Envs: []*attestation.Envelope{loadAttestation(t, ExampleAttestation)},
|
||||
}
|
||||
|
||||
@@ -46,7 +46,7 @@ func TestRegoEvaluator_Evaluate(t *testing.T) {
|
||||
repo string
|
||||
expectSuccess bool
|
||||
isCanonical bool
|
||||
resolver oci.AttestationResolver
|
||||
resolver attestation.Resolver
|
||||
policy *policy.Options
|
||||
policyID string
|
||||
errorStr string
|
||||
@@ -117,10 +117,10 @@ func TestLoadingMappings(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCreateAttestationResolver(t *testing.T) {
|
||||
mockResolver := oci.MockResolver{
|
||||
mockResolver := attestation.MockResolver{
|
||||
Envs: []*attestation.Envelope{},
|
||||
}
|
||||
layoutResolver := &oci.LayoutResolver{}
|
||||
layoutResolver := &attestation.LayoutResolver{}
|
||||
registryResolver := &oci.RegistryImageDetailsResolver{}
|
||||
|
||||
nilRepoReferrers := &config.PolicyMapping{
|
||||
@@ -166,11 +166,11 @@ func TestCreateAttestationResolver(t *testing.T) {
|
||||
return
|
||||
}
|
||||
switch resolver.(type) {
|
||||
case *oci.ReferrersResolver:
|
||||
case *attestation.ReferrersResolver:
|
||||
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleReferrers)
|
||||
case *oci.RegistryResolver:
|
||||
case *attestation.RegistryResolver:
|
||||
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleAttached)
|
||||
case *oci.LayoutResolver:
|
||||
case *attestation.LayoutResolver:
|
||||
assert.Equal(t, tc.mapping.Attestations.Style, config.AttestationStyleAttached)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -7,8 +7,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
att "github.com/docker/attest/pkg/attestation"
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
"github.com/docker/attest/pkg/attestation"
|
||||
intoto "github.com/in-toto/in-toto-golang/in_toto"
|
||||
"github.com/open-policy-agent/opa/ast"
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
@@ -36,7 +35,7 @@ func NewRegoEvaluator(debug bool) Evaluator {
|
||||
}
|
||||
}
|
||||
|
||||
func (re *regoEvaluator) Evaluate(ctx context.Context, resolver oci.AttestationResolver, pctx *Policy, input *Input) (*Result, error) {
|
||||
func (re *regoEvaluator) Evaluate(ctx context.Context, resolver attestation.Resolver, pctx *Policy, input *Input) (*Result, error) {
|
||||
var regoOpts []func(*rego.Rego)
|
||||
|
||||
// Create a new in-memory store
|
||||
@@ -170,7 +169,7 @@ func handleErrors2(f func(rCtx *rego.BuiltinContext, a, b *ast.Term) (*ast.Term,
|
||||
}
|
||||
}
|
||||
|
||||
func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin {
|
||||
func RegoFunctions(resolver attestation.Resolver) []*tester.Builtin {
|
||||
return []*tester.Builtin{
|
||||
{
|
||||
Decl: verifyDecl,
|
||||
@@ -197,7 +196,7 @@ func RegoFunctions(resolver oci.AttestationResolver) []*tester.Builtin {
|
||||
}
|
||||
}
|
||||
|
||||
func fetchInTotoAttestations(resolver oci.AttestationResolver) rego.Builtin1 {
|
||||
func fetchInTotoAttestations(resolver attestation.Resolver) rego.Builtin1 {
|
||||
return func(rCtx rego.BuiltinContext, predicateTypeTerm *ast.Term) (*ast.Term, error) {
|
||||
predicateTypeStr, ok := predicateTypeTerm.Value.(ast.String)
|
||||
if !ok {
|
||||
@@ -228,8 +227,8 @@ func fetchInTotoAttestations(resolver oci.AttestationResolver) rego.Builtin1 {
|
||||
}
|
||||
|
||||
func verifyInTotoEnvelope(rCtx *rego.BuiltinContext, envTerm, optsTerm *ast.Term) (*ast.Term, error) {
|
||||
env := new(att.Envelope)
|
||||
opts := new(att.VerifyOptions)
|
||||
env := new(attestation.Envelope)
|
||||
opts := new(attestation.VerifyOptions)
|
||||
err := ast.As(envTerm.Value, env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to cast envelope: %w", err)
|
||||
@@ -239,7 +238,7 @@ func verifyInTotoEnvelope(rCtx *rego.BuiltinContext, envTerm, optsTerm *ast.Term
|
||||
return nil, fmt.Errorf("failed to cast verifier options: %w", err)
|
||||
}
|
||||
|
||||
payload, err := att.VerifyDSSE(rCtx.Context, env, opts)
|
||||
payload, err := attestation.VerifyDSSE(rCtx.Context, env, opts)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
2
pkg/signerverifier/README.md
Normal file
2
pkg/signerverifier/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## signerverifier
|
||||
This package implements methods to sign and verify attestation envelopes.
|
||||
2
pkg/tlog/README.md
Normal file
2
pkg/tlog/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## tlog
|
||||
This package implements transparency logging.
|
||||
2
pkg/tuf/README.md
Normal file
2
pkg/tuf/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## tuf
|
||||
This package implements TUF clients for http and oci data sources.
|
||||
@@ -32,6 +32,9 @@ const (
|
||||
tufTargetMediaType = "application/vnd.tuf.target"
|
||||
testRole = "test-role"
|
||||
tufMetadataRepo = "tuf-metadata"
|
||||
targetsPath = "/tuf-targets"
|
||||
metadataPath = "/tuf-metadata"
|
||||
targetsRepo = "test" + targetsPath
|
||||
)
|
||||
|
||||
func TestRegistryFetcher(t *testing.T) {
|
||||
@@ -44,9 +47,9 @@ func TestRegistryFetcher(t *testing.T) {
|
||||
}()
|
||||
LoadRegistryTestData(t, regAddr, OCITUFTestDataPath)
|
||||
|
||||
metadataRepo := regAddr.Host + "/tuf-metadata"
|
||||
metadataRepo := regAddr.Host + metadataPath
|
||||
metadataImgTag := LatestTag
|
||||
targetsRepo := regAddr.Host + "/tuf-targets"
|
||||
targetsRepo := regAddr.Host + targetsPath
|
||||
targetFile := "test.txt"
|
||||
delegatedRole := testRole
|
||||
dir := CreateTempDir(t, "", "tuf_temp")
|
||||
@@ -122,7 +125,7 @@ func TestFindFileInManifest(t *testing.T) {
|
||||
// make test image manifest
|
||||
file := "test.json"
|
||||
data := []byte("test")
|
||||
hash := v1.Hash{Algorithm: "sha256", Hex: util.SHA256Hex(data)}
|
||||
hash := v1.Hash{Hex: util.SHA256Hex(data)}
|
||||
img := empty.Image
|
||||
img = mutate.MediaType(img, types.OCIManifestSchema1)
|
||||
img = mutate.ConfigMediaType(img, types.OCIConfigJSON)
|
||||
@@ -150,7 +153,6 @@ func TestFindFileInManifest(t *testing.T) {
|
||||
indexManifest, err := idx.RawManifest()
|
||||
assert.NoError(t, err)
|
||||
// cache image layer
|
||||
targetsRepo := "test/tuf-targets"
|
||||
d := &RegistryFetcher{
|
||||
cache: NewImageCache(),
|
||||
targetsRepo: targetsRepo,
|
||||
@@ -183,9 +185,8 @@ func TestFindFileInManifest(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseImgRef(t *testing.T) {
|
||||
metadataRepo := "test/tuf-metadata"
|
||||
metadataRepo := "test" + metadataPath
|
||||
metadataTag := LatestTag
|
||||
targetsRepo := "test/tuf-targets"
|
||||
delegatedRole := testRole
|
||||
testCases := []struct {
|
||||
name string
|
||||
|
||||
4
scripts/README.md
Normal file
4
scripts/README.md
Normal file
@@ -0,0 +1,4 @@
|
||||
## scripts
|
||||
This directory contains project scripts.
|
||||
|
||||
`gen-testdata.sh` - used to generate static test data saved in `/test/testdata/`
|
||||
2
test/README.md
Normal file
2
test/README.md
Normal file
@@ -0,0 +1,2 @@
|
||||
## test
|
||||
This directory contains static `testdata` used to run go tests.
|
||||
Reference in New Issue
Block a user