Skip to content

Commit ae4ee62

Browse files
authored
Merge pull request #6344 from tonistiigi/git-policy-signatures
client: enable git signature checks via policy
2 parents 594543f + a372e4e commit ae4ee62

File tree

5 files changed

+297
-18
lines changed

5 files changed

+297
-18
lines changed

client/client_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,7 @@ var allTests = []func(t *testing.T, sb integration.Sandbox){
254254
testSourcePolicySession,
255255
testSourceMetaPolicySession,
256256
testSourcePolicyParallelSession,
257+
testSourcePolicySignedCommit,
257258
}
258259

259260
func TestIntegration(t *testing.T) {
@@ -12744,13 +12745,18 @@ func testGitResolveMutatedSource(t *testing.T, sb integration.Sandbox) {
1274412745
}
1274512746

1274612747
func runInDir(dir string, cmds ...string) error {
12748+
return runInDirEnv(dir, nil, cmds...)
12749+
}
12750+
12751+
func runInDirEnv(dir string, env []string, cmds ...string) error {
1274712752
for _, args := range cmds {
1274812753
var cmd *exec.Cmd
1274912754
if runtime.GOOS == "windows" {
1275012755
cmd = exec.Command("powershell", "-command", args)
1275112756
} else {
1275212757
cmd = exec.Command("sh", "-c", args)
1275312758
}
12759+
cmd.Env = append(os.Environ(), env...)
1275412760
cmd.Dir = dir
1275512761
if err := cmd.Run(); err != nil {
1275612762
return errors.Wrapf(err, "error running %v", args)

client/policy_test.go

Lines changed: 250 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ package client
33
import (
44
"context"
55
"encoding/json"
6+
"net/http"
7+
"net/http/httptest"
8+
"os"
9+
"path/filepath"
610
"runtime"
711
"strings"
812
"testing"
@@ -11,10 +15,10 @@ import (
1115
"github.com/containerd/platforms"
1216
"github.com/moby/buildkit/client/llb"
1317
"github.com/moby/buildkit/client/llb/sourceresolver"
14-
client "github.com/moby/buildkit/frontend/gateway/client"
18+
gateway "github.com/moby/buildkit/frontend/gateway/client"
1519
pb "github.com/moby/buildkit/frontend/gateway/pb"
1620
opspb "github.com/moby/buildkit/solver/pb"
17-
sourcepolicpb "github.com/moby/buildkit/sourcepolicy/pb"
21+
sourcepolicypb "github.com/moby/buildkit/sourcepolicy/pb"
1822
"github.com/moby/buildkit/sourcepolicy/policysession"
1923
"github.com/moby/buildkit/util/testutil/integration"
2024
digest "github.com/opencontainers/go-digest"
@@ -50,7 +54,7 @@ func testSourcePolicySession(t *testing.T, sb integration.Sandbox) {
5054

5155
require.Equal(t, "docker-image://docker.io/library/alpine:latest", req.Source.Source.Identifier)
5256
return &policysession.DecisionResponse{
53-
Action: sourcepolicpb.PolicyAction_ALLOW,
57+
Action: sourcepolicypb.PolicyAction_ALLOW,
5458
}, nil, nil
5559
},
5660
},
@@ -65,7 +69,7 @@ func testSourcePolicySession(t *testing.T, sb integration.Sandbox) {
6569
"image.layerlimit": "1",
6670
}, req.Source.Source.Attrs)
6771
return &policysession.DecisionResponse{
68-
Action: sourcepolicpb.PolicyAction_ALLOW,
72+
Action: sourcepolicypb.PolicyAction_ALLOW,
6973
}, nil, nil
7074
},
7175
},
@@ -104,7 +108,7 @@ func testSourcePolicySession(t *testing.T, sb integration.Sandbox) {
104108
require.NoError(t, err)
105109
require.NotEmpty(t, cfg.RootFS)
106110
return &policysession.DecisionResponse{
107-
Action: sourcepolicpb.PolicyAction_ALLOW,
111+
Action: sourcepolicypb.PolicyAction_ALLOW,
108112
}, nil, nil
109113
},
110114
},
@@ -178,7 +182,7 @@ func testSourceMetaPolicySession(t *testing.T, sb integration.Sandbox) {
178182

179183
require.Equal(t, "docker-image://docker.io/library/alpine:latest", req.Source.Source.Identifier)
180184
return &policysession.DecisionResponse{
181-
Action: sourcepolicpb.PolicyAction_ALLOW,
185+
Action: sourcepolicypb.PolicyAction_ALLOW,
182186
}, nil, nil
183187
},
184188
},
@@ -214,7 +218,7 @@ func testSourceMetaPolicySession(t *testing.T, sb integration.Sandbox) {
214218
})
215219
_, err = c.Build(ctx, SolveOpt{
216220
SourcePolicyProvider: p,
217-
}, "test", func(ctx context.Context, c client.Client) (*client.Result, error) {
221+
}, "test", func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
218222
sop, opts := tc.source()
219223
_, err = c.ResolveSourceMetadata(ctx, sop, opts)
220224
return nil, err
@@ -267,7 +271,7 @@ func testSourcePolicyParallelSession(t *testing.T, sb integration.Sandbox) {
267271
countAlpine++
268272
close(waitAlpineDone)
269273
return &policysession.DecisionResponse{
270-
Action: sourcepolicpb.PolicyAction_ALLOW,
274+
Action: sourcepolicypb.PolicyAction_ALLOW,
271275
}, nil, nil
272276
default:
273277
require.Fail(t, "too many calls for alpine")
@@ -278,7 +282,7 @@ func testSourcePolicyParallelSession(t *testing.T, sb integration.Sandbox) {
278282
countBusybox++
279283
<-waitAlpineDone
280284
return &policysession.DecisionResponse{
281-
Action: sourcepolicpb.PolicyAction_ALLOW,
285+
Action: sourcepolicypb.PolicyAction_ALLOW,
282286
}, nil, nil
283287
}
284288
return nil, nil, errors.Errorf("unexpected source %q", req.Source.Source.Identifier)
@@ -292,3 +296,240 @@ func testSourcePolicyParallelSession(t *testing.T, sb integration.Sandbox) {
292296
require.Equal(t, 2, countAlpine)
293297
require.Equal(t, 1, countBusybox)
294298
}
299+
300+
func testSourcePolicySignedCommit(t *testing.T, sb integration.Sandbox) {
301+
requiresLinux(t)
302+
c, err := New(sb.Context(), sb.Address())
303+
require.NoError(t, err)
304+
defer c.Close()
305+
306+
signFixturesPath, ok := os.LookupEnv("BUILDKIT_TEST_SIGN_FIXTURES")
307+
if !ok {
308+
t.Skip("missing BUILDKIT_TEST_SIGN_FIXTURES")
309+
}
310+
311+
withSign := func(user, method string) []string {
312+
return []string{
313+
"GIT_CONFIG_GLOBAL=" + filepath.Join(signFixturesPath, user+"."+method+".gitconfig"),
314+
}
315+
}
316+
317+
gitDir := t.TempDir()
318+
gitCommands := []string{
319+
"git init",
320+
"git config --local user.email test",
321+
"git config --local user.name test",
322+
"echo a > a",
323+
"git add a",
324+
"git commit -m a",
325+
"git tag -a v0.1 -m v0.1",
326+
}
327+
err = runInDir(gitDir, gitCommands...)
328+
require.NoError(t, err)
329+
gitCommands = []string{
330+
"echo b > b",
331+
"git add b",
332+
"git commit -m b",
333+
"git checkout -B v2",
334+
}
335+
err = runInDirEnv(gitDir, withSign("user1", "gpg"), gitCommands...)
336+
require.NoError(t, err)
337+
gitCommands = []string{
338+
"git tag -s -a v2.0 -m v2.0-tag",
339+
"git update-server-info",
340+
}
341+
err = runInDirEnv(gitDir, withSign("user2", "ssh"), gitCommands...)
342+
require.NoError(t, err)
343+
344+
server := httptest.NewServer(http.FileServer(http.Dir(filepath.Clean(gitDir))))
345+
defer server.Close()
346+
347+
pubKeyUser1gpg, err := os.ReadFile(filepath.Join(signFixturesPath, "user1.gpg.pub"))
348+
require.NoError(t, err)
349+
350+
pubKeyUser2ssh, err := os.ReadFile(filepath.Join(signFixturesPath, "user2.ssh.pub"))
351+
require.NoError(t, err)
352+
353+
type testCase struct {
354+
state func() llb.State
355+
name string
356+
srcPol *sourcepolicypb.Policy
357+
expectedErr string
358+
}
359+
360+
gitURL := "git://" + strings.TrimPrefix(server.URL, "http://") + "/.git"
361+
362+
tests := []testCase{
363+
{
364+
name: "unsigned commit fails",
365+
srcPol: &sourcepolicypb.Policy{
366+
Rules: []*sourcepolicypb.Rule{
367+
{
368+
Action: sourcepolicypb.PolicyAction_CONVERT,
369+
Selector: &sourcepolicypb.Selector{
370+
Identifier: gitURL + "#v0.1",
371+
},
372+
Updates: &sourcepolicypb.Update{
373+
Identifier: gitURL + "#v0.1",
374+
Attrs: map[string]string{
375+
"git.sig.pubkey": string(pubKeyUser1gpg),
376+
},
377+
},
378+
},
379+
},
380+
},
381+
state: func() llb.State {
382+
return llb.Git(server.URL+"/.git", "", llb.GitRef("v0.1"))
383+
},
384+
expectedErr: "git object is not signed",
385+
},
386+
{
387+
name: "valid gpg signature for branch",
388+
srcPol: &sourcepolicypb.Policy{
389+
Rules: []*sourcepolicypb.Rule{
390+
{
391+
Action: sourcepolicypb.PolicyAction_CONVERT,
392+
Selector: &sourcepolicypb.Selector{
393+
Identifier: gitURL + "#v2",
394+
},
395+
Updates: &sourcepolicypb.Update{
396+
Identifier: gitURL + "#v2",
397+
Attrs: map[string]string{
398+
"git.sig.pubkey": string(pubKeyUser1gpg),
399+
"git.sig.rejectexpired": "true",
400+
"git.sig.ignoresignedtag": "false",
401+
},
402+
},
403+
},
404+
},
405+
},
406+
state: func() llb.State {
407+
return llb.Git(server.URL+"/.git", "", llb.GitRef("v2"))
408+
},
409+
},
410+
{
411+
name: "valid ssh signature for signed tag",
412+
srcPol: &sourcepolicypb.Policy{
413+
Rules: []*sourcepolicypb.Rule{
414+
{
415+
Action: sourcepolicypb.PolicyAction_CONVERT,
416+
Selector: &sourcepolicypb.Selector{
417+
Identifier: gitURL + "#v2.0",
418+
},
419+
Updates: &sourcepolicypb.Update{
420+
Identifier: gitURL + "#v2.0",
421+
Attrs: map[string]string{
422+
"git.sig.pubkey": string(pubKeyUser2ssh),
423+
"git.sig.requiresignedtag": "true",
424+
"git.sig.rejectexpired": "true",
425+
},
426+
},
427+
},
428+
},
429+
},
430+
state: func() llb.State {
431+
return llb.Git(server.URL+"/.git", "", llb.GitRef("v2.0"))
432+
},
433+
},
434+
{
435+
name: "invalid ssh signature for signed tag",
436+
srcPol: &sourcepolicypb.Policy{
437+
Rules: []*sourcepolicypb.Rule{
438+
{
439+
Action: sourcepolicypb.PolicyAction_CONVERT,
440+
Selector: &sourcepolicypb.Selector{
441+
Identifier: gitURL + "#v2.0",
442+
},
443+
Updates: &sourcepolicypb.Update{
444+
Identifier: gitURL + "#v2.0",
445+
Attrs: map[string]string{
446+
"git.sig.pubkey": string(pubKeyUser1gpg),
447+
"git.sig.requiresignedtag": "true",
448+
"git.sig.rejectexpired": "true",
449+
},
450+
},
451+
},
452+
},
453+
},
454+
expectedErr: "failed to parse ssh public key",
455+
state: func() llb.State {
456+
return llb.Git(server.URL+"/.git", "", llb.GitRef("v2.0"))
457+
},
458+
},
459+
{
460+
name: "commit ssh signature for signed tag",
461+
srcPol: &sourcepolicypb.Policy{
462+
Rules: []*sourcepolicypb.Rule{
463+
{
464+
Action: sourcepolicypb.PolicyAction_CONVERT,
465+
Selector: &sourcepolicypb.Selector{
466+
Identifier: gitURL + "#v2.0",
467+
},
468+
Updates: &sourcepolicypb.Update{
469+
Identifier: gitURL + "#v2.0",
470+
Attrs: map[string]string{
471+
"git.sig.pubkey": string(pubKeyUser1gpg),
472+
"git.sig.requiresignedtag": "false",
473+
"git.sig.rejectexpired": "true",
474+
},
475+
},
476+
},
477+
},
478+
},
479+
state: func() llb.State {
480+
return llb.Git(server.URL+"/.git", "", llb.GitRef("v2.0"))
481+
},
482+
},
483+
{
484+
name: "invalid tag signature for commit",
485+
srcPol: &sourcepolicypb.Policy{
486+
Rules: []*sourcepolicypb.Rule{
487+
{
488+
Action: sourcepolicypb.PolicyAction_CONVERT,
489+
Selector: &sourcepolicypb.Selector{
490+
Identifier: gitURL + "#v2.0",
491+
},
492+
Updates: &sourcepolicypb.Update{
493+
Identifier: gitURL + "#v2.0",
494+
Attrs: map[string]string{
495+
"git.sig.pubkey": string(pubKeyUser2ssh),
496+
"git.sig.rejectexpired": "true",
497+
"git.sig.ignoresignedtag": "true",
498+
},
499+
},
500+
},
501+
},
502+
},
503+
expectedErr: "failed to read armored public key: openpgp",
504+
state: func() llb.State {
505+
return llb.Git(server.URL+"/.git", "", llb.GitRef("v2.0"))
506+
},
507+
},
508+
}
509+
510+
for _, tt := range tests {
511+
t.Run(tt.name, func(t *testing.T) {
512+
frontend := func(ctx context.Context, c gateway.Client) (*gateway.Result, error) {
513+
st := llb.Scratch().File(
514+
llb.Copy(tt.state(), "a", "/a2"),
515+
)
516+
def, err := st.Marshal(sb.Context())
517+
if err != nil {
518+
return nil, err
519+
}
520+
return c.Solve(ctx, gateway.SolveRequest{
521+
Definition: def.ToPB(),
522+
})
523+
}
524+
525+
_, err := c.Build(sb.Context(), SolveOpt{
526+
SourcePolicy: tt.srcPol,
527+
}, "", frontend, nil)
528+
if tt.expectedErr == "" {
529+
require.NoError(t, err, "test case %q failed", tt.name)
530+
return
531+
}
532+
require.ErrorContains(t, err, tt.expectedErr, "test case %q failed", tt.name)
533+
})
534+
}
535+
}

solver/pb/attr.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,11 @@ const AttrMountSSHSock = "git.mountsshsock"
99
const AttrGitChecksum = "git.checksum"
1010
const AttrGitSkipSubmodules = "git.skipsubmodules"
1111

12+
const AttrGitSignatureVerifyPubKey = "git.sig.pubkey"
13+
const AttrGitSignatureVerifyRejectExpired = "git.sig.rejectexpired"
14+
const AttrGitSignatureVerifyRequireSignedTag = "git.sig.requiresignedtag"
15+
const AttrGitSignatureVerifyIgnoreSignedTag = "git.sig.ignoresignedtag"
16+
1217
const AttrLocalSessionID = "local.session"
1318
const AttrLocalUniqueID = "local.unique"
1419
const AttrIncludePatterns = "local.includepattern"

0 commit comments

Comments
 (0)