From b3fe2874cb1eb87de4a1c19fda181fe6be02563c Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Wed, 15 Apr 2026 16:43:02 -0700 Subject: [PATCH] provenance: add dockerfile frontend version Record the builtin Dockerfile frontend version in provenance attestations for both SLSA v0.2 and v1 formats. The version is derived from a new builder.Version constant, normalized with the BuildKit release version suffix. The external frontend Dockerfile validates that the builtin version constant matches the git tag at release time. Signed-off-by: Tonis Tiigi --- frontend/dockerfile/builder/version.go | 3 ++ .../cmd/dockerfile-frontend/Dockerfile | 19 ++++++++ .../dockerfile/dockerfile_provenance_test.go | 36 +++++++++++++++ solver/llbsolver/dockerfile_version.go | 45 +++++++++++++++++++ solver/llbsolver/dockerfile_version_test.go | 37 +++++++++++++++ solver/llbsolver/provenance.go | 7 +++ solver/llbsolver/provenance/types/types.go | 10 +++-- .../llbsolver/provenance/types/types_test.go | 6 ++- 8 files changed, 159 insertions(+), 4 deletions(-) create mode 100644 frontend/dockerfile/builder/version.go create mode 100644 solver/llbsolver/dockerfile_version.go create mode 100644 solver/llbsolver/dockerfile_version_test.go diff --git a/frontend/dockerfile/builder/version.go b/frontend/dockerfile/builder/version.go new file mode 100644 index 000000000..8b3ff2b20 --- /dev/null +++ b/frontend/dockerfile/builder/version.go @@ -0,0 +1,3 @@ +package builder + +const Version = "1.24" diff --git a/frontend/dockerfile/cmd/dockerfile-frontend/Dockerfile b/frontend/dockerfile/cmd/dockerfile-frontend/Dockerfile index 0f7ef5bc4..352e57b0c 100644 --- a/frontend/dockerfile/cmd/dockerfile-frontend/Dockerfile +++ b/frontend/dockerfile/cmd/dockerfile-frontend/Dockerfile @@ -28,6 +28,25 @@ RUN --mount=target=. < /foo if isDockerd { expCustom = provenancetypes.ProvenanceCustomEnv{} } + expectedDockerfileVersionPrefix := dockerfilebuilder.Version + if strings.Count(expectedDockerfileVersionPrefix, ".") == 1 { + expectedDockerfileVersionPrefix += ".0" + } if slsaVersion == "" || slsaVersion == "v1" { type stmtT struct { @@ -277,6 +282,11 @@ RUN echo ok> /foo require.True(t, pred.RunDetails.Metadata.StartedOn.Before(*pred.RunDetails.Metadata.FinishedOn)) require.Equal(t, platforms.Format(platforms.Normalize(platforms.DefaultSpec())), pred.BuildDefinition.InternalParameters.BuilderPlatform) + if isClient || isGateway { + require.Empty(t, pred.BuildDefinition.InternalParameters.DockerfileVersion) + } else { + require.True(t, strings.HasPrefix(pred.BuildDefinition.InternalParameters.DockerfileVersion, expectedDockerfileVersionPrefix)) + } require.False(t, pred.RunDetails.Metadata.Completeness.ResolvedDependencies) require.False(t, pred.RunDetails.Metadata.Reproducible) @@ -346,6 +356,11 @@ RUN echo ok> /foo } require.Equal(t, "https://xxxxx:xxxxx@example.invalid/foo.html", args["context:foo"]) } + if isClient || isGateway { + require.Empty(t, pred.Invocation.Environment.DockerfileVersion) + } else { + require.True(t, strings.HasPrefix(pred.Invocation.Environment.DockerfileVersion, expectedDockerfileVersionPrefix)) + } expectedBaseImage := integration.UnixOrWindows("busybox", "nanoserver") escapedPlatform := url.PathEscape(platforms.Format(platforms.Normalize(platforms.DefaultSpec()))) @@ -516,6 +531,10 @@ COPY myapp.Dockerfile / _, isClient := f.(*clientFrontend) _, isGateway := f.(*gatewayFrontend) + expectedDockerfileVersionPrefix := dockerfilebuilder.Version + if strings.Count(expectedDockerfileVersionPrefix, ".") == 1 { + expectedDockerfileVersionPrefix += ".0" + } if slsaVersion == "" || slsaVersion == "v1" { require.Equal(t, "https://slsa.dev/provenance/v1", attest.PredicateType) // intentionally not const @@ -531,10 +550,16 @@ COPY myapp.Dockerfile / require.Empty(t, pred.BuildDefinition.ExternalParameters.Request.Frontend) require.Equal(t, "", pred.BuildDefinition.ExternalParameters.ConfigSource.URI) require.Equal(t, "", pred.BuildDefinition.ExternalParameters.ConfigSource.Path) + require.Empty(t, pred.BuildDefinition.InternalParameters.DockerfileVersion) } else { require.NotEmpty(t, pred.BuildDefinition.ExternalParameters.Request.Frontend) require.Equal(t, expectedURL+"/.git#v1:src", pred.BuildDefinition.ExternalParameters.ConfigSource.URI) require.Equal(t, "myapp.Dockerfile", pred.BuildDefinition.ExternalParameters.ConfigSource.Path) + if isGateway { + require.Empty(t, pred.BuildDefinition.InternalParameters.DockerfileVersion) + } else { + require.True(t, strings.HasPrefix(pred.BuildDefinition.InternalParameters.DockerfileVersion, expectedDockerfileVersionPrefix)) + } } expBase := "pkg:docker/busybox@latest?platform=" + url.PathEscape(platforms.Format(platforms.Normalize(platforms.DefaultSpec()))) @@ -586,10 +611,16 @@ COPY myapp.Dockerfile / require.Empty(t, pred.Invocation.Parameters.Frontend) require.Equal(t, "", pred.Invocation.ConfigSource.URI) require.Equal(t, "", pred.Invocation.ConfigSource.EntryPoint) + require.Empty(t, pred.Invocation.Environment.DockerfileVersion) } else { require.NotEmpty(t, pred.Invocation.Parameters.Frontend) require.Equal(t, expectedURL+"/.git#v1:src", pred.Invocation.ConfigSource.URI) require.Equal(t, "myapp.Dockerfile", pred.Invocation.ConfigSource.EntryPoint) + if isGateway { + require.Empty(t, pred.Invocation.Environment.DockerfileVersion) + } else { + require.True(t, strings.HasPrefix(pred.Invocation.Environment.DockerfileVersion, expectedDockerfileVersionPrefix)) + } } expBase := "pkg:docker/busybox@latest?platform=" + url.PathEscape(platforms.Format(platforms.Normalize(platforms.DefaultSpec()))) @@ -1040,6 +1071,11 @@ COPY --from=base C:\out C:\Files pred := stmt.Predicate require.Equal(t, "dockerfile.v0", pred.BuildDefinition.ExternalParameters.Request.Frontend) + expectedDockerfileVersionPrefix := dockerfilebuilder.Version + if strings.Count(expectedDockerfileVersionPrefix, ".") == 1 { + expectedDockerfileVersionPrefix += ".0" + } + require.True(t, strings.HasPrefix(pred.BuildDefinition.InternalParameters.DockerfileVersion, expectedDockerfileVersionPrefix)) require.NotContains(t, pred.BuildDefinition.ExternalParameters.Request.Args, "source") require.Equal(t, 2, len(pred.BuildDefinition.ExternalParameters.Request.Locals), "%+v", pred.BuildDefinition.ExternalParameters.Request.Locals) diff --git a/solver/llbsolver/dockerfile_version.go b/solver/llbsolver/dockerfile_version.go new file mode 100644 index 000000000..6427cfa4b --- /dev/null +++ b/solver/llbsolver/dockerfile_version.go @@ -0,0 +1,45 @@ +package llbsolver + +import ( + "strings" + + dockerfilebuilder "github.com/moby/buildkit/frontend/dockerfile/builder" + "github.com/moby/buildkit/version" + "github.com/pkg/errors" + "golang.org/x/mod/semver" +) + +func builtinDockerfileVersion() (string, error) { + return normalizeBuiltinDockerfileVersion(dockerfilebuilder.Version, version.Version) +} + +func normalizeBuiltinDockerfileVersion(v, buildkitVersion string) (string, error) { + v = strings.TrimSpace(v) + parts := strings.Split(v, ".") + if len(parts) != 2 && len(parts) != 3 { + return "", errors.Errorf("invalid dockerfile frontend version %q", v) + } + for _, part := range parts { + if part == "" { + return "", errors.Errorf("invalid dockerfile frontend version %q", v) + } + for _, ch := range part { + if ch < '0' || ch > '9' { + return "", errors.Errorf("invalid dockerfile frontend version %q", v) + } + } + } + if len(parts) == 2 { + v += ".0" + } + if !semver.IsValid(buildkitVersion) { + return v + "-dev", nil + } + if prerelease := semver.Prerelease(buildkitVersion); prerelease != "" { + return v + prerelease, nil + } + if semver.Build(buildkitVersion) != "" { + return v + "-dev", nil + } + return v, nil +} diff --git a/solver/llbsolver/dockerfile_version_test.go b/solver/llbsolver/dockerfile_version_test.go new file mode 100644 index 000000000..4fe096dbe --- /dev/null +++ b/solver/llbsolver/dockerfile_version_test.go @@ -0,0 +1,37 @@ +package llbsolver + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestBuiltinDockerfileVersion(t *testing.T) { + for _, tc := range []struct { + name string + dockerfileVer string + buildkitVersion string + want string + wantErr bool + }{ + {name: "stable short", dockerfileVer: "1.24", buildkitVersion: "v0.29.0", want: "1.24.0"}, + {name: "stable patch", dockerfileVer: "1.24", buildkitVersion: "v0.29.1", want: "1.24.0"}, + {name: "prerelease", dockerfileVer: "1.24", buildkitVersion: "v0.29.0-rc1", want: "1.24.0-rc1"}, + {name: "describe", dockerfileVer: "1.24", buildkitVersion: "v0.29.0-45-g95735c4ef", want: "1.24.0-45-g95735c4ef"}, + {name: "metadata", dockerfileVer: "1.24", buildkitVersion: "v0.29.0+unknown", want: "1.24.0-dev"}, + {name: "invalid buildkit", dockerfileVer: "1.24", buildkitVersion: "not-semver", want: "1.24.0-dev"}, + {name: "dockerfile with patch", dockerfileVer: "1.24.3", buildkitVersion: "v0.29.0", want: "1.24.3"}, + {name: "invalid dockerfile major only", dockerfileVer: "1", buildkitVersion: "v0.29.0", wantErr: true}, + {name: "invalid dockerfile suffix", dockerfileVer: "1.24.x", buildkitVersion: "v0.29.0", wantErr: true}, + } { + t.Run(tc.name, func(t *testing.T) { + got, err := normalizeBuiltinDockerfileVersion(tc.dockerfileVer, tc.buildkitVersion) + if tc.wantErr { + require.Error(t, err) + return + } + require.NoError(t, err) + require.Equal(t, tc.want, got) + }) + } +} diff --git a/solver/llbsolver/provenance.go b/solver/llbsolver/provenance.go index e18355988..892d7890c 100644 --- a/solver/llbsolver/provenance.go +++ b/solver/llbsolver/provenance.go @@ -465,6 +465,13 @@ func NewProvenanceCreator(ctx context.Context, slsaVersion provenancetypes.Prove } pr.BuildDefinition.InternalParameters.ProvenanceCustomEnv = customEnv + if pr.BuildDefinition.ExternalParameters.Request.Frontend == "dockerfile.v0" { + dockerfileVersion, err := builtinDockerfileVersion() + if err != nil { + return nil, err + } + pr.BuildDefinition.InternalParameters.DockerfileVersion = dockerfileVersion + } pc := &ProvenanceCreator{ pr: pr, diff --git a/solver/llbsolver/provenance/types/types.go b/solver/llbsolver/provenance/types/types.go index 7b1e5023d..009769210 100644 --- a/solver/llbsolver/provenance/types/types.go +++ b/solver/llbsolver/provenance/types/types.go @@ -171,8 +171,9 @@ type ProvenanceConfigSourceSLSA1 struct { } type ProvenanceInternalParametersSLSA1 struct { - BuildConfig *BuildConfig `json:"buildConfig,omitempty"` - BuilderPlatform string `json:"builderPlatform"` + BuildConfig *BuildConfig `json:"buildConfig,omitempty"` + BuilderPlatform string `json:"builderPlatform"` + DockerfileVersion string `json:"dockerfileVersion,omitempty"` ProvenanceCustomEnv } @@ -195,7 +196,8 @@ type Parameters struct { } type Environment struct { - Platform string `json:"platform"` + Platform string `json:"platform"` + DockerfileVersion string `json:"dockerfileVersion,omitempty"` ProvenanceCustomEnv } @@ -259,6 +261,7 @@ func (p *ProvenancePredicateSLSA1) ConvertToSLSA02() *ProvenancePredicateSLSA02 Parameters: p.BuildDefinition.ExternalParameters.Request, Environment: Environment{ Platform: p.BuildDefinition.InternalParameters.BuilderPlatform, + DockerfileVersion: p.BuildDefinition.InternalParameters.DockerfileVersion, ProvenanceCustomEnv: p.BuildDefinition.InternalParameters.ProvenanceCustomEnv, }, }, @@ -293,6 +296,7 @@ func (p *ProvenancePredicateSLSA02) ConvertToSLSA1() *ProvenancePredicateSLSA1 { InternalParameters: ProvenanceInternalParametersSLSA1{ BuildConfig: p.BuildConfig, BuilderPlatform: p.Invocation.Environment.Platform, + DockerfileVersion: p.Invocation.Environment.DockerfileVersion, ProvenanceCustomEnv: p.Invocation.Environment.ProvenanceCustomEnv, }, } diff --git a/solver/llbsolver/provenance/types/types_test.go b/solver/llbsolver/provenance/types/types_test.go index 5da999944..3618f451c 100644 --- a/solver/llbsolver/provenance/types/types_test.go +++ b/solver/llbsolver/provenance/types/types_test.go @@ -15,7 +15,8 @@ func TestMarsalBuildDefinitionSLSA1(t *testing.T) { "request": {} }, "internalParameters": { - "builderPlatform": "linux/amd64", + "builderPlatform": "linux/amd64", + "dockerfileVersion": "1.24.0-dev", "foo": "bar", "abc": 123, "def": {"one": 1} @@ -28,6 +29,7 @@ func TestMarsalBuildDefinitionSLSA1(t *testing.T) { require.Equal(t, "btype1", def.BuildType) require.Equal(t, "linux/amd64", def.InternalParameters.BuilderPlatform) + require.Equal(t, "1.24.0-dev", def.InternalParameters.DockerfileVersion) require.Equal(t, "bar", def.InternalParameters.ProvenanceCustomEnv["foo"]) require.InEpsilon(t, float64(123), def.InternalParameters.ProvenanceCustomEnv["abc"], 0.001) require.Equal(t, map[string]any{"one": float64(1)}, def.InternalParameters.ProvenanceCustomEnv["def"]) @@ -48,6 +50,7 @@ func TestMarshalInvocation(t *testing.T) { }, "environment": { "platform": "linux/amd64", + "dockerfileVersion": "1.24.0-dev", "buildkit": "v0.10.3", "custom": { "foo": "bar" @@ -63,6 +66,7 @@ func TestMarshalInvocation(t *testing.T) { require.Equal(t, "git+https://github.com/example/repo.git", inv.ConfigSource.URI) require.Equal(t, "dockerfile.v0", inv.Parameters.Frontend) require.Equal(t, "linux/amd64", inv.Environment.Platform) + require.Equal(t, "1.24.0-dev", inv.Environment.DockerfileVersion) require.Equal(t, "v0.10.3", inv.Environment.ProvenanceCustomEnv["buildkit"]) require.Equal(t, "bar", inv.Environment.ProvenanceCustomEnv["custom"].(map[string]any)["foo"]) require.Equal(t, []any{float64(1), float64(2), float64(3)}, inv.Environment.ProvenanceCustomEnv["bar"])