fix: cyclical imports
This commit is contained in:
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,8 @@ import (
|
||||
"github.com/secure-systems-lab/go-securesystemslib/dsse"
|
||||
)
|
||||
|
||||
// GetAttestationManifestsFromIndex extracts all attestation manifests from an index.
|
||||
func GetAttestationManifestsFromIndex(index v1.ImageIndex) ([]*Manifest, error) {
|
||||
// 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 +45,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 +60,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 +97,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 +108,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 +131,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 +143,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 +173,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 +207,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 +232,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 +244,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 +253,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 +299,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)
|
||||
}
|
||||
|
||||
18
pkg/attestation/attestation_test.go
Normal file
18
pkg/attestation/attestation_test.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package attestation_test
|
||||
|
||||
import (
|
||||
"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"
|
||||
)
|
||||
|
||||
const ExpectedStatements = 4
|
||||
|
||||
func TestExtractAnnotatedStatements(t *testing.T) {
|
||||
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))
|
||||
}
|
||||
@@ -68,7 +68,7 @@ func ExampleManifest() {
|
||||
}
|
||||
|
||||
// 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 +79,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)
|
||||
}
|
||||
|
||||
147
pkg/attestation/layout.go
Normal file
147
pkg/attestation/layout.go
Normal file
@@ -0,0 +1,147 @@
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/google/go-containerregistry/pkg/v1/layout"
|
||||
)
|
||||
|
||||
// implementation of Resolver that closes over attestations from an oci layout.
|
||||
type LayoutResolver struct {
|
||||
*Manifest
|
||||
*oci.ImageSpec
|
||||
}
|
||||
|
||||
func NewOCILayoutResolver(src *oci.ImageSpec) (*LayoutResolver, error) {
|
||||
r := &LayoutResolver{
|
||||
ImageSpec: src,
|
||||
}
|
||||
_, err := r.fetchManifest()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *LayoutResolver) fetchManifest() (*Manifest, error) {
|
||||
if r.Manifest == nil {
|
||||
m, err := manifestFromOCILayout(r.Identifier, r.ImageSpec.Platform)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.Manifest = m
|
||||
}
|
||||
|
||||
return r.Manifest, nil
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
for _, attestationLayer := range r.Manifest.OriginalLayers {
|
||||
mt, err := attestationLayer.Layer.MediaType()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
||||
}
|
||||
mts := string(mt)
|
||||
if mts != dsseMediaType {
|
||||
continue
|
||||
}
|
||||
env := new(Envelope)
|
||||
// parse layer blob as json
|
||||
r, err := attestationLayer.Layer.Uncompressed()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer contents: %w", err)
|
||||
}
|
||||
defer r.Close()
|
||||
err = json.NewDecoder(r).Decode(env)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode envelope: %w", err)
|
||||
}
|
||||
envs = append(envs, env)
|
||||
}
|
||||
return envs, nil
|
||||
}
|
||||
|
||||
func (r *LayoutResolver) ImageName(_ context.Context) (string, error) {
|
||||
return r.SubjectName, nil
|
||||
}
|
||||
|
||||
func (r *LayoutResolver) ImageDescriptor(_ context.Context) (*v1.Descriptor, error) {
|
||||
return r.SubjectDescriptor, nil
|
||||
}
|
||||
|
||||
func (r *LayoutResolver) ImagePlatform(_ context.Context) (*v1.Platform, error) {
|
||||
return r.ImageSpec.Platform, nil
|
||||
}
|
||||
|
||||
func manifestFromOCILayout(path string, platform *v1.Platform) (*Manifest, error) {
|
||||
idx, err := layout.ImageIndexFromPath(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
idxm, err := idx.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get digest: %w", err)
|
||||
}
|
||||
|
||||
idxDescriptor := idxm.Manifests[0]
|
||||
idxDigest := idxDescriptor.Digest
|
||||
|
||||
mfs, err := idx.ImageIndex(idxDigest)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract ImageIndex for digest %s: %w", idxDigest.String(), err)
|
||||
}
|
||||
mfs2, err := mfs.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract IndexManifest from ImageIndex: %w", err)
|
||||
}
|
||||
var subjectDescriptor *v1.Descriptor
|
||||
for i := range mfs2.Manifests {
|
||||
manifest := &mfs2.Manifests[i]
|
||||
if manifest.Platform != nil {
|
||||
if manifest.Platform.Equals(*platform) {
|
||||
subjectDescriptor = manifest
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if subjectDescriptor == nil {
|
||||
return nil, fmt.Errorf("platform not found in index")
|
||||
}
|
||||
for i := range mfs2.Manifests {
|
||||
mf := &mfs2.Manifests[i]
|
||||
if mf.Annotations[DockerReferenceType] != AttestationManifestType {
|
||||
continue
|
||||
}
|
||||
|
||||
if mf.Annotations[DockerReferenceDigest] != subjectDescriptor.Digest.String() {
|
||||
continue
|
||||
}
|
||||
|
||||
attestationImage, err := mfs.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 := layersFromImage(attestationImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
|
||||
}
|
||||
attest := &Manifest{
|
||||
OriginalLayers: layers,
|
||||
OriginalDescriptor: mf,
|
||||
SubjectName: idxDescriptor.Annotations["org.opencontainers.image.ref.name"],
|
||||
SubjectDescriptor: subjectDescriptor,
|
||||
}
|
||||
return attest, nil
|
||||
}
|
||||
return nil, fmt.Errorf("attestation manifest not found")
|
||||
}
|
||||
68
pkg/attestation/layout_test.go
Normal file
68
pkg/attestation/layout_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
package attestation_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"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/docker/attest/pkg/policy"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAttestationFromOCILayout(t *testing.T) {
|
||||
ctx, signer := test.Setup(t)
|
||||
outputLayout := test.CreateTempDir(t, "", "attest-oci-layout")
|
||||
|
||||
invalidPlatform := &v1.Platform{
|
||||
Architecture: "invalid",
|
||||
OS: "invalid",
|
||||
}
|
||||
|
||||
opts := &attestation.SigningOptions{}
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
require.NoError(t, err)
|
||||
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
||||
require.NoError(t, err)
|
||||
signedIndex := attIdx.Index
|
||||
signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests)
|
||||
require.NoError(t, err)
|
||||
spec, err := oci.ParseImageSpec(oci.LocalPrefix + outputLayout)
|
||||
require.NoError(t, err)
|
||||
err = oci.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, outputLayout)
|
||||
require.NoError(t, err)
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
platform *v1.Platform
|
||||
errorStr string
|
||||
}{
|
||||
{name: "nominal", platform: spec.Platform},
|
||||
{name: "invalid platform", platform: invalidPlatform, errorStr: "platform not found in index"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
spec := &oci.ImageSpec{
|
||||
Type: oci.OCI,
|
||||
Identifier: outputLayout,
|
||||
Platform: tc.platform,
|
||||
}
|
||||
resolver, err := policy.CreateImageDetailsResolver(spec)
|
||||
if tc.errorStr != "" {
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.errorStr)
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
desc, err := resolver.ImageDescriptor(ctx)
|
||||
require.NoError(t, err)
|
||||
digest := desc.Digest.String()
|
||||
assert.True(t, strings.Contains(digest, "sha256:"))
|
||||
})
|
||||
}
|
||||
}
|
||||
50
pkg/attestation/mock.go
Normal file
50
pkg/attestation/mock.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/docker/attest/pkg/oci"
|
||||
v1 "github.com/google/go-containerregistry/pkg/v1"
|
||||
)
|
||||
|
||||
type MockResolver struct {
|
||||
Envs []*Envelope
|
||||
}
|
||||
|
||||
func (r MockResolver) Attestations(_ context.Context, _ string) ([]*Envelope, error) {
|
||||
return r.Envs, nil
|
||||
}
|
||||
|
||||
func (r MockResolver) ImageName(_ context.Context) (string, error) {
|
||||
return "library/alpine:latest", nil
|
||||
}
|
||||
|
||||
func (r MockResolver) ImageDescriptor(_ context.Context) (*v1.Descriptor, error) {
|
||||
digest, err := v1.NewHash("sha256:da8b190665956ea07890a0273e2a9c96bfe291662f08e2860e868eef69c34620")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &v1.Descriptor{
|
||||
Digest: digest,
|
||||
Size: 1234,
|
||||
MediaType: "application/vnd.oci.image.manifest.v1+json",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r MockResolver) ImagePlatform(_ context.Context) (*v1.Platform, error) {
|
||||
return oci.ParsePlatform("linux/amd64")
|
||||
}
|
||||
|
||||
type MockRegistryResolver struct {
|
||||
Subject *v1.Descriptor
|
||||
ImageNameStr string
|
||||
*MockResolver
|
||||
}
|
||||
|
||||
func (r *MockRegistryResolver) ImageDescriptor(_ context.Context) (*v1.Descriptor, error) {
|
||||
return r.Subject, nil
|
||||
}
|
||||
|
||||
func (r *MockRegistryResolver) ImageName(_ context.Context) (string, error) {
|
||||
return r.ImageNameStr, nil
|
||||
}
|
||||
123
pkg/attestation/referrers.go
Normal file
123
pkg/attestation/referrers.go
Normal file
@@ -0,0 +1,123 @@
|
||||
package attestation
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"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
|
||||
oci.ImageDetailsResolver
|
||||
}
|
||||
|
||||
func NewReferrersResolver(src oci.ImageDetailsResolver, options ...func(*ReferrersResolver) error) (*ReferrersResolver, error) {
|
||||
res := &ReferrersResolver{
|
||||
ImageDetailsResolver: src,
|
||||
}
|
||||
for _, opt := range options {
|
||||
err := opt(res)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func WithReferrersRepo(repo string) func(*ReferrersResolver) error {
|
||||
return func(r *ReferrersResolver) error {
|
||||
r.referrersRepo = repo
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
imageName, err := r.ImageName(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get image name: %w", err)
|
||||
}
|
||||
subjectRef, err := name.ParseReference(imageName)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse reference: %w", err)
|
||||
}
|
||||
desc, err := r.ImageDescriptor(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get descriptor: %w", err)
|
||||
}
|
||||
subjectDigest := desc.Digest.String()
|
||||
var referrersSubjectRef name.Digest
|
||||
if r.referrersRepo != "" {
|
||||
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 := oci.WithOptions(ctx, nil)
|
||||
options = append(options, remote.WithFilter("artifactType", dsseMediaType))
|
||||
referrersIndex, err := remote.Referrers(referrersSubjectRef, options...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get referrers: %w", err)
|
||||
}
|
||||
referrersIndexManifest, err := referrersIndex.IndexManifest()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get index manifest: %w", err)
|
||||
}
|
||||
aManifests := make([]*Manifest, 0)
|
||||
for i := range referrersIndexManifest.Manifests {
|
||||
m := referrersIndexManifest.Manifests[i]
|
||||
remoteRef := referrersSubjectRef.Context().Digest(m.Digest.String())
|
||||
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 := layersFromImage(attestationImage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get attestations from image: %w", err)
|
||||
}
|
||||
if len(layers) != 1 {
|
||||
return nil, fmt.Errorf("expected exactly one layer, got %d", len(layers))
|
||||
}
|
||||
mt, err := layers[0].Layer.MediaType()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get layer media type: %w", err)
|
||||
}
|
||||
if string(mt) != dsseMediaType {
|
||||
return nil, fmt.Errorf("expected layer media type %s, got %s", dsseMediaType, mt)
|
||||
}
|
||||
attest := &Manifest{
|
||||
SubjectName: imageName,
|
||||
OriginalLayers: layers,
|
||||
OriginalDescriptor: &m,
|
||||
SubjectDescriptor: desc,
|
||||
}
|
||||
aManifests = append(aManifests, attest)
|
||||
}
|
||||
return aManifests, nil
|
||||
}
|
||||
|
||||
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 []*Envelope
|
||||
for _, attest := range manifests {
|
||||
es, err := ExtractEnvelopes(attest, predicateType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to extract envelopes: %w", err)
|
||||
}
|
||||
envs = append(envs, es...)
|
||||
}
|
||||
return envs, nil
|
||||
}
|
||||
@@ -23,7 +23,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")
|
||||
LocalPolicy = filepath.Join("..", "..", "test", "testdata", "local-policy")
|
||||
@@ -94,7 +93,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 +119,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 +214,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 +227,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 +240,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 +295,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)
|
||||
@@ -330,7 +331,7 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) {
|
||||
|
||||
func TestEmptyConfigImageDigest(t *testing.T) {
|
||||
empty := empty.Image
|
||||
img := attestation.EmptyConfigImage{empty}
|
||||
img := oci.EmptyConfigImage{Image: empty}
|
||||
mf, err := img.RawManifest()
|
||||
require.NoError(t, err)
|
||||
hash := util.SHA256Hex(mf)
|
||||
|
||||
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
|
||||
}
|
||||
50
pkg/attestation/registry_test.go
Normal file
50
pkg/attestation/registry_test.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package attestation_test
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"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/docker/attest/pkg/policy"
|
||||
"github.com/google/go-containerregistry/pkg/registry"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRegistry(t *testing.T) {
|
||||
ctx, signer := test.Setup(t)
|
||||
server := httptest.NewServer(registry.New(registry.WithReferrersSupport(false)))
|
||||
defer server.Close()
|
||||
u, err := url.Parse(server.URL)
|
||||
require.NoError(t, err)
|
||||
|
||||
opts := &attestation.SigningOptions{}
|
||||
attIdx, err := oci.IndexFromPath(test.UnsignedTestImage)
|
||||
require.NoError(t, err)
|
||||
signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts)
|
||||
require.NoError(t, err)
|
||||
signedIndex := attIdx.Index
|
||||
signedIndex, err = attestation.UpdateIndexImages(signedIndex, signedManifests)
|
||||
require.NoError(t, err)
|
||||
|
||||
indexName := fmt.Sprintf("%s/repo:root", u.Host)
|
||||
require.NoError(t, err)
|
||||
err = oci.PushIndexToRegistry(signedIndex, indexName)
|
||||
require.NoError(t, err)
|
||||
|
||||
spec, err := oci.ParseImageSpec(indexName)
|
||||
require.NoError(t, err)
|
||||
|
||||
resolver, err := policy.CreateImageDetailsResolver(spec)
|
||||
require.NoError(t, err)
|
||||
desc, err := resolver.ImageDescriptor(ctx)
|
||||
require.NoError(t, err)
|
||||
digest := desc.Digest.String()
|
||||
assert.True(t, strings.Contains(digest, "sha256:"))
|
||||
}
|
||||
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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user