Skip to content

Commit bd366f8

Browse files
authored
Merge pull request moby#3918 from jedevc/purl-oci
Ensure Provenance Material URIs for OCI sources are configured to use `pkg:oci` PURLs
2 parents 60d134b + 9bc425a commit bd366f8

File tree

9 files changed

+176
-60
lines changed

9 files changed

+176
-60
lines changed

client/client_test.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"io"
1616
"net"
1717
"net/http"
18+
"net/url"
1819
"os"
1920
"path"
2021
"path/filepath"
@@ -35,6 +36,7 @@ import (
3536
"github.com/containerd/containerd/remotes/docker"
3637
"github.com/containerd/containerd/snapshots"
3738
"github.com/containerd/continuity/fs/fstest"
39+
"github.com/docker/distribution/reference"
3840
intoto "github.com/in-toto/in-toto-golang/in_toto"
3941
controlapi "github.com/moby/buildkit/api/services/control"
4042
"github.com/moby/buildkit/client/llb"
@@ -53,7 +55,6 @@ import (
5355
"github.com/moby/buildkit/util/attestation"
5456
"github.com/moby/buildkit/util/contentutil"
5557
"github.com/moby/buildkit/util/entitlements"
56-
"github.com/moby/buildkit/util/purl"
5758
"github.com/moby/buildkit/util/testutil"
5859
"github.com/moby/buildkit/util/testutil/echoserver"
5960
"github.com/moby/buildkit/util/testutil/httpserver"
@@ -7564,7 +7565,14 @@ func testExportAttestations(t *testing.T, sb integration.Sandbox) {
75647565

75657566
purls := map[string]string{}
75667567
for _, k := range targets {
7567-
p, _ := purl.RefToPURL(k, &ps[i])
7568+
named, err := reference.ParseNormalizedNamed(k)
7569+
require.NoError(t, err)
7570+
name := reference.FamiliarName(named)
7571+
version := ""
7572+
if tagged, ok := named.(reference.Tagged); ok {
7573+
version = tagged.Tag()
7574+
}
7575+
p := fmt.Sprintf("pkg:docker/%s%s@%s?platform=%s", url.QueryEscape(registry), strings.TrimPrefix(name, registry), version, url.PathEscape(platforms.Format(ps[i])))
75687576
purls[k] = p
75697577
}
75707578

@@ -7852,8 +7860,7 @@ func testAttestationDefaultSubject(t *testing.T, sb integration.Sandbox) {
78527860
require.Equal(t, "https://example.com/attestations/v1.0", attest.PredicateType)
78537861
require.Equal(t, map[string]interface{}{"success": true}, attest.Predicate)
78547862

7855-
name, _ := purl.RefToPURL(target, &ps[0])
7856-
7863+
name := fmt.Sprintf("pkg:docker/%s/buildkit/testattestationsemptysubject@latest?platform=%s", url.QueryEscape(registry), url.QueryEscape(platforms.Format(ps[i])))
78577864
subjects := []intoto.Subject{{
78587865
Name: name,
78597866
Digest: map[string]string{
@@ -8004,7 +8011,7 @@ func testAttestationBundle(t *testing.T, sb integration.Sandbox) {
80048011

80058012
require.Equal(t, "https://example.com/attestations/v1.0", attest.PredicateType)
80068013
require.Equal(t, map[string]interface{}{"foo": "1"}, attest.Predicate)
8007-
name, _ := purl.RefToPURL(target, &ps[i])
8014+
name := fmt.Sprintf("pkg:docker/%s/buildkit/testattestationsbundle@latest?platform=%s", url.QueryEscape(registry), url.QueryEscape(platforms.Format(ps[i])))
80088015
subjects := []intoto.Subject{{
80098016
Name: name,
80108017
Digest: map[string]string{

exporter/containerimage/writer.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import (
3434
digest "github.com/opencontainers/go-digest"
3535
specs "github.com/opencontainers/image-spec/specs-go"
3636
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
37+
"github.com/package-url/packageurl-go"
3738
"github.com/pkg/errors"
3839
"go.opentelemetry.io/otel/attribute"
3940
"go.opentelemetry.io/otel/trace"
@@ -235,7 +236,7 @@ func (ic *ImageWriter) Commit(ctx context.Context, inp *exporter.Source, session
235236
if name == "" {
236237
continue
237238
}
238-
pl, err := purl.RefToPURL(name, &p.Platform)
239+
pl, err := purl.RefToPURL(packageurl.TypeDocker, name, &p.Platform)
239240
if err != nil {
240241
return nil, err
241242
}

frontend/dockerfile/dockerfile_provenance_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,18 +3,23 @@ package dockerfile
33
import (
44
"context"
55
"encoding/json"
6+
"fmt"
67
"net/http"
78
"net/http/httptest"
89
"net/url"
10+
"os"
911
"os/exec"
1012
"path/filepath"
1113
"strings"
1214
"testing"
1315
"time"
1416

17+
"github.com/containerd/containerd/content"
18+
"github.com/containerd/containerd/content/local"
1519
"github.com/containerd/containerd/platforms"
1620
"github.com/containerd/continuity/fs/fstest"
1721
intoto "github.com/in-toto/in-toto-golang/in_toto"
22+
provenanceCommon "github.com/in-toto/in-toto-golang/in_toto/slsa_provenance/common"
1823
"github.com/moby/buildkit/client"
1924
"github.com/moby/buildkit/client/llb"
2025
"github.com/moby/buildkit/exporter/containerimage/exptypes"
@@ -871,6 +876,141 @@ RUN --mount=type=secret,id=mysecret --mount=type=secret,id=othersecret --mount=t
871876
require.True(t, pred.Invocation.Parameters.SSH[0].Optional)
872877
}
873878

879+
func testOCILayoutProvenance(t *testing.T, sb integration.Sandbox) {
880+
integration.CheckFeatureCompat(t, sb, integration.FeatureProvenance)
881+
ctx := sb.Context()
882+
883+
c, err := client.New(ctx, sb.Address())
884+
require.NoError(t, err)
885+
defer c.Close()
886+
887+
registry, err := sb.NewRegistry()
888+
if errors.Is(err, integration.ErrRequirements) {
889+
t.Skip(err.Error())
890+
}
891+
require.NoError(t, err)
892+
target := registry + "/buildkit/clientprovenance:ocilayout"
893+
894+
f := getFrontend(t, sb)
895+
_, isGateway := f.(*gatewayFrontend)
896+
897+
ocidir := t.TempDir()
898+
ociDockerfile := []byte(`
899+
FROM scratch
900+
COPY <<EOF /foo
901+
foo
902+
EOF
903+
`)
904+
dir, err := integration.Tmpdir(
905+
t,
906+
fstest.CreateFile("Dockerfile", ociDockerfile, 0600),
907+
)
908+
require.NoError(t, err)
909+
910+
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
911+
LocalDirs: map[string]string{
912+
dockerui.DefaultLocalNameDockerfile: dir,
913+
dockerui.DefaultLocalNameContext: dir,
914+
},
915+
Exports: []client.ExportEntry{
916+
{
917+
Type: client.ExporterOCI,
918+
OutputDir: ocidir,
919+
Attrs: map[string]string{
920+
"tar": "false",
921+
},
922+
},
923+
},
924+
}, nil)
925+
require.NoError(t, err)
926+
927+
var index ocispecs.Index
928+
dt, err := os.ReadFile(filepath.Join(ocidir, "index.json"))
929+
require.NoError(t, err)
930+
err = json.Unmarshal(dt, &index)
931+
require.NoError(t, err)
932+
require.Equal(t, 1, len(index.Manifests))
933+
digest := index.Manifests[0].Digest.Hex()
934+
935+
store, err := local.NewStore(ocidir)
936+
require.NoError(t, err)
937+
ociID := "ocione"
938+
939+
dockerfile := []byte(`
940+
FROM foo
941+
COPY <<EOF /bar
942+
bar
943+
EOF
944+
`)
945+
dir, err = integration.Tmpdir(
946+
t,
947+
fstest.CreateFile("Dockerfile", dockerfile, 0600),
948+
)
949+
require.NoError(t, err)
950+
951+
_, err = f.Solve(sb.Context(), c, client.SolveOpt{
952+
LocalDirs: map[string]string{
953+
dockerui.DefaultLocalNameDockerfile: dir,
954+
dockerui.DefaultLocalNameContext: dir,
955+
},
956+
FrontendAttrs: map[string]string{
957+
"context:foo": fmt.Sprintf("oci-layout:%s@sha256:%s", ociID, digest),
958+
"attest:provenance": "mode=max",
959+
},
960+
OCIStores: map[string]content.Store{
961+
ociID: store,
962+
},
963+
Exports: []client.ExportEntry{
964+
{
965+
Type: client.ExporterImage,
966+
Attrs: map[string]string{
967+
"name": target,
968+
"push": "true",
969+
},
970+
},
971+
},
972+
}, nil)
973+
require.NoError(t, err)
974+
975+
desc, provider, err := contentutil.ProviderFromRef(target)
976+
require.NoError(t, err)
977+
imgs, err := testutil.ReadImages(sb.Context(), provider, desc)
978+
require.NoError(t, err)
979+
require.Equal(t, 2, len(imgs.Images))
980+
981+
expPlatform := platforms.Format(platforms.Normalize(platforms.DefaultSpec()))
982+
983+
img := imgs.Find(expPlatform)
984+
require.NotNil(t, img)
985+
require.Equal(t, []byte("foo\n"), img.Layers[0]["foo"].Data)
986+
require.Equal(t, []byte("bar\n"), img.Layers[1]["bar"].Data)
987+
988+
att := imgs.FindAttestation(expPlatform)
989+
type stmtT struct {
990+
Predicate provenance.ProvenancePredicate `json:"predicate"`
991+
}
992+
var stmt stmtT
993+
require.NoError(t, json.Unmarshal(att.LayersRaw[0], &stmt))
994+
pred := stmt.Predicate
995+
996+
if isGateway {
997+
require.Len(t, pred.Materials, 2)
998+
} else {
999+
require.Len(t, pred.Materials, 1)
1000+
}
1001+
var material *provenanceCommon.ProvenanceMaterial
1002+
for _, m := range pred.Materials {
1003+
if strings.Contains(m.URI, "/foo") {
1004+
require.Nil(t, material, pred.Materials)
1005+
material = &m
1006+
}
1007+
}
1008+
require.NotNil(t, material)
1009+
prefix, _, _ := strings.Cut(material.URI, "/")
1010+
require.Equal(t, "pkg:oci", prefix)
1011+
require.Equal(t, digest, material.Digest["sha256"])
1012+
}
1013+
8741014
func testNilProvenance(t *testing.T, sb integration.Sandbox) {
8751015
integration.CheckFeatureCompat(t, sb, integration.FeatureProvenance)
8761016
ctx := sb.Context()

frontend/dockerfile/dockerfile_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ var allTests = integration.TestFuncs(
161161
testClientFrontendProvenance,
162162
testClientLLBProvenance,
163163
testSecretSSHProvenance,
164+
testOCILayoutProvenance,
164165
testNilProvenance,
165166
testSBOMScannerArgs,
166167
testMultiPlatformWarnings,

solver/llbsolver/provenance.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ func (b *provenanceBridge) ResolveImageConfig(ctx context.Context, ref string, o
140140
Ref: ref,
141141
Platform: opt.Platform,
142142
Digest: dgst,
143+
Local: opt.ResolverType == llb.ResolverTypeOCILayout,
143144
})
144145
return dgst, config, nil
145146
}
@@ -322,10 +323,11 @@ func captureProvenance(ctx context.Context, res solver.CachedResultWithProvenanc
322323
if err != nil {
323324
return errors.Wrapf(err, "failed to parse OCI digest %s", pin)
324325
}
325-
c.AddLocalImage(provenance.ImageSource{
326+
c.AddImage(provenance.ImageSource{
326327
Ref: s.Reference.String(),
327328
Platform: s.Platform,
328329
Digest: dgst,
330+
Local: true,
329331
})
330332
default:
331333
return errors.Errorf("unknown source identifier %T", id)

solver/llbsolver/provenance/capture.go

Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type ImageSource struct {
1616
Ref string
1717
Platform *ocispecs.Platform
1818
Digest digest.Digest
19+
Local bool
1920
}
2021

2122
type GitSource struct {
@@ -43,11 +44,10 @@ type SSH struct {
4344
}
4445

4546
type Sources struct {
46-
Images []ImageSource
47-
LocalImages []ImageSource
48-
Git []GitSource
49-
HTTP []HTTPSource
50-
Local []LocalSource
47+
Images []ImageSource
48+
Git []GitSource
49+
HTTP []HTTPSource
50+
Local []LocalSource
5151
}
5252

5353
type Capture struct {
@@ -67,9 +67,6 @@ func (c *Capture) Merge(c2 *Capture) error {
6767
for _, i := range c2.Sources.Images {
6868
c.AddImage(i)
6969
}
70-
for _, i := range c2.Sources.LocalImages {
71-
c.AddLocalImage(i)
72-
}
7370
for _, l := range c2.Sources.Local {
7471
c.AddLocal(l)
7572
}
@@ -98,9 +95,6 @@ func (c *Capture) Sort() {
9895
sort.Slice(c.Sources.Images, func(i, j int) bool {
9996
return c.Sources.Images[i].Ref < c.Sources.Images[j].Ref
10097
})
101-
sort.Slice(c.Sources.LocalImages, func(i, j int) bool {
102-
return c.Sources.LocalImages[i].Ref < c.Sources.LocalImages[j].Ref
103-
})
10498
sort.Slice(c.Sources.Local, func(i, j int) bool {
10599
return c.Sources.Local[i].Name < c.Sources.Local[j].Name
106100
})
@@ -151,7 +145,7 @@ func (c *Capture) OptimizeImageSources() error {
151145

152146
func (c *Capture) AddImage(i ImageSource) {
153147
for _, v := range c.Sources.Images {
154-
if v.Ref == i.Ref {
148+
if v.Ref == i.Ref && v.Local == i.Local {
155149
if v.Platform == i.Platform {
156150
return
157151
}
@@ -165,22 +159,6 @@ func (c *Capture) AddImage(i ImageSource) {
165159
c.Sources.Images = append(c.Sources.Images, i)
166160
}
167161

168-
func (c *Capture) AddLocalImage(i ImageSource) {
169-
for _, v := range c.Sources.LocalImages {
170-
if v.Ref == i.Ref {
171-
if v.Platform == i.Platform {
172-
return
173-
}
174-
if v.Platform != nil && i.Platform != nil {
175-
if v.Platform.Architecture == i.Platform.Architecture && v.Platform.OS == i.Platform.OS && v.Platform.Variant == i.Platform.Variant {
176-
return
177-
}
178-
}
179-
}
180-
}
181-
c.Sources.LocalImages = append(c.Sources.LocalImages, i)
182-
}
183-
184162
func (c *Capture) AddLocal(l LocalSource) {
185163
for _, v := range c.Sources.Local {
186164
if v.Name == l.Name {

solver/llbsolver/provenance/predicate.go

Lines changed: 8 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -56,11 +56,17 @@ type BuildKitMetadata struct {
5656
}
5757

5858
func slsaMaterials(srcs Sources) ([]slsa.ProvenanceMaterial, error) {
59-
count := len(srcs.Images) + len(srcs.Git) + len(srcs.HTTP) + len(srcs.LocalImages)
59+
count := len(srcs.Images) + len(srcs.Git) + len(srcs.HTTP)
6060
out := make([]slsa.ProvenanceMaterial, 0, count)
6161

6262
for _, s := range srcs.Images {
63-
uri, err := purl.RefToPURL(s.Ref, s.Platform)
63+
var uri string
64+
var err error
65+
if s.Local {
66+
uri, err = purl.RefToPURL(packageurl.TypeOCI, s.Ref, s.Platform)
67+
} else {
68+
uri, err = purl.RefToPURL(packageurl.TypeDocker, s.Ref, s.Platform)
69+
}
6470
if err != nil {
6571
return nil, err
6672
}
@@ -93,26 +99,6 @@ func slsaMaterials(srcs Sources) ([]slsa.ProvenanceMaterial, error) {
9399
})
94100
}
95101

96-
for _, s := range srcs.LocalImages {
97-
q := []packageurl.Qualifier{}
98-
if s.Platform != nil {
99-
q = append(q, packageurl.Qualifier{
100-
Key: "platform",
101-
Value: platforms.Format(*s.Platform),
102-
})
103-
}
104-
packageurl.NewPackageURL(packageurl.TypeOCI, "", s.Ref, "", q, "")
105-
106-
material := slsa.ProvenanceMaterial{
107-
URI: s.Ref,
108-
}
109-
if s.Digest != "" {
110-
material.Digest = slsa.DigestSet{
111-
s.Digest.Algorithm().String(): s.Digest.Hex(),
112-
}
113-
}
114-
out = append(out, material)
115-
}
116102
return out, nil
117103
}
118104

0 commit comments

Comments
 (0)