diff --git a/go.mod b/go.mod index b46feb6..c09d710 100644 --- a/go.mod +++ b/go.mod @@ -28,8 +28,8 @@ require ( sigs.k8s.io/yaml v1.4.0 ) -// fork of a fork (in case it goes away) with changes to support ArtifactType (https://github.com/google/go-containerregistry/pull/1931) -replace github.com/google/go-containerregistry => github.com/kipz/go-containerregistry v0.0.0-20240722163910-ebe90246535d +// fork with changes to support ArtifactType (https://github.com/google/go-containerregistry/pull/1931) +replace github.com/google/go-containerregistry => github.com/docker/go-containerregistry v0.0.0-20240808132857-c8bfc44af7c8 require ( cloud.google.com/go v0.115.0 // indirect @@ -75,7 +75,7 @@ require ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/digitorus/pkcs7 v0.0.0-20230818184609-3a137a874352 // indirect github.com/digitorus/timestamp v0.0.0-20231217203849-220c5c2851b7 // indirect - github.com/docker/cli v26.1.3+incompatible // indirect + github.com/docker/cli v27.1.1+incompatible // indirect github.com/docker/distribution v2.8.3+incompatible // indirect github.com/docker/docker v27.1.0+incompatible // indirect github.com/docker/docker-credential-helpers v0.8.1 // indirect diff --git a/go.sum b/go.sum index ce87ba6..3a415bd 100644 --- a/go.sum +++ b/go.sum @@ -218,8 +218,8 @@ github.com/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v26.1.3+incompatible h1:bUpXT/N0kDE3VUHI2r5VMsYQgi38kYuoC0oL9yt3lqc= -github.com/docker/cli v26.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v27.1.1+incompatible h1:goaZxOqs4QKxznZjjBWKONQci/MywhtRv2oNn0GkeZE= +github.com/docker/cli v27.1.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v27.1.0+incompatible h1:rEHVQc4GZ0MIQKifQPHSFGV/dVgaZafgRf8fCPtDYBs= @@ -228,6 +228,8 @@ github.com/docker/docker-credential-helpers v0.8.1 h1:j/eKUktUltBtMzKqmfLB0PAgqY github.com/docker/docker-credential-helpers v0.8.1/go.mod h1:P3ci7E3lwkZg6XiHdRKft1KckHiO9a2rNtyFbZ/ry9M= github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= +github.com/docker/go-containerregistry v0.0.0-20240808132857-c8bfc44af7c8 h1:T/wutVfQ1Oj4H5tbP5IZL5l6PZqzvapVJ5cB4Wy4Ucc= +github.com/docker/go-containerregistry v0.0.0-20240808132857-c8bfc44af7c8/go.mod h1:z38EKdKh4h7IP2gSfUUqEvalZBqs6AoLeWfUy34nQC8= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= @@ -417,8 +419,6 @@ github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8Hm github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/kipz/go-containerregistry v0.0.0-20240722163910-ebe90246535d h1:5QaWAwKhslfqxEyMZY0ofvsbMJkMLcx5E30JFufMVj8= -github.com/kipz/go-containerregistry v0.0.0-20240722163910-ebe90246535d/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.17.8 h1:YcnTYrq7MikUT7k0Yb5eceMmALQPYBW/Xltxn0NAMnU= diff --git a/pkg/attest/example_sign_test.go b/pkg/attest/example_sign_test.go index a867562..c4ef6ad 100644 --- a/pkg/attest/example_sign_test.go +++ b/pkg/attest/example_sign_test.go @@ -5,7 +5,6 @@ import ( "github.com/docker/attest/pkg/attest" "github.com/docker/attest/pkg/attestation" - "github.com/docker/attest/pkg/mirror" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/signerverifier" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -55,7 +54,7 @@ func ExampleSignStatements_remote() { } // push image index with signed attestation-manifests - err = mirror.PushIndexToRegistry(signedIndex, ref) + err = oci.PushIndexToRegistry(signedIndex, ref) if err != nil { panic(err) } @@ -70,7 +69,7 @@ func ExampleSignStatements_remote() { }, }, }) - err = mirror.SaveIndexAsOCILayout(idx, path) + err = oci.SaveIndexAsOCILayout(idx, path) if err != nil { panic(err) } diff --git a/pkg/attest/sign_test.go b/pkg/attest/sign_test.go index de577fe..c10265f 100644 --- a/pkg/attest/sign_test.go +++ b/pkg/attest/sign_test.go @@ -10,7 +10,6 @@ import ( "github.com/docker/attest/internal/test" "github.com/docker/attest/pkg/attestation" - "github.com/docker/attest/pkg/mirror" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/policy" "github.com/google/go-containerregistry/pkg/registry" @@ -65,7 +64,7 @@ func TestSignVerifyOCILayout(t *testing.T) { require.NoError(t, err) spec, err := oci.ParseImageSpec(oci.LocalPrefix + outputLayout) require.NoError(t, err) - err = mirror.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name) + err = oci.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name) require.NoError(t, err) policy, err := Verify(ctx, spec, policyOpts) require.NoError(t, err) @@ -226,7 +225,7 @@ func TestSimpleStatementSigning(t *testing.T) { indexName := fmt.Sprintf("%s/repo:root", u.Host) output, err := oci.ParseImageSpecs(indexName) require.NoError(t, err) - err = mirror.SaveReferrers(manifest, output) + err = oci.SaveReferrers(manifest, output) require.NoError(t, err) }) } diff --git a/pkg/attest/verify_test.go b/pkg/attest/verify_test.go index 34c4ecc..5034e0c 100644 --- a/pkg/attest/verify_test.go +++ b/pkg/attest/verify_test.go @@ -11,7 +11,6 @@ import ( "github.com/docker/attest/internal/test" "github.com/docker/attest/pkg/attestation" "github.com/docker/attest/pkg/config" - "github.com/docker/attest/pkg/mirror" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/policy" intoto "github.com/in-toto/in-toto-golang/in_toto" @@ -84,7 +83,7 @@ func TestVSA(t *testing.T) { // output signed attestations spec, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64)) require.NoError(t, err) - err = mirror.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name) + err = oci.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name) assert.NoError(t, err) // mocked vsa query should pass @@ -134,7 +133,7 @@ func TestVerificationFailure(t *testing.T) { // output signed attestations spec, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64)) require.NoError(t, err) - err = mirror.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name) + err = oci.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, attIdx.Name) assert.NoError(t, err) // mocked vsa query should fail @@ -208,7 +207,7 @@ func TestSignVerify(t *testing.T) { // output signed attestations spec, err := oci.ParseImageSpec(oci.LocalPrefix+outputLayout, oci.WithPlatform(LinuxAMD64)) require.NoError(t, err) - err = mirror.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, imageName) + err = oci.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, imageName) require.NoError(t, err) policyOpts := &policy.Options{ diff --git a/pkg/attestation/attestation.go b/pkg/attestation/attestation.go index c1a522c..c7050dc 100644 --- a/pkg/attestation/attestation.go +++ b/pkg/attestation/attestation.go @@ -1,6 +1,7 @@ package attestation import ( + "bytes" "context" "encoding/json" "fmt" @@ -249,7 +250,7 @@ func (manifest *Manifest) BuildReferringArtifacts() ([]v1.Image, error) { return images, nil } -// build and image containing only layers. +// build an image containing only layers. func buildImage(layers []*Layer, manifest *v1.Descriptor, subject *v1.Descriptor, opts *ManifestImageOptions) (v1.Image, error) { newImg := empty.Image var err error @@ -329,3 +330,12 @@ func (i *EmptyConfigImage) RawManifest() ([]byte, error) { } 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 +} diff --git a/pkg/attestation/example_attestation_manifest_test.go b/pkg/attestation/example_attestation_manifest_test.go index 8abe117..040501d 100644 --- a/pkg/attestation/example_attestation_manifest_test.go +++ b/pkg/attestation/example_attestation_manifest_test.go @@ -6,7 +6,6 @@ import ( "github.com/docker/attest/pkg/attest" "github.com/docker/attest/pkg/attestation" - "github.com/docker/attest/pkg/mirror" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/signerverifier" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -80,7 +79,7 @@ func ExampleManifest() { } // save the manifest to the registry as a referrers artifact - err = mirror.SaveReferrers(manifest, output) + err = oci.SaveReferrers(manifest, output) if err != nil { panic(err) } diff --git a/pkg/attestation/referrers_test.go b/pkg/attestation/referrers_test.go index 6588769..8c02c38 100644 --- a/pkg/attestation/referrers_test.go +++ b/pkg/attestation/referrers_test.go @@ -8,14 +8,15 @@ 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" - "github.com/docker/attest/pkg/mirror" "github.com/docker/attest/pkg/oci" "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" @@ -112,14 +113,14 @@ func TestAttestationReferenceTypes(t *testing.T) { // push subject image so that it can be resolved require.NoError(t, err) - err = mirror.PushIndexToRegistry(attIdx.Index, indexName) + err = oci.PushIndexToRegistry(attIdx.Index, indexName) require.NoError(t, err) // upload referrers output, err := oci.ParseImageSpec(outputRepo) require.NoError(t, err) for _, attIdx := range signedManifests { - err = mirror.SaveReferrers(attIdx, []*oci.ImageSpec{output}) + err = oci.SaveReferrers(attIdx, []*oci.ImageSpec{output}) require.NoError(t, err) } @@ -216,7 +217,7 @@ func TestReferencesInDifferentRepo(t *testing.T) { require.NoError(t, err) indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName) - err = mirror.PushIndexToRegistry(attIdx.Index, indexName) + err = oci.PushIndexToRegistry(attIdx.Index, indexName) require.NoError(t, err) signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts) @@ -227,7 +228,7 @@ func TestReferencesInDifferentRepo(t *testing.T) { // push references using subject-digest.att convention image, err := signedManifest.BuildAttestationImage() require.NoError(t, err) - err = mirror.PushImageToRegistry(image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerURL.Host, repoName)) + err = oci.PushImageToRegistry(image, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerURL.Host, repoName)) require.NoError(t, err) refServer := tc.refServer @@ -242,7 +243,7 @@ func TestReferencesInDifferentRepo(t *testing.T) { require.NoError(t, err) indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName) - err = mirror.PushIndexToRegistry(attIdx.Index, indexName) + err = oci.PushIndexToRegistry(attIdx.Index, indexName) require.NoError(t, err) signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts) @@ -254,7 +255,7 @@ func TestReferencesInDifferentRepo(t *testing.T) { imgs, err := mf.BuildReferringArtifacts() require.NoError(t, err) for _, img := range imgs { - err = mirror.PushImageToRegistry(img, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerURL.Host, repoName)) + err = oci.PushImageToRegistry(img, fmt.Sprintf("%s/%s:tag-does-not-matter", refServerURL.Host, repoName)) require.NoError(t, err) } } @@ -297,7 +298,7 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) { require.NoError(t, err) indexName := fmt.Sprintf("%s/%s:latest", serverURL.Host, repoName) - err = mirror.PushIndexToRegistry(attIdx.Index, indexName) + err = oci.PushIndexToRegistry(attIdx.Index, indexName) require.NoError(t, err) signedManifests, err := attest.SignStatements(ctx, attIdx.Index, signer, opts) @@ -308,7 +309,7 @@ func TestCorrectArtifactTypeInTagFallback(t *testing.T) { imgs, err := mf.BuildReferringArtifacts() require.NoError(t, err) for _, img := range imgs { - err = mirror.PushImageToRegistry(img, fmt.Sprintf("%s/%s:tag-does-not-matter", serverURL.Host, repoName)) + err = oci.PushImageToRegistry(img, fmt.Sprintf("%s/%s:tag-does-not-matter", serverURL.Host, repoName)) require.NoError(t, err) mf, err := img.Manifest() require.NoError(t, err) @@ -326,3 +327,14 @@ 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) +} diff --git a/pkg/mirror/authn_test.go b/pkg/mirror/authn_test.go index fd5476b..03e6c30 100644 --- a/pkg/mirror/authn_test.go +++ b/pkg/mirror/authn_test.go @@ -6,7 +6,6 @@ import ( "path/filepath" "testing" - "github.com/docker/attest/pkg/mirror" "github.com/docker/attest/pkg/oci" "github.com/stretchr/testify/require" ) @@ -25,7 +24,7 @@ func TestRegistryAuth(t *testing.T) { } for _, tc := range testCases { t.Run(tc.Image, func(t *testing.T) { - err := mirror.PushIndexToRegistry(attIdx.Index, tc.Image) + err := oci.PushIndexToRegistry(attIdx.Index, tc.Image) require.NoError(t, err) _, err = oci.IndexFromRemote(tc.Image) require.NoError(t, err) diff --git a/pkg/mirror/example_mirror_test.go b/pkg/mirror/example_mirror_test.go index 534b9fd..37003a2 100644 --- a/pkg/mirror/example_mirror_test.go +++ b/pkg/mirror/example_mirror_test.go @@ -8,6 +8,7 @@ import ( "github.com/docker/attest/internal/embed" "github.com/docker/attest/pkg/mirror" + "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/tuf" v1 "github.com/google/go-containerregistry/pkg/v1" ) @@ -80,7 +81,7 @@ func ExampleNewTUFMirror() { func mirrorToRegistry(o *TufMirrorOutput) error { // push metadata to registry metadataRepo := "registry-1.docker.io/docker/tuf-metadata:latest" - err := mirror.PushImageToRegistry(o.metadata, metadataRepo) + err := oci.PushImageToRegistry(o.metadata, metadataRepo) if err != nil { return err } @@ -91,7 +92,7 @@ func mirrorToRegistry(o *TufMirrorOutput) error { return fmt.Errorf("failed to get repo without tag: %s", metadataRepo) } imageName := fmt.Sprintf("%s:%s", repo, metadata.Tag) - err = mirror.PushImageToRegistry(metadata.Image, imageName) + err = oci.PushImageToRegistry(metadata.Image, imageName) if err != nil { return err } @@ -101,7 +102,7 @@ func mirrorToRegistry(o *TufMirrorOutput) error { targetsRepo := "registry-1.docker.io/docker/tuf-targets" for _, target := range o.targets { imageName := fmt.Sprintf("%s:%s", targetsRepo, target.Tag) - err = mirror.PushImageToRegistry(target.Image, imageName) + err = oci.PushImageToRegistry(target.Image, imageName) if err != nil { return err } @@ -109,7 +110,7 @@ func mirrorToRegistry(o *TufMirrorOutput) error { // push delegated targets to registry for _, target := range o.delegatedTargets { imageName := fmt.Sprintf("%s:%s", targetsRepo, target.Tag) - err = mirror.PushIndexToRegistry(target.Index, imageName) + err = oci.PushIndexToRegistry(target.Index, imageName) if err != nil { return err } @@ -119,14 +120,14 @@ func mirrorToRegistry(o *TufMirrorOutput) error { func mirrorToLocal(o *TufMirrorOutput, outputPath string) error { // output metadata to local directory - err := mirror.SaveImageAsOCILayout(o.metadata, outputPath) + err := oci.SaveImageAsOCILayout(o.metadata, outputPath) if err != nil { return err } // output delegated metadata to local directory for _, metadata := range o.delegatedMetadata { path := filepath.Join(outputPath, metadata.Tag) - err = mirror.SaveImageAsOCILayout(metadata.Image, path) + err = oci.SaveImageAsOCILayout(metadata.Image, path) if err != nil { return err } @@ -135,7 +136,7 @@ func mirrorToLocal(o *TufMirrorOutput, outputPath string) error { // output top-level targets to local directory for _, target := range o.targets { path := filepath.Join(outputPath, target.Tag) - err = mirror.SaveImageAsOCILayout(target.Image, path) + err = oci.SaveImageAsOCILayout(target.Image, path) if err != nil { return err } @@ -143,7 +144,7 @@ func mirrorToLocal(o *TufMirrorOutput, outputPath string) error { // output delegated targets to local directory for _, target := range o.delegatedTargets { path := filepath.Join(outputPath, target.Tag) - err = mirror.SaveIndexAsOCILayout(target.Index, path) + err = oci.SaveIndexAsOCILayout(target.Index, path) if err != nil { return err } diff --git a/pkg/mirror/mirror.go b/pkg/mirror/mirror.go index e9e35de..bf376bf 100644 --- a/pkg/mirror/mirror.go +++ b/pkg/mirror/mirror.go @@ -2,18 +2,9 @@ package mirror import ( "fmt" - "os" "github.com/docker/attest/internal/embed" - "github.com/docker/attest/pkg/attestation" - "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/tuf" - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/empty" - "github.com/google/go-containerregistry/pkg/v1/layout" - "github.com/google/go-containerregistry/pkg/v1/mutate" - "github.com/google/go-containerregistry/pkg/v1/remote" ) func NewTUFMirror(root []byte, tufPath, metadataURL, targetsURL string, versionChecker tuf.VersionChecker) (*TUFMirror, error) { @@ -26,133 +17,3 @@ func NewTUFMirror(root []byte, tufPath, metadataURL, targetsURL string, versionC } return &TUFMirror{TUFClient: tufClient, tufPath: tufPath, metadataURL: metadataURL, targetsURL: targetsURL}, nil } - -func PushImageToRegistry(image v1.Image, imageName string) error { - ref, err := name.ParseReference(imageName) - if err != nil { - return fmt.Errorf("Failed to parse image name '%s': %w", imageName, err) - } - - // Push the image to the registry - return remote.Write(ref, image, oci.MultiKeychainOption()) -} - -func PushIndexToRegistry(index v1.ImageIndex, imageName string) error { - // Parse the index name - ref, err := name.ParseReference(imageName) - if err != nil { - return fmt.Errorf("Failed to parse image name: %w", err) - } - - // Push the index to the registry - return remote.WriteIndex(ref, index, oci.MultiKeychainOption()) -} - -func SaveImageAsOCILayout(image v1.Image, path string) error { - // Save the image to the local filesystem - err := os.MkdirAll(path, os.ModePerm) - if err != nil { - return fmt.Errorf("failed to create directory: %w", err) - } - index := empty.Index - l, err := layout.Write(path, index) - if err != nil { - return fmt.Errorf("failed to create index: %w", err) - } - return l.AppendImage(image) -} - -func SaveIndexAsOCILayout(image v1.ImageIndex, path string) error { - // Save the index to the local filesystem - err := os.MkdirAll(path, os.ModePerm) - if err != nil { - return fmt.Errorf("failed to create directory: %w", err) - } - - _, err = layout.Write(path, image) - if err != nil { - return fmt.Errorf("failed to create index: %w", err) - } - return nil -} - -func SaveIndex(outputs []*oci.ImageSpec, index v1.ImageIndex, indexName string) error { - // split output by comma and write or push each one - for _, output := range outputs { - if output.Type == oci.OCI { - idx := v1.ImageIndex(empty.Index) - idx = mutate.AppendManifests(idx, mutate.IndexAddendum{ - Add: index, - Descriptor: v1.Descriptor{ - Annotations: map[string]string{ - oci.OCIReferenceTarget: indexName, - }, - }, - }) - err := SaveIndexAsOCILayout(idx, output.Identifier) - if err != nil { - return fmt.Errorf("failed to write signed image: %w", err) - } - } else { - err := PushIndexToRegistry(index, output.Identifier) - if err != nil { - return fmt.Errorf("failed to push signed image: %w", err) - } - } - } - return nil -} - -func SaveImage(output *oci.ImageSpec, image v1.Image, imageName string) error { - if output.Type == oci.OCI { - idx := v1.ImageIndex(empty.Index) - idx = mutate.AppendManifests(idx, mutate.IndexAddendum{ - Add: image, - Descriptor: v1.Descriptor{ - Annotations: map[string]string{ - oci.OCIReferenceTarget: imageName, - }, - }, - }) - err := SaveIndexAsOCILayout(idx, output.Identifier) - if err != nil { - return fmt.Errorf("failed to write signed image: %w", err) - } - } else { - err := PushImageToRegistry(image, output.Identifier) - if err != nil { - return fmt.Errorf("failed to push signed image: %w", err) - } - } - return nil -} - -func SaveReferrers(manifest *attestation.Manifest, outputs []*oci.ImageSpec) error { - for _, output := range outputs { - // OCI layout output for referrers not supported - if output.Type == oci.OCI { - continue - } - // so that we use the same tag each time to reduce number of tags (tags aren't needed for referrers but we must push one) - attOut, err := oci.ReplaceTagInSpec(output, manifest.SubjectDescriptor.Digest) - if err != nil { - return err - } - // otherwise we end up with the detected platform, though I'm not sure it matters - attOut.Platform = &v1.Platform{ - OS: "unknown", - Architecture: "unknown", - } - images, err := manifest.BuildReferringArtifacts() - if err != nil { - return fmt.Errorf("failed to build image: %w", err) - } - for _, image := range images { - err := PushImageToRegistry(image, attOut.Identifier) - if err != nil { - return fmt.Errorf("failed to push image: %w", err) - } - } - } - return nil -} diff --git a/pkg/oci/layout_test.go b/pkg/oci/layout_test.go index 12feb93..870b091 100644 --- a/pkg/oci/layout_test.go +++ b/pkg/oci/layout_test.go @@ -7,7 +7,6 @@ import ( "github.com/docker/attest/internal/test" "github.com/docker/attest/pkg/attest" "github.com/docker/attest/pkg/attestation" - "github.com/docker/attest/pkg/mirror" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/policy" v1 "github.com/google/go-containerregistry/pkg/v1" @@ -34,7 +33,7 @@ func TestAttestationFromOCILayout(t *testing.T) { require.NoError(t, err) spec, err := oci.ParseImageSpec(oci.LocalPrefix + outputLayout) require.NoError(t, err) - err = mirror.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, outputLayout) + err = oci.SaveIndex([]*oci.ImageSpec{spec}, signedIndex, outputLayout) require.NoError(t, err) testCases := []struct { diff --git a/pkg/oci/oci.go b/pkg/oci/oci.go index f362224..f3a5961 100644 --- a/pkg/oci/oci.go +++ b/pkg/oci/oci.go @@ -172,3 +172,26 @@ func replaceTag(image string, digest v1.Hash) (string, error) { } return fmt.Sprintf("%s:%s-%s.att", notag, digest.Algorithm, digest.Hex), nil } + +func ReplaceDigestInSpec(src *ImageSpec, digest v1.Hash) (*ImageSpec, error) { + newName, err := replaceDigest(src.Identifier, digest) + if err != nil { + return nil, fmt.Errorf("failed to parse repo name: %w", err) + } + return &ImageSpec{ + Identifier: newName, + Type: src.Type, + Platform: src.Platform, + }, nil +} + +func replaceDigest(image string, digest v1.Hash) (string, error) { + if strings.HasPrefix(image, LocalPrefix) { + return image, nil + } + notag, err := WithoutTag(image) + if err != nil { + return "", nil + } + return fmt.Sprintf("%s@%s:%s", notag, digest.Algorithm, digest.Hex), nil +} diff --git a/pkg/oci/oci_test.go b/pkg/oci/oci_test.go index 3039aac..d28574a 100644 --- a/pkg/oci/oci_test.go +++ b/pkg/oci/oci_test.go @@ -10,6 +10,8 @@ import ( "github.com/stretchr/testify/require" ) +var UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image") + func TestRefToPurl(t *testing.T) { arm, err := ParsePlatform("arm64/linux") require.NoError(t, err) @@ -54,8 +56,6 @@ func TestRefToPurl(t *testing.T) { assert.True(t, canonical) } -var UnsignedTestImage = filepath.Join("..", "..", "test", "testdata", "unsigned-test-image") - // Test fix for https://github.com/docker/secure-artifacts-team-issues/issues/202 func TestImageDigestForPlatform(t *testing.T) { idx, err := layout.ImageIndexFromPath(UnsignedTestImage) diff --git a/pkg/oci/output.go b/pkg/oci/output.go new file mode 100644 index 0000000..af358c1 --- /dev/null +++ b/pkg/oci/output.go @@ -0,0 +1,142 @@ +package oci + +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" + "github.com/google/go-containerregistry/pkg/v1/layout" + "github.com/google/go-containerregistry/pkg/v1/mutate" + "github.com/google/go-containerregistry/pkg/v1/remote" +) + +func PushImageToRegistry(image v1.Image, imageName string) error { + ref, err := name.ParseReference(imageName) + if err != nil { + return fmt.Errorf("Failed to parse image name '%s': %w", imageName, err) + } + + // Push the image to the registry + return remote.Write(ref, image, MultiKeychainOption()) +} + +func PushIndexToRegistry(index v1.ImageIndex, imageName string) error { + // Parse the index name + ref, err := name.ParseReference(imageName) + if err != nil { + return fmt.Errorf("Failed to parse image name: %w", err) + } + + // Push the index to the registry + return remote.WriteIndex(ref, index, MultiKeychainOption()) +} + +func SaveImageAsOCILayout(image v1.Image, path string) error { + // Save the image to the local filesystem + err := os.MkdirAll(path, os.ModePerm) + if err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + index := empty.Index + l, err := layout.Write(path, index) + if err != nil { + return fmt.Errorf("failed to create index: %w", err) + } + return l.AppendImage(image) +} + +func SaveIndexAsOCILayout(image v1.ImageIndex, path string) error { + // Save the index to the local filesystem + err := os.MkdirAll(path, os.ModePerm) + if err != nil { + return fmt.Errorf("failed to create directory: %w", err) + } + + _, err = layout.Write(path, image) + if err != nil { + return fmt.Errorf("failed to create index: %w", err) + } + return nil +} + +func SaveIndex(outputs []*ImageSpec, index v1.ImageIndex, indexName string) error { + // split output by comma and write or push each one + for _, output := range outputs { + if output.Type == OCI { + idx := v1.ImageIndex(empty.Index) + idx = mutate.AppendManifests(idx, mutate.IndexAddendum{ + Add: index, + Descriptor: v1.Descriptor{ + Annotations: map[string]string{ + OCIReferenceTarget: indexName, + }, + }, + }) + err := SaveIndexAsOCILayout(idx, output.Identifier) + if err != nil { + return fmt.Errorf("failed to write signed image: %w", err) + } + } else { + err := PushIndexToRegistry(index, output.Identifier) + if err != nil { + return fmt.Errorf("failed to push signed image: %w", err) + } + } + } + return nil +} + +func SaveImage(output *ImageSpec, image v1.Image, imageName string) error { + if output.Type == OCI { + idx := v1.ImageIndex(empty.Index) + idx = mutate.AppendManifests(idx, mutate.IndexAddendum{ + Add: image, + Descriptor: v1.Descriptor{ + Annotations: map[string]string{ + OCIReferenceTarget: imageName, + }, + }, + }) + err := SaveIndexAsOCILayout(idx, output.Identifier) + if err != nil { + return fmt.Errorf("failed to write signed image: %w", err) + } + } else { + err := PushImageToRegistry(image, output.Identifier) + if err != nil { + return fmt.Errorf("failed to push signed image: %w", err) + } + } + return nil +} + +func SaveReferrers(manifest *attestation.Manifest, outputs []*ImageSpec) error { + for _, output := range outputs { + // OCI layout output for referrers 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) + } + attOut, err := ReplaceDigestInSpec(output, digest) + if err != nil { + return fmt.Errorf("failed to create attestation image spec: %w", err) + } + err = PushImageToRegistry(image, attOut.Identifier) + if err != nil { + return fmt.Errorf("failed to push image: %w", err) + } + } + } + return nil +} diff --git a/pkg/mirror/mirror_test.go b/pkg/oci/output_test.go similarity index 91% rename from pkg/mirror/mirror_test.go rename to pkg/oci/output_test.go index 7a42d80..fc30a65 100644 --- a/pkg/mirror/mirror_test.go +++ b/pkg/oci/output_test.go @@ -1,4 +1,4 @@ -package mirror +package oci_test import ( "fmt" @@ -33,12 +33,12 @@ func TestSavingIndex(t *testing.T) { indexName := fmt.Sprintf("%s/repo:root", u.Host) output, err := oci.ParseImageSpecs(indexName) require.NoError(t, err) - err = SaveIndex(output, attIdx.Index, indexName) + err = oci.SaveIndex(output, attIdx.Index, indexName) require.NoError(t, err) ociOutput, err := oci.ParseImageSpecs(oci.LocalPrefix + outputLayout) require.NoError(t, err) - err = SaveIndex(ociOutput, attIdx.Index, indexName) + err = oci.SaveIndex(ociOutput, attIdx.Index, indexName) require.NoError(t, err) } @@ -56,12 +56,12 @@ func TestSavingImage(t *testing.T) { indexName := fmt.Sprintf("%s/repo:root", u.Host) output, err := oci.ParseImageSpec(indexName) require.NoError(t, err) - err = SaveImage(output, img, indexName) + err = oci.SaveImage(output, img, indexName) require.NoError(t, err) ociOutput, err := oci.ParseImageSpec(oci.LocalPrefix + outputLayout) require.NoError(t, err) - err = SaveImage(ociOutput, img, indexName) + err = oci.SaveImage(ociOutput, img, indexName) require.NoError(t, err) } @@ -93,7 +93,7 @@ func TestSavingReferrers(t *testing.T) { indexName := fmt.Sprintf("%s/repo:root", u.Host) output, err := oci.ParseImageSpecs(indexName) require.NoError(t, err) - err = SaveReferrers(manifest, output) + err = oci.SaveReferrers(manifest, output) require.NoError(t, err) reg := &oci.MockRegistryResolver{ diff --git a/pkg/oci/registry_test.go b/pkg/oci/registry_test.go index d6b1edf..4de65bc 100644 --- a/pkg/oci/registry_test.go +++ b/pkg/oci/registry_test.go @@ -10,7 +10,6 @@ import ( "github.com/docker/attest/internal/test" "github.com/docker/attest/pkg/attest" "github.com/docker/attest/pkg/attestation" - "github.com/docker/attest/pkg/mirror" "github.com/docker/attest/pkg/oci" "github.com/docker/attest/pkg/policy" "github.com/google/go-containerregistry/pkg/registry" @@ -36,7 +35,7 @@ func TestRegistry(t *testing.T) { indexName := fmt.Sprintf("%s/repo:root", u.Host) require.NoError(t, err) - err = mirror.PushIndexToRegistry(signedIndex, indexName) + err = oci.PushIndexToRegistry(signedIndex, indexName) require.NoError(t, err) spec, err := oci.ParseImageSpec(indexName)