From 2895f61c95c4a3aba2c744da98278e031013a639 Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Mon, 1 Apr 2024 15:23:13 -0400 Subject: [PATCH 01/28] Add fields to struct that will enable package signing The general idea is that the image specified in spec.targets.x.package_config.signer.image will receive an `llb.State` as an input, and provide a tranformed state (with the artifacts in question signed in the desired manner) as the output. Signed-off-by: Peter Engelbert --- spec.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/spec.go b/spec.go index dffb57e3..9dd864bb 100644 --- a/spec.go +++ b/spec.go @@ -402,6 +402,21 @@ type Target struct { // Tests are the list of tests to run which are specific to the target. // Tests are appended to the list of tests in the main [Spec] Tests []*TestSpec `yaml:"tests,omitempty" json:"tests,omitempty"` + + // PackageConfig is the configuration to use for artifact targets, such as + // rpms, debs, or zip files containing Windows binaries + PackageConfig *PackageConfig `yaml:"package_config,omitempty" json:"package_config,omitempty"` +} + +// PackageConfig encapsulates the configuration for artifact targets +type PackageConfig struct { + // Signer is the configuration to use for signing packages + Signer *Signer `yaml:"signer,omitempty" json:"signer,omitempty"` +} + +// Signer encapsulates the configuration for an image that performs package signing +type Signer struct { + Image *SourceDockerImage `yaml:"image,omitempty" json:"image,omitempty"` } // TestSpec is used to execute tests against a container with the package installed in it. From d2fd2400f639f6ee76e22dc7b8261adfb67e71a3 Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Thu, 4 Apr 2024 09:50:12 -0400 Subject: [PATCH 02/28] Begin implementing forwarding for signing Signed-off-by: Peter Engelbert --- frontend/mariner2/handle_rpm.go | 97 +++++++++++++++++++++++++++++++++ test/fixtures/phony.go | 2 +- test/testenv/buildx.go | 2 + test/testenv/builld.go | 6 +- 4 files changed, 104 insertions(+), 3 deletions(-) diff --git a/frontend/mariner2/handle_rpm.go b/frontend/mariner2/handle_rpm.go index 41d5ec03..81c9c832 100644 --- a/frontend/mariner2/handle_rpm.go +++ b/frontend/mariner2/handle_rpm.go @@ -11,12 +11,16 @@ import ( "github.com/Azure/dalec/frontend/rpm" "github.com/moby/buildkit/client/llb" gwclient "github.com/moby/buildkit/frontend/gateway/client" + "github.com/moby/buildkit/identity" + "github.com/moby/buildkit/solver/pb" ocispecs "github.com/opencontainers/image-spec/specs-go/v1" ) const ( marinerRef = "mcr.microsoft.com/cbl-mariner/base/core:2.0" + initialState = "initialstate" + tdnfCacheDir = "/var/cache/tdnf" tdnfCacheName = "mariner2-tdnf-cache" ) @@ -31,6 +35,69 @@ func tdnfCacheMountWithPrefix(prefix string) llb.RunOption { return llb.AddMount(filepath.Join(prefix, tdnfCacheDir), llb.Scratch(), llb.AsPersistentCacheDir(tdnfCacheName, llb.CacheMountLocked)) } +func hasSigner(t *dalec.Target) bool { + return t != nil && t.PackageConfig != nil && t.PackageConfig.Signer != nil && t.PackageConfig.Signer.Image != nil +} + +func forwardToSigner(ctx context.Context, client gwclient.Client, cfg *dalec.Signer, s llb.State, sOpt dalec.SourceOpts) (llb.State, error) { + signer := llb.Image(cfg.Image.Ref, llb.WithMetaResolver(sOpt.Resolver)) + + d, err := signer.Marshal(ctx) + if err != nil { + return llb.Scratch(), err + } + + res, err := client.Solve(ctx, gwclient.SolveRequest{ + Definition: d.ToPB(), + }) + if err != nil { + return llb.Scratch(), err + } + + r, _ := res.SingleRef() + _ = r + + dt := res.Metadata["result.json"] + _ = dt + + req := gwclient.SolveRequest{ + Frontend: "gateway.v0", + FrontendOpt: make(map[string]string), + FrontendInputs: make(map[string]*pb.Definition), + } + id := identity.NewID() + req.FrontendOpt["context:"+id] = "input:" + id + + signerDef, err := signer.Marshal(ctx) + if err != nil { + return llb.Scratch(), err + } + + req.FrontendInputs[id] = signerDef.ToPB() + req.FrontendOpt["source"] = id + req.FrontendOpt["cmdline"] = "" + req.FrontendOpt["context:"+initialState] = "input:" + initialState + + stateDef, err := s.Marshal(ctx) + if err != nil { + return llb.Scratch(), err + } + + req.FrontendInputs[initialState] = stateDef.ToPB() + + res, err = client.Solve(ctx, req) + if err != nil { + return llb.Scratch(), err + } + + r2, err := res.SingleRef() + if err != nil { + return llb.Scratch(), err + } + + return r2.ToState() +} + func handleRPM(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) { return frontend.BuildWithPlatform(ctx, client, func(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (gwclient.Reference, *dalec.DockerImageSpec, error) { if err := rpm.ValidateSpec(spec); err != nil { @@ -48,6 +115,35 @@ func handleRPM(ctx context.Context, client gwclient.Client) (*gwclient.Result, e return nil, nil, err } + t := spec.Targets[targetKey] + if hasSigner(&t) { + signed, err := forwardToSigner(ctx, client, t.PackageConfig.Signer, st, sOpt) + if err != nil { + return nil, nil, err + } + + st = signed + + def, err := st.Marshal(ctx, pg) + if err != nil { + return nil, nil, fmt.Errorf("error marshalling llb: %w", err) + } + + res, err := client.Solve(ctx, gwclient.SolveRequest{ + Definition: def.ToPB(), + }) + if err != nil { + return nil, nil, err + } + + ref, err := res.SingleRef() + if err != nil { + return nil, nil, err + } + + return ref, nil, nil + } + def, err := st.Marshal(ctx, pg) if err != nil { return nil, nil, fmt.Errorf("error marshalling llb: %w", err) @@ -64,6 +160,7 @@ func handleRPM(ctx context.Context, client gwclient.Client) (*gwclient.Result, e if err != nil { return nil, nil, err } + return ref, nil, nil }) } diff --git a/test/fixtures/phony.go b/test/fixtures/phony.go index b7dadbb2..02c3d737 100644 --- a/test/fixtures/phony.go +++ b/test/fixtures/phony.go @@ -30,7 +30,7 @@ func PhonyFrontend(ctx context.Context, gwc gwclient.Client) (*gwclient.Result, st := llb.Image("golang:1.21", llb.WithMetaResolver(gwc)). Run( - llb.Args([]string{"go", "build", "-o=/build/out", "./test/fixtures/phony"}), + llb.Args([]string{"go", "build", "-gcflags=all=-N -l", "-o=/build/out", "./test/fixtures/phony"}), llb.AddEnv("CGO_ENABLED", "0"), goModCache, goBuildCache, diff --git a/test/testenv/buildx.go b/test/testenv/buildx.go index 63451da6..7f3d0a8d 100644 --- a/test/testenv/buildx.go +++ b/test/testenv/buildx.go @@ -11,6 +11,7 @@ import ( "path/filepath" "sync" "testing" + "time" "github.com/cpuguy83/dockercfg" "github.com/cpuguy83/go-docker" @@ -189,6 +190,7 @@ func (b *BuildxEnv) bootstrap(ctx context.Context) (retErr error) { } c, err := client.New(ctx, "", client.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) { + conn2.SetDeadline(time.Now().Add(time.Hour * 24)) return conn2, nil })) if err != nil { diff --git a/test/testenv/builld.go b/test/testenv/builld.go index a548ce27..223c5898 100644 --- a/test/testenv/builld.go +++ b/test/testenv/builld.go @@ -46,8 +46,10 @@ func buildBaseFrontend(ctx context.Context, c gwclient.Client) (*gwclient.Result defPB := def.ToPB() return c.Solve(ctx, gwclient.SolveRequest{ - Frontend: "dockerfile.v0", - FrontendOpt: map[string]string{}, + Frontend: "dockerfile.v0", + FrontendOpt: map[string]string{ + "build-arg:GOFLAGS": "\"-gcflags=all=-N -l\"", + }, FrontendInputs: map[string]*pb.Definition{ dockerui.DefaultLocalNameContext: defPB, dockerui.DefaultLocalNameDockerfile: dockerfileDef.ToPB(), From bcd6850a5f696e782fd67e37b48708f6ab3705e9 Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Thu, 4 Apr 2024 13:44:12 -0400 Subject: [PATCH 03/28] Clean up signing forwarding code Signed-off-by: Peter Engelbert --- frontend/mariner2/handle_rpm.go | 101 +++++++++++++++++--------------- 1 file changed, 53 insertions(+), 48 deletions(-) diff --git a/frontend/mariner2/handle_rpm.go b/frontend/mariner2/handle_rpm.go index 81c9c832..6a152ac3 100644 --- a/frontend/mariner2/handle_rpm.go +++ b/frontend/mariner2/handle_rpm.go @@ -2,6 +2,7 @@ package mariner2 import ( "context" + "encoding/json" "fmt" "path/filepath" "strings" @@ -10,6 +11,7 @@ import ( "github.com/Azure/dalec/frontend" "github.com/Azure/dalec/frontend/rpm" "github.com/moby/buildkit/client/llb" + "github.com/moby/buildkit/client/llb/sourceresolver" gwclient "github.com/moby/buildkit/frontend/gateway/client" "github.com/moby/buildkit/identity" "github.com/moby/buildkit/solver/pb" @@ -39,44 +41,66 @@ func hasSigner(t *dalec.Target) bool { return t != nil && t.PackageConfig != nil && t.PackageConfig.Signer != nil && t.PackageConfig.Signer.Image != nil } -func forwardToSigner(ctx context.Context, client gwclient.Client, cfg *dalec.Signer, s llb.State, sOpt dalec.SourceOpts) (llb.State, error) { - signer := llb.Image(cfg.Image.Ref, llb.WithMetaResolver(sOpt.Resolver)) +func compound(k, v string) string { + return fmt.Sprintf("%s:%s", k, v) +} - d, err := signer.Marshal(ctx) - if err != nil { - return llb.Scratch(), err +func forwardToSigner(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, cfg *dalec.Signer, s llb.State) (llb.State, error) { + const ( + sourceKey = "source" + contextKey = "context" + targetKey = "target" + inputKey = "input" + resolveModeKey = "image.resolvemode" + containerImageConfigKey = "containerimage.config" + inputMetadataKey = "input-metadata" + + gatewayFrontend = "gateway.v0" + ) + + opts := client.BuildOpts().Opts + signer := llb.Image(cfg.Image.Ref) + id := identity.NewID() + + req := gwclient.SolveRequest{ + Frontend: gatewayFrontend, + FrontendOpt: make(map[string]string), + FrontendInputs: make(map[string]*pb.Definition), } - res, err := client.Solve(ctx, gwclient.SolveRequest{ - Definition: d.ToPB(), + _, _, b, err := client.ResolveImageConfig(ctx, cfg.Image.Ref, sourceresolver.Opt{ + Platform: platform, + ImageOpt: &sourceresolver.ResolveImageOpt{ + ResolveMode: opts[resolveModeKey], + }, }) + + withConfig, err := signer.WithImageConfig(b) if err != nil { return llb.Scratch(), err } - r, _ := res.SingleRef() - _ = r + signerDef, err := withConfig.Marshal(ctx) + if err != nil { + return llb.Scratch(), err + } + signerPB := signerDef.ToPB() - dt := res.Metadata["result.json"] - _ = dt + req.FrontendOpt[compound(contextKey, id)] = compound(inputKey, id) + req.FrontendInputs[id] = signerPB + req.FrontendOpt[sourceKey] = id + req.FrontendOpt[compound(contextKey, initialState)] = compound(inputKey, initialState) + req.FrontendOpt[targetKey] = "check" + req.FrontendInputs[contextKey] = signerPB - req := gwclient.SolveRequest{ - Frontend: "gateway.v0", - FrontendOpt: make(map[string]string), - FrontendInputs: make(map[string]*pb.Definition), + meta := map[string][]byte{ + containerImageConfigKey: b, } - id := identity.NewID() - req.FrontendOpt["context:"+id] = "input:" + id - - signerDef, err := signer.Marshal(ctx) + metaDt, err := json.Marshal(meta) if err != nil { - return llb.Scratch(), err + return llb.Scratch(), fmt.Errorf("error marshaling local frontend metadata: %w", err) } - - req.FrontendInputs[id] = signerDef.ToPB() - req.FrontendOpt["source"] = id - req.FrontendOpt["cmdline"] = "" - req.FrontendOpt["context:"+initialState] = "input:" + initialState + req.FrontendOpt[compound(inputMetadataKey, id)] = string(metaDt) stateDef, err := s.Marshal(ctx) if err != nil { @@ -85,17 +109,17 @@ func forwardToSigner(ctx context.Context, client gwclient.Client, cfg *dalec.Sig req.FrontendInputs[initialState] = stateDef.ToPB() - res, err = client.Solve(ctx, req) + res, err := client.Solve(ctx, req) if err != nil { return llb.Scratch(), err } - r2, err := res.SingleRef() + ref, err := res.SingleRef() if err != nil { return llb.Scratch(), err } - return r2.ToState() + return ref.ToState() } func handleRPM(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) { @@ -117,31 +141,12 @@ func handleRPM(ctx context.Context, client gwclient.Client) (*gwclient.Result, e t := spec.Targets[targetKey] if hasSigner(&t) { - signed, err := forwardToSigner(ctx, client, t.PackageConfig.Signer, st, sOpt) + signed, err := forwardToSigner(ctx, client, platform, t.PackageConfig.Signer, st) if err != nil { return nil, nil, err } st = signed - - def, err := st.Marshal(ctx, pg) - if err != nil { - return nil, nil, fmt.Errorf("error marshalling llb: %w", err) - } - - res, err := client.Solve(ctx, gwclient.SolveRequest{ - Definition: def.ToPB(), - }) - if err != nil { - return nil, nil, err - } - - ref, err := res.SingleRef() - if err != nil { - return nil, nil, err - } - - return ref, nil, nil } def, err := st.Marshal(ctx, pg) From 7e935895e3a7e3d2bf19adb9edb7cef6f67aa625 Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Mon, 8 Apr 2024 13:57:42 -0400 Subject: [PATCH 04/28] Add minimal signer Signed-off-by: Peter Engelbert --- cmd/signer/Dockerfile | 31 ++++++++++++++++++++++++++++ cmd/signer/esrp.yml | 32 +++++++++++++++++++++++++++++ cmd/signer/main.go | 48 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 111 insertions(+) create mode 100644 cmd/signer/Dockerfile create mode 100644 cmd/signer/esrp.yml create mode 100644 cmd/signer/main.go diff --git a/cmd/signer/Dockerfile b/cmd/signer/Dockerfile new file mode 100644 index 00000000..56adcba0 --- /dev/null +++ b/cmd/signer/Dockerfile @@ -0,0 +1,31 @@ +# syntax=docker/dockerfile:1.6 + +FROM scratch AS ext + +FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.21@sha256:f5fc8bf2873d2fa538b2d87e6e8f9bc6d05a3c419a840147b25f6386855192e9 AS signer-builder +WORKDIR /build +COPY . . +ENV CGO_ENABLED=0 +ARG TARGETARCH TARGETOS GOFLAGS=-trimpath + +ENV GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOFLAGS=${GOFLAGS} +RUN \ + --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + go build -o /signer ./cmd/signer + +FROM mcr.microsoft.com/azure-cli:cbl-mariner2.0 as az-base +WORKDIR /root +COPY --from=ext /xsignextension-0.46-py3-none-any.whl . +COPY --from=signer-builder /signer /signer +ARG XSIGNEXTENSION_VERSION=0.46 +RUN < Date: Thu, 11 Apr 2024 11:42:56 -0400 Subject: [PATCH 05/28] test Signed-off-by: Peter Engelbert --- cmd/signer/main.go | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/cmd/signer/main.go b/cmd/signer/main.go index 69d0e770..9f71aa27 100644 --- a/cmd/signer/main.go +++ b/cmd/signer/main.go @@ -6,6 +6,7 @@ import ( "fmt" "os" + "github.com/moby/buildkit/client/llb" gwclient "github.com/moby/buildkit/frontend/gateway/client" "github.com/moby/buildkit/frontend/gateway/grpcclient" "github.com/moby/buildkit/util/appcontext" @@ -33,7 +34,40 @@ func main() { return nil, fmt.Errorf("no artifact state provided to signer") } - def, err := artifacts.Marshal(ctx) + base, ok := inputs["context"] + if !ok { + return nil, fmt.Errorf("no base signing image provided") + } + + base = base.File(llb.Mkfile("/config.json", 0o600, []byte(` +{ + "clientId": "12f74099-0b7a-4e7b-8b7f-c1e0747fadc8", + "gatewayApi": "https://api.esrp.microsoft.com", + "requestSigningCert": { + "subject": "esrp-prss", + "vaultName": "upstreamci-ado" + }, + "driEmail": [ + "pengelbert@microsoft.com" + ], + "signingOperations": [ + { + "keyCode": "CP-450778-Pgp", + "operationSetCode": "LinuxSign", + "parameters": [], + "toolName": "sign", + "toolVersion": 1 + } + ], + "hashType": "sha256" +} +`))) + + output := base.Run(llb.Args([]string{ + "az", "xsign", "sign-file", "--file-name", "/artifacts/RPMS/x86_64/*", "--config-file", "/config.json", + })).AddMount("/artifacts", artifacts) + + def, err := output.Marshal(ctx) if err != nil { return nil, err } From 0425ba5eb50cc27ba6edb06ed431e0eb4399ed35 Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Fri, 12 Apr 2024 14:07:33 +0000 Subject: [PATCH 06/28] save Signed-off-by: Peter Engelbert --- cmd/signer/Dockerfile | 8 ++++---- cmd/signer/main.go | 14 +++++++++++--- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/cmd/signer/Dockerfile b/cmd/signer/Dockerfile index 56adcba0..b422a67c 100644 --- a/cmd/signer/Dockerfile +++ b/cmd/signer/Dockerfile @@ -1,7 +1,5 @@ # syntax=docker/dockerfile:1.6 -FROM scratch AS ext - FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.21@sha256:f5fc8bf2873d2fa538b2d87e6e8f9bc6d05a3c419a840147b25f6386855192e9 AS signer-builder WORKDIR /build COPY . . @@ -16,8 +14,7 @@ RUN \ FROM mcr.microsoft.com/azure-cli:cbl-mariner2.0 as az-base WORKDIR /root -COPY --from=ext /xsignextension-0.46-py3-none-any.whl . -COPY --from=signer-builder /signer /signer +COPY ./tmp/xsignextension-0.46-py3-none-any.whl . ARG XSIGNEXTENSION_VERSION=0.46 RUN < Date: Fri, 12 Apr 2024 12:10:11 -0400 Subject: [PATCH 07/28] Working signing setup Signed-off-by: Peter Engelbert --- cmd/signer/Dockerfile | 3 +++ cmd/signer/main.go | 50 +++++++++++++++++++++++-------------------- 2 files changed, 30 insertions(+), 23 deletions(-) diff --git a/cmd/signer/Dockerfile b/cmd/signer/Dockerfile index b422a67c..194c1f86 100644 --- a/cmd/signer/Dockerfile +++ b/cmd/signer/Dockerfile @@ -1,5 +1,7 @@ # syntax=docker/dockerfile:1.6 +FROM scratch as creds + FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.21@sha256:f5fc8bf2873d2fa538b2d87e6e8f9bc6d05a3c419a840147b25f6386855192e9 AS signer-builder WORKDIR /build COPY . . @@ -26,6 +28,7 @@ EOF FROM az-base as final COPY --from=signer-builder /signer /signer +COPY --from=creds .azure /root/.azure LABEL moby.buildkit.frontend.network.none="true" LABEL moby.buildkit.frontend.caps="moby.buildkit.frontend.inputs,moby.buildkit.frontend.subrequests,moby.buildkit.frontend.contexts" ENTRYPOINT ["/signer"] diff --git a/cmd/signer/main.go b/cmd/signer/main.go index 006ecde9..061a2fdc 100644 --- a/cmd/signer/main.go +++ b/cmd/signer/main.go @@ -39,41 +39,45 @@ func main() { return nil, fmt.Errorf("no base signing image provided") } - base = base.File(llb.Mkfile("/script.sh", 0o700, []byte(` + base = base.File(llb.Mkfile("/script.sh", 0o777, []byte(` + #!/usr/bin/env bash set -exu KEYVAULT_NAME="temp-azcu-signing-kv" - az login --use-device-code export AZURE_CLIENT_SECRET=$(az keyvault secret show --name esrp-token --vault-name $KEYVAULT_NAME --query value -o tsv) export AZURE_CLIENT_ID=$(az keyvault secret show --name esrp-sp-id --vault-name $KEYVAULT_NAME --query value -o tsv) export ESRP_KEYCODE=$(az keyvault secret show --name esrp-keycode-test --vault-name $KEYVAULT_NAME --query value -o tsv) export AZURE_TENANT_ID=$(az keyvault secret show --name esrp-sp-tenant --vault-name $KEYVAULT_NAME --query value -o tsv) az login --service-principal -u "$AZURE_CLIENT_ID" -p "$AZURE_CLIENT_SECRET" --tenant "$AZURE_TENANT_ID" --allow-no-subscriptions - az xsign sign-file --file-name /artifacts/RPMS/x86_64/* --config-file /config.json`))) + az xsign sign-file --file-name /artifacts/RPMS/x86_64/* --config-file /config.json + `))) base = base.File(llb.Mkfile("/config.json", 0o600, []byte(` { - "clientId": "12f74099-0b7a-4e7b-8b7f-c1e0747fadc8", - "gatewayApi": "https://api.esrp.microsoft.com", - "requestSigningCert": { - "subject": "esrp-prss", - "vaultName": "upstreamci-ado" - }, - "driEmail": [ - "pengelbert@microsoft.com" - ], - "signingOperations": [ - { - "keyCode": "CP-450778-Pgp", - "operationSetCode": "LinuxSign", - "parameters": [], - "toolName": "sign", - "toolVersion": 1 - } - ], - "hashType": "sha256" + "clientId": "f7eef2fc-cc8d-48f3-adfd-b97960fdb1dd", + "gatewayApi": "https://api.esrp.microsoft.com", + "requestSigningCert": { + "subject": "esrp-prss", + "vaultName": "temp-azcu-signing-kv" + }, + "driEmail": ["pengelbert@microsoft.com"], + "signingOperations": [ + { + "keyCode": "CP-467215", + "operationSetCode": "NotaryCoseSign", + "parameters": [ + { + "parameterName": "CoseFlags", + "parameterValue": "chainunprotected" + } + ], + "toolName": "sign", + "toolVersion": "1.0" + } + ], + "hashType": "sha256" } `))) - output := base.Run(llb.Args([]string{"/script.sh"})).AddMount("/artifacts", artifacts) + output := base.Run(llb.Args([]string{"bash", "/script.sh"})).AddMount("/artifacts", artifacts) def, err := output.Marshal(ctx) if err != nil { From 5747f74dfc11e77a4831dfd3679691c5da9f42f7 Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Mon, 15 Apr 2024 17:44:12 -0400 Subject: [PATCH 08/28] Wire up secrets for signing image Signed-off-by: Peter Engelbert --- cmd/signer/Dockerfile | 2 +- cmd/signer/main.go | 123 ++++++++++++++++++++++++++++++------------ 2 files changed, 90 insertions(+), 35 deletions(-) diff --git a/cmd/signer/Dockerfile b/cmd/signer/Dockerfile index 194c1f86..03455260 100644 --- a/cmd/signer/Dockerfile +++ b/cmd/signer/Dockerfile @@ -19,6 +19,7 @@ WORKDIR /root COPY ./tmp/xsignextension-0.46-py3-none-any.whl . ARG XSIGNEXTENSION_VERSION=0.46 RUN < /config.json + az login --service-principal -u "$AZURE_CLIENT_ID" -p "$AZURE_CLIENT_SECRET" --tenant "$AZURE_TENANT_ID" --allow-no-subscriptions az xsign sign-file --file-name /artifacts/RPMS/x86_64/* --config-file /config.json `))) - base = base.File(llb.Mkfile("/config.json", 0o600, []byte(` -{ - "clientId": "f7eef2fc-cc8d-48f3-adfd-b97960fdb1dd", - "gatewayApi": "https://api.esrp.microsoft.com", - "requestSigningCert": { - "subject": "esrp-prss", - "vaultName": "temp-azcu-signing-kv" - }, - "driEmail": ["pengelbert@microsoft.com"], - "signingOperations": [ - { - "keyCode": "CP-467215", - "operationSetCode": "NotaryCoseSign", - "parameters": [ - { - "parameterName": "CoseFlags", - "parameterValue": "chainunprotected" - } - ], - "toolName": "sign", - "toolVersion": "1.0" - } - ], - "hashType": "sha256" -} -`))) - output := base.Run(llb.Args([]string{"bash", "/script.sh"})).AddMount("/artifacts", artifacts) + base = base.File(llb.Mkfile("/config_template.json", 0o600, configBytes)) + + output := base.Run(llb.Args([]string{"bash", "/script.sh"}), + llb.AddSecret("/run/secrets/AZURE_CLIENT_ID", llb.SecretID("AZURE_CLIENT_ID")), + llb.AddSecret("/run/secrets/KEYVAULT_NAME", llb.SecretID("KEYVAULT_NAME")), + llb.AddSecret("/run/secrets/AZURE_CLIENT_SECRET", llb.SecretID("AZURE_CLIENT_SECRET")), + llb.AddSecret("/run/secrets/AZURE_TENANT_ID", llb.SecretID("AZURE_TENANT_ID")), + llb.AddSecret("/run/secrets/ESRP_KEYCODE", llb.SecretID("ESRP_KEYCODE")), + llb.AddSecret("/run/secrets/BUILDER_EMAIL", llb.SecretID("BUILDER_EMAIL")), + ).AddMount("/artifacts", artifacts) def, err := output.Marshal(ctx) if err != nil { From f2f89ef278223dd549eaed3119c462311c895c10 Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Wed, 17 Apr 2024 10:40:16 -0400 Subject: [PATCH 09/28] Set up test for signer Signed-off-by: Peter Engelbert --- test/fixtures/signer.go | 66 ++++++++++++++++++ test/fixtures/signer/main.go | 127 +++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 test/fixtures/signer.go create mode 100644 test/fixtures/signer/main.go diff --git a/test/fixtures/signer.go b/test/fixtures/signer.go new file mode 100644 index 00000000..83f2ef95 --- /dev/null +++ b/test/fixtures/signer.go @@ -0,0 +1,66 @@ +package fixtures + +import ( + "context" + "encoding/json" + + "github.com/Azure/dalec" + "github.com/moby/buildkit/client/llb" + "github.com/moby/buildkit/exporter/containerimage/exptypes" + "github.com/moby/buildkit/frontend/dockerui" + gwclient "github.com/moby/buildkit/frontend/gateway/client" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" +) + +func PhonySigner(ctx context.Context, gwc gwclient.Client) (*gwclient.Result, error) { + dc, err := dockerui.NewClient(gwc) + if err != nil { + return nil, err + } + + bctx, err := dc.MainContext(ctx) + if err != nil { + return nil, err + } + + st := llb.Image("golang:1.21", llb.WithMetaResolver(gwc)). + Run( + llb.Args([]string{"go", "build", "-gcflags=all=-N -l", "-o=/build/out", "./test/fixtures/signer"}), + llb.AddEnv("CGO_ENABLED", "0"), + goModCache, + goBuildCache, + llb.Dir("/build/src"), + llb.AddMount("/build/src", *bctx, llb.Readonly), + ). + AddMount("/build/out", llb.Scratch()) + + cfg := dalec.DockerImageSpec{ + Config: dalec.DockerImageConfig{ + ImageConfig: ocispecs.ImageConfig{ + Entrypoint: []string{"/signer"}, + }, + }, + } + injectFrontendLabels(&cfg) + + dt, err := json.Marshal(cfg) + if err != nil { + return nil, err + } + + def, err := st.Marshal(ctx) + if err != nil { + return nil, err + } + + res, err := gwc.Solve(ctx, gwclient.SolveRequest{ + Definition: def.ToPB(), + }) + if err != nil { + return nil, err + } + + res.AddMeta(exptypes.ExporterImageConfigKey, dt) + + return res, nil +} diff --git a/test/fixtures/signer/main.go b/test/fixtures/signer/main.go new file mode 100644 index 00000000..96a987b7 --- /dev/null +++ b/test/fixtures/signer/main.go @@ -0,0 +1,127 @@ +package main + +import ( + "context" + _ "embed" + "encoding/json" + "fmt" + "os" + + "github.com/moby/buildkit/client/llb" + gwclient "github.com/moby/buildkit/frontend/gateway/client" + "github.com/moby/buildkit/frontend/gateway/grpcclient" + "github.com/moby/buildkit/util/appcontext" + "github.com/moby/buildkit/util/bklog" + "github.com/sirupsen/logrus" + "google.golang.org/grpc/grpclog" +) + +const ( + linuxSignOp = "LinuxSign" + windowsSignOp = "NotaryCoseSign" +) + +type Config struct { + ClientId string `json:"clientId"` + GatewayAPI string `json:"gatewayApi"` + RequestSigningCert map[string]string `json:"requestSigningCert"` + DRIEmail []string `json:"driEmail"` + SigningOperations []SigningOperation `json:"signingOperations"` + HashType string `json:"hashType"` +} + +type SigningOperation struct { + KeyCode string `json:"keyCode"` + OperationSetCode string `json:"operationSetCode"` + Parameters []ParameterKV `json:"parameters"` + ToolName string `json:"toolName"` + ToolVersion string `json:"toolVersion"` +} + +type ParameterKV struct { + ParameterName string `json:"parameterName"` + ParameterValue string `json:"parameterValue"` +} + +func main() { + bklog.L.Logger.SetOutput(os.Stderr) + grpclog.SetLoggerV2(grpclog.NewLoggerV2WithVerbosity(bklog.L.WriterLevel(logrus.InfoLevel), bklog.L.WriterLevel(logrus.WarnLevel), bklog.L.WriterLevel(logrus.ErrorLevel), 1)) + + ctx := appcontext.Context() + + if err := grpcclient.RunFromEnvironment(ctx, func(ctx context.Context, c gwclient.Client) (*gwclient.Result, error) { + bopts := c.BuildOpts().Opts + target := bopts["dalec.target"] + + inputs, err := c.Inputs(ctx) + if err != nil { + return nil, err + } + + signOp := "" + params := []ParameterKV{} + + switch target { + case "windowscross", "windows": + signOp = windowsSignOp + params = append(params, ParameterKV{ + ParameterName: "CoseFlags", + ParameterValue: "chainunprotected", + }) + + default: + signOp = linuxSignOp + } + + config := Config{ + ClientId: "${AZURE_CLIENT_ID}", + GatewayAPI: "https://api.esrp.microsoft.com", + RequestSigningCert: map[string]string{ + "subject": "esrp-prss", + "vaultName": "${KEYVAULT_NAME}", + }, + DRIEmail: []string{"${BUILDER_EMAIL}"}, + SigningOperations: []SigningOperation{ + { + KeyCode: "${ESRP_KEYCODE}", + OperationSetCode: signOp, + Parameters: params, + ToolName: "sign", + ToolVersion: "1.0", + }, + }, + HashType: "sha256", + } + + _, ok := inputs["initialstate"] + if !ok { + return nil, fmt.Errorf("no artifact state provided to signer") + } + + _, ok = inputs["context"] + if !ok { + return nil, fmt.Errorf("no base signing image provided") + } + + configBytes, err := json.Marshal(&config) + if err != nil { + return nil, err + } + + output := llb.Scratch(). + File(llb.Mkfile("/target", 0o600, []byte(target))). + File(llb.Mkfile("/config.json", 0o600, configBytes)) + + def, err := output.Marshal(ctx) + if err != nil { + return nil, err + } + + return c.Solve(ctx, gwclient.SolveRequest{ + Definition: def.ToPB(), + }) + }); err != nil { + bklog.L.WithError(err).Fatal("error running frontend") + os.Exit(137) + } +} From baa9342fe549ef218b563f6e176a7139853b7600 Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Wed, 17 Apr 2024 17:39:16 -0400 Subject: [PATCH 10/28] Signing for windows and linux Signed-off-by: Peter Engelbert --- cmd/signer/main.go | 48 ++++++---- frontend/mariner2/handle_rpm.go | 93 +------------------ frontend/request.go | 55 +++++++++++ frontend/windows/handle_container.go | 9 ++ frontend/windows/handle_zip.go | 9 ++ spec.go | 7 +- test/helpers_test.go | 3 +- test/main_test.go | 4 + test/signer_test.go | 133 +++++++++++++++++++++++++++ 9 files changed, 247 insertions(+), 114 deletions(-) create mode 100644 test/signer_test.go diff --git a/cmd/signer/main.go b/cmd/signer/main.go index 4d5bd0d8..c8bcb430 100644 --- a/cmd/signer/main.go +++ b/cmd/signer/main.go @@ -6,7 +6,9 @@ import ( "encoding/json" "fmt" "os" + "strings" + "github.com/Azure/dalec/frontend" "github.com/moby/buildkit/client/llb" gwclient "github.com/moby/buildkit/frontend/gateway/client" "github.com/moby/buildkit/frontend/gateway/grpcclient" @@ -53,16 +55,39 @@ func main() { bopts := c.BuildOpts().Opts target := bopts["dalec.target"] + cc, ok := c.(frontend.CurrentFrontend) + if !ok { + return nil, fmt.Errorf("cast to currentFrontend failed") + } + + basePtr, err := cc.CurrentFrontend() + if err != nil { + return nil, err + } + base := *basePtr + + inputs, err := c.Inputs(ctx) + if err != nil { + return nil, err + } + + inputId := strings.TrimPrefix(bopts["context"], "input:") + artifacts, ok := inputs[inputId] + if !ok { + return nil, fmt.Errorf("no artifact state provided to signer") + } + signOp := "" params := []ParameterKV{} switch target { - case "windowscross", "windows": + case "windowscross", "windows", "mariner2": signOp = windowsSignOp params = append(params, ParameterKV{ ParameterName: "CoseFlags", ParameterValue: "chainunprotected", }) + default: signOp = linuxSignOp } @@ -92,20 +117,7 @@ func main() { return nil, err } - inputs, err := c.Inputs(ctx) - if err != nil { - return nil, err - } - - artifacts, ok := inputs["initialstate"] - if !ok { - return nil, fmt.Errorf("no artifact state provided to signer") - } - - base, ok := inputs["context"] - if !ok { - return nil, fmt.Errorf("no base signing image provided") - } + findPattern := bopts["find.pattern"] // In order for this signing image to work, we need base = base.File(llb.Mkfile("/script.sh", 0o777, []byte(` @@ -120,12 +132,16 @@ func main() { envsubst < /config_template.json > /config.json az login --service-principal -u "$AZURE_CLIENT_ID" -p "$AZURE_CLIENT_SECRET" --tenant "$AZURE_TENANT_ID" --allow-no-subscriptions - az xsign sign-file --file-name /artifacts/RPMS/x86_64/* --config-file /config.json + readarray -d '' artifacts < <(find /artifacts -type f ${FIND_PATTERN:+-name "$FIND_PATTERN"} -print0) + for f in "${artifacts[@]}"; do + az xsign sign-file --file-name "$f" --config-file /config.json + done `))) base = base.File(llb.Mkfile("/config_template.json", 0o600, configBytes)) output := base.Run(llb.Args([]string{"bash", "/script.sh"}), + llb.AddEnv("FIND_PATTERN", findPattern), llb.AddSecret("/run/secrets/AZURE_CLIENT_ID", llb.SecretID("AZURE_CLIENT_ID")), llb.AddSecret("/run/secrets/KEYVAULT_NAME", llb.SecretID("KEYVAULT_NAME")), llb.AddSecret("/run/secrets/AZURE_CLIENT_SECRET", llb.SecretID("AZURE_CLIENT_SECRET")), diff --git a/frontend/mariner2/handle_rpm.go b/frontend/mariner2/handle_rpm.go index 6a152ac3..38847e16 100644 --- a/frontend/mariner2/handle_rpm.go +++ b/frontend/mariner2/handle_rpm.go @@ -2,7 +2,6 @@ package mariner2 import ( "context" - "encoding/json" "fmt" "path/filepath" "strings" @@ -11,10 +10,7 @@ import ( "github.com/Azure/dalec/frontend" "github.com/Azure/dalec/frontend/rpm" "github.com/moby/buildkit/client/llb" - "github.com/moby/buildkit/client/llb/sourceresolver" gwclient "github.com/moby/buildkit/frontend/gateway/client" - "github.com/moby/buildkit/identity" - "github.com/moby/buildkit/solver/pb" ocispecs "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -37,91 +33,6 @@ func tdnfCacheMountWithPrefix(prefix string) llb.RunOption { return llb.AddMount(filepath.Join(prefix, tdnfCacheDir), llb.Scratch(), llb.AsPersistentCacheDir(tdnfCacheName, llb.CacheMountLocked)) } -func hasSigner(t *dalec.Target) bool { - return t != nil && t.PackageConfig != nil && t.PackageConfig.Signer != nil && t.PackageConfig.Signer.Image != nil -} - -func compound(k, v string) string { - return fmt.Sprintf("%s:%s", k, v) -} - -func forwardToSigner(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, cfg *dalec.Signer, s llb.State) (llb.State, error) { - const ( - sourceKey = "source" - contextKey = "context" - targetKey = "target" - inputKey = "input" - resolveModeKey = "image.resolvemode" - containerImageConfigKey = "containerimage.config" - inputMetadataKey = "input-metadata" - - gatewayFrontend = "gateway.v0" - ) - - opts := client.BuildOpts().Opts - signer := llb.Image(cfg.Image.Ref) - id := identity.NewID() - - req := gwclient.SolveRequest{ - Frontend: gatewayFrontend, - FrontendOpt: make(map[string]string), - FrontendInputs: make(map[string]*pb.Definition), - } - - _, _, b, err := client.ResolveImageConfig(ctx, cfg.Image.Ref, sourceresolver.Opt{ - Platform: platform, - ImageOpt: &sourceresolver.ResolveImageOpt{ - ResolveMode: opts[resolveModeKey], - }, - }) - - withConfig, err := signer.WithImageConfig(b) - if err != nil { - return llb.Scratch(), err - } - - signerDef, err := withConfig.Marshal(ctx) - if err != nil { - return llb.Scratch(), err - } - signerPB := signerDef.ToPB() - - req.FrontendOpt[compound(contextKey, id)] = compound(inputKey, id) - req.FrontendInputs[id] = signerPB - req.FrontendOpt[sourceKey] = id - req.FrontendOpt[compound(contextKey, initialState)] = compound(inputKey, initialState) - req.FrontendOpt[targetKey] = "check" - req.FrontendInputs[contextKey] = signerPB - - meta := map[string][]byte{ - containerImageConfigKey: b, - } - metaDt, err := json.Marshal(meta) - if err != nil { - return llb.Scratch(), fmt.Errorf("error marshaling local frontend metadata: %w", err) - } - req.FrontendOpt[compound(inputMetadataKey, id)] = string(metaDt) - - stateDef, err := s.Marshal(ctx) - if err != nil { - return llb.Scratch(), err - } - - req.FrontendInputs[initialState] = stateDef.ToPB() - - res, err := client.Solve(ctx, req) - if err != nil { - return llb.Scratch(), err - } - - ref, err := res.SingleRef() - if err != nil { - return llb.Scratch(), err - } - - return ref.ToState() -} - func handleRPM(ctx context.Context, client gwclient.Client) (*gwclient.Result, error) { return frontend.BuildWithPlatform(ctx, client, func(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, spec *dalec.Spec, targetKey string) (gwclient.Reference, *dalec.DockerImageSpec, error) { if err := rpm.ValidateSpec(spec); err != nil { @@ -140,8 +51,8 @@ func handleRPM(ctx context.Context, client gwclient.Client) (*gwclient.Result, e } t := spec.Targets[targetKey] - if hasSigner(&t) { - signed, err := forwardToSigner(ctx, client, platform, t.PackageConfig.Signer, st) + if frontend.HasSigner(&t) { + signed, err := frontend.ForwardToSigner(ctx, client, platform, t.PackageConfig.Signer, st, "*.rpm") if err != nil { return nil, nil, err } diff --git a/frontend/request.go b/frontend/request.go index b4ecff9c..4d3ec9ac 100644 --- a/frontend/request.go +++ b/frontend/request.go @@ -2,6 +2,7 @@ package frontend import ( "context" + "fmt" "github.com/Azure/dalec" "github.com/goccy/go-yaml" @@ -9,7 +10,9 @@ import ( "github.com/moby/buildkit/frontend/dockerfile/parser" "github.com/moby/buildkit/frontend/dockerui" gwclient "github.com/moby/buildkit/frontend/gateway/client" + "github.com/moby/buildkit/identity" "github.com/moby/buildkit/solver/pb" + ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" ) @@ -116,3 +119,55 @@ func marshalDockerfile(ctx context.Context, dt []byte, opts ...llb.ConstraintsOp st := llb.Scratch().File(llb.Mkfile(dockerui.DefaultDockerfileName, 0600, dt), opts...) return st.Marshal(ctx) } + +func ForwardToSigner(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, cfg *dalec.Frontend, s llb.State, filePattern string) (llb.State, error) { + const ( + sourceKey = "source" + contextKey = "context" + inputKey = "input" + + gatewayFrontend = "gateway.v0" + ) + + opts := client.BuildOpts().Opts + id := identity.NewID() + + req, err := newSolveRequest(toFrontend(cfg)) + if err != nil { + return llb.Scratch(), err + } + + if req.FrontendInputs == nil { + req.FrontendInputs = make(map[string]*pb.Definition) + } + + stateDef, err := s.Marshal(ctx) + if err != nil { + return llb.Scratch(), err + } + + req.FrontendOpt[contextKey] = compound(inputKey, id) + req.FrontendInputs[id] = stateDef.ToPB() + req.FrontendOpt["dalec.target"] = opts["dalec.target"] + req.FrontendOpt["find.pattern"] = filePattern + + res, err := client.Solve(ctx, req) + if err != nil { + return llb.Scratch(), err + } + + ref, err := res.SingleRef() + if err != nil { + return llb.Scratch(), err + } + + return ref.ToState() +} + +func compound(k, v string) string { + return fmt.Sprintf("%s:%s", k, v) +} + +func HasSigner(t *dalec.Target) bool { + return t != nil && t.PackageConfig != nil && t.PackageConfig.Signer != nil && t.PackageConfig.Signer.Image != "" +} diff --git a/frontend/windows/handle_container.go b/frontend/windows/handle_container.go index 08bf58ea..c596b5be 100644 --- a/frontend/windows/handle_container.go +++ b/frontend/windows/handle_container.go @@ -54,6 +54,15 @@ func handleContainer(ctx context.Context, client gwclient.Client) (*gwclient.Res return nil, nil, fmt.Errorf("unable to build binary %w", err) } + if t, ok := spec.Targets[targetKey]; ok && frontend.HasSigner(&t) { + signed, err := frontend.ForwardToSigner(ctx, client, platform, t.PackageConfig.Signer, bin, "*.exe") + if err != nil { + return nil, nil, err + } + + bin = signed + } + baseImgName := getBaseOutputImage(spec, targetKey, defaultBaseImage) baseImage := llb.Image(baseImgName, llb.Platform(targetPlatform)) diff --git a/frontend/windows/handle_zip.go b/frontend/windows/handle_zip.go index a56f5ae1..35a23f22 100644 --- a/frontend/windows/handle_zip.go +++ b/frontend/windows/handle_zip.go @@ -43,6 +43,15 @@ func handleZip(ctx context.Context, client gwclient.Client) (*gwclient.Result, e return nil, nil, fmt.Errorf("unable to build binaries: %w", err) } + if t, ok := spec.Targets[targetKey]; ok && frontend.HasSigner(&t) { + signed, err := frontend.ForwardToSigner(ctx, client, platform, t.PackageConfig.Signer, bin, "*.exe") + if err != nil { + return nil, nil, err + } + + bin = signed + } + st := getZipLLB(worker, spec.Name, bin) if err != nil { return nil, nil, err diff --git a/spec.go b/spec.go index 9dd864bb..b45a1dac 100644 --- a/spec.go +++ b/spec.go @@ -411,12 +411,7 @@ type Target struct { // PackageConfig encapsulates the configuration for artifact targets type PackageConfig struct { // Signer is the configuration to use for signing packages - Signer *Signer `yaml:"signer,omitempty" json:"signer,omitempty"` -} - -// Signer encapsulates the configuration for an image that performs package signing -type Signer struct { - Image *SourceDockerImage `yaml:"image,omitempty" json:"image,omitempty"` + Signer *Frontend `yaml:"signer,omitempty" json:"signer,omitempty"` } // TestSpec is used to execute tests against a container with the package installed in it. diff --git a/test/helpers_test.go b/test/helpers_test.go index e4a0b069..25ecaef7 100644 --- a/test/helpers_test.go +++ b/test/helpers_test.go @@ -24,7 +24,8 @@ import ( ) const ( - phonyRef = "dalec/integration/frontend/phony" + phonyRef = "dalec/integration/frontend/phony" + phonySignerRef = "dalec/integration/signer/phony" ) func startTestSpan(ctx context.Context, t *testing.T) context.Context { diff --git a/test/main_test.go b/test/main_test.go index 8c618d19..0da8cf8e 100644 --- a/test/main_test.go +++ b/test/main_test.go @@ -76,6 +76,10 @@ func TestMain(m *testing.M) { panic(err) } + if err := testEnv.Load(ctx, phonySignerRef, fixtures.PhonySigner); err != nil { + panic(err) + } + return m.Run() } diff --git a/test/signer_test.go b/test/signer_test.go new file mode 100644 index 00000000..819beb0a --- /dev/null +++ b/test/signer_test.go @@ -0,0 +1,133 @@ +package test + +import ( + "bytes" + "context" + "slices" + "strings" + "testing" + + "github.com/Azure/dalec" + gwclient "github.com/moby/buildkit/frontend/gateway/client" + "github.com/moby/buildkit/frontend/subrequests/targets" +) + +// TestHandlerTargetForwarding tests that targets are forwarded to the correct frontend. +// We do this by registering a phony frontend and then forwarding a target to it and checking the outputs. +func TestSignerForwarding(t *testing.T) { + runTest := func(t *testing.T, f gwclient.BuildFunc) { + t.Helper() + ctx := startTestSpan(baseCtx, t) + testEnv.RunTest(ctx, t, f) + } + + t.Run("list targets", func(t *testing.T) { + t.Parallel() + runTest(t, func(ctx context.Context, gwc gwclient.Client) (*gwclient.Result, error) { + // Make sure phony is not in the list of targets since it shouldn't be registered in the base frontend. + ls := listTargets(ctx, t, gwc, &dalec.Spec{ + Targets: map[string]dalec.Target{ + // Note: This is not setting the frontend image, so it should use the default frontend. + "phony": {}, + }, + }) + + checkTargetExists(t, ls, "debug/resolve") + if slices.ContainsFunc(ls.Targets, func(tgt targets.Target) bool { + return strings.Contains(tgt.Name, "phony") + }) { + t.Fatal("found phony target") + } + + // Now make sure the forwarded target works. + spec := &dalec.Spec{ + Targets: map[string]dalec.Target{ + "phony": { + Frontend: &dalec.Frontend{ + Image: phonyRef, + }, + }, + }} + + // Make sure phony is in the list of targets since it should be registered in the forwarded frontend. + ls = listTargets(ctx, t, gwc, spec) + checkTargetExists(t, ls, "debug/resolve") + checkTargetExists(t, ls, "phony/check") + checkTargetExists(t, ls, "phony/debug/resolve") + return gwclient.NewResult(), nil + }) + }) + + t.Run("execute target", func(t *testing.T) { + t.Parallel() + runTest(t, func(ctx context.Context, gwc gwclient.Client) (*gwclient.Result, error) { + spec := &dalec.Spec{ + Targets: map[string]dalec.Target{ + "phony": { + Frontend: &dalec.Frontend{ + Image: phonyRef, + }, + }, + }} + + sr := newSolveRequest(withSpec(ctx, t, spec), withBuildTarget("phony/check")) + res, err := gwc.Solve(ctx, sr) + if err != nil { + t.Fatal(err) + } + dt := readFile(ctx, t, "hello", res) + expect := []byte("phony hello") + if !bytes.Equal(dt, expect) { + t.Fatalf("expected %q, got %q", expect, string(dt)) + } + + // In this case we want to make sure that any targets that are registered by the frontend are namespaced by our target name prefix. + // This is to ensure that the frontend is not overwriting any other targets. + // Technically I suppose the target in the user-supplied spec could technically interfere with the base frontend, but that's not really a concern. + // e.g. if a user-supplied target was called "debug" it could overwrite the "debug/resolve" target in the base frontend. + + sr = newSolveRequest(withSpec(ctx, t, spec), withBuildTarget("debug/resolve")) + res, err = gwc.Solve(ctx, sr) + if err != nil { + return nil, err + } + + // The builtin debug/resolve target adds the resolved spec to /spec.yml, so check that its there. + statFile(ctx, t, "spec.yml", res) + + sr = newSolveRequest(withSpec(ctx, t, spec), withBuildTarget("phony/debug/resolve")) + res, err = gwc.Solve(ctx, sr) + if err != nil { + return nil, err + } + + // The phony/debug/resolve target creates a file with the contents "phony resolve". + // Check that its there to ensure we got the expected target. + checkFile(ctx, t, "resolve", res, []byte("phony resolve")) + return gwclient.NewResult(), nil + }) + }) + + t.Run("target not found", func(t *testing.T) { + t.Parallel() + runTest(t, func(ctx context.Context, gwc gwclient.Client) (*gwclient.Result, error) { + spec := &dalec.Spec{ + Targets: map[string]dalec.Target{ + "phony": { + Frontend: &dalec.Frontend{ + Image: phonyRef, + }, + }, + }, + } + sr := newSolveRequest(withBuildTarget("phony/does-not-exist"), withSpec(ctx, t, spec)) + + _, err := gwc.Solve(ctx, sr) + expect := "no such handler for target" + if err == nil || !strings.Contains(err.Error(), expect) { + t.Fatalf("expected error %q, got %v", expect, err) + } + return gwclient.NewResult(), nil + }) + }) +} From e31d6f24c5e28d44a59b4657c304fd96753da014 Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Fri, 19 Apr 2024 14:34:57 -0400 Subject: [PATCH 11/28] Set up test for signer forwarding Signed-off-by: Peter Engelbert --- cmd/signer/esrp.yml | 32 ------ frontend/request.go | 23 +++- test/fixtures/signer/main.go | 19 +++- test/signer_test.go | 214 ++++++++++++++++++++++------------- 4 files changed, 173 insertions(+), 115 deletions(-) delete mode 100644 cmd/signer/esrp.yml diff --git a/cmd/signer/esrp.yml b/cmd/signer/esrp.yml deleted file mode 100644 index 371ea42c..00000000 --- a/cmd/signer/esrp.yml +++ /dev/null @@ -1,32 +0,0 @@ -# syntax=ghcr.io/azure/dalec/frontend:latest - -targets: - mariner2: - dependencies: - runtime: - -# https://dev.azure.com/AzureContainerUpstream/Support/_search?action=contents&text=xsignextension&type=code&lp=code-Project&filters=ProjectFilters%7BSupport%7DRepositoryFilters%7Bupstream-ado-image-builder%7D&pageSize=25&result=DefaultCollection/Support/upstream-ado-image-builder/GBmain//image-builder/mariner-preprovision.sh -sources: - xsignextension: - path: /usr - image: - ref: mcr.microsoft.com/azure-cli:cbl-mariner2.0 - cmd: - steps: - - command: | - az config set extension.use_dynamic_install=yes_without_prompt - az login --identity - az storage blob download \ - --account-name upstreamaibartifacts \ - --container-name az-sign-extension \ - --name "versions/xsignextension-${XSIGNEXTENSION_VERSION}-py3-none-any.whl" \ - --file "xsignextension-${XSIGNEXTENSION_VERSION}-py3-none-any.whl" \ - --auth-mode login - az extension add \ - -s xsignextension-${XSIGNEXTENSION_VERSION}-py3-none-any.whl \ - -y --system --debug - -build: - - - diff --git a/frontend/request.go b/frontend/request.go index 4d3ec9ac..47ac69bb 100644 --- a/frontend/request.go +++ b/frontend/request.go @@ -137,9 +137,28 @@ func ForwardToSigner(ctx context.Context, client gwclient.Client, platform *ocis return llb.Scratch(), err } - if req.FrontendInputs == nil { - req.FrontendInputs = make(map[string]*pb.Definition) + for k, v := range opts { + if k == "source" || k == "cmdline" { + continue + } + req.FrontendOpt[k] = v + } + + inputs, err := client.Inputs(ctx) + if err != nil { + return llb.Scratch(), err + } + + m := make(map[string]*pb.Definition) + + for k, st := range inputs { + def, err := st.Marshal(ctx) + if err != nil { + return llb.Scratch(), err + } + m[k] = def.ToPB() } + req.FrontendInputs = m stateDef, err := s.Marshal(ctx) if err != nil { diff --git a/test/fixtures/signer/main.go b/test/fixtures/signer/main.go index 96a987b7..51ccdd1e 100644 --- a/test/fixtures/signer/main.go +++ b/test/fixtures/signer/main.go @@ -6,7 +6,9 @@ import ( "encoding/json" "fmt" "os" + "strings" + "github.com/Azure/dalec/frontend" "github.com/moby/buildkit/client/llb" gwclient "github.com/moby/buildkit/frontend/gateway/client" "github.com/moby/buildkit/frontend/gateway/grpcclient" @@ -93,14 +95,23 @@ func main() { HashType: "sha256", } - _, ok := inputs["initialstate"] + cc, ok := c.(frontend.CurrentFrontend) if !ok { - return nil, fmt.Errorf("no artifact state provided to signer") + return nil, fmt.Errorf("cast to currentFrontend failed") } - _, ok = inputs["context"] + basePtr, err := cc.CurrentFrontend() + if err != nil || basePtr == nil { + if err == nil { + err = fmt.Errorf("base frontend ptr was nil") + } + return nil, err + } + + inputId := strings.TrimPrefix(bopts["context"], "input:") + _, ok = inputs[inputId] if !ok { - return nil, fmt.Errorf("no base signing image provided") + return nil, fmt.Errorf("no artifact state provided to signer") } configBytes, err := json.Marshal(&config) diff --git a/test/signer_test.go b/test/signer_test.go index 819beb0a..cdb1a729 100644 --- a/test/signer_test.go +++ b/test/signer_test.go @@ -1,15 +1,14 @@ package test import ( - "bytes" "context" - "slices" + "fmt" "strings" "testing" "github.com/Azure/dalec" + "github.com/moby/buildkit/client/llb" gwclient "github.com/moby/buildkit/frontend/gateway/client" - "github.com/moby/buildkit/frontend/subrequests/targets" ) // TestHandlerTargetForwarding tests that targets are forwarded to the correct frontend. @@ -21,113 +20,174 @@ func TestSignerForwarding(t *testing.T) { testEnv.RunTest(ctx, t, f) } - t.Run("list targets", func(t *testing.T) { + t.Run("test mariner2 signing", func(t *testing.T) { t.Parallel() runTest(t, func(ctx context.Context, gwc gwclient.Client) (*gwclient.Result, error) { - // Make sure phony is not in the list of targets since it shouldn't be registered in the base frontend. - ls := listTargets(ctx, t, gwc, &dalec.Spec{ + spec := dalec.Spec{ + Name: "foo", + Version: "v0.0.1", + Description: "foo bar baz", + Website: "https://foo.bar.baz", + Revision: "1", Targets: map[string]dalec.Target{ - // Note: This is not setting the frontend image, so it should use the default frontend. - "phony": {}, + "mariner2": { + PackageConfig: &dalec.PackageConfig{ + Signer: &dalec.Frontend{ + Image: phonySignerRef, + }, + }, + }, }, - }) - - checkTargetExists(t, ls, "debug/resolve") - if slices.ContainsFunc(ls.Targets, func(tgt targets.Target) bool { - return strings.Contains(tgt.Name, "phony") - }) { - t.Fatal("found phony target") - } - - // Now make sure the forwarded target works. - spec := &dalec.Spec{ - Targets: map[string]dalec.Target{ - "phony": { - Frontend: &dalec.Frontend{ - Image: phonyRef, + Sources: map[string]dalec.Source{ + "foo": { + Inline: &dalec.SourceInline{ + File: &dalec.SourceInlineFile{ + Contents: "#!/usr/bin/env bash\necho \"hello, world!\"\n", + }, }, }, - }} - - // Make sure phony is in the list of targets since it should be registered in the forwarded frontend. - ls = listTargets(ctx, t, gwc, spec) - checkTargetExists(t, ls, "debug/resolve") - checkTargetExists(t, ls, "phony/check") - checkTargetExists(t, ls, "phony/debug/resolve") - return gwclient.NewResult(), nil - }) - }) - - t.Run("execute target", func(t *testing.T) { - t.Parallel() - runTest(t, func(ctx context.Context, gwc gwclient.Client) (*gwclient.Result, error) { - spec := &dalec.Spec{ - Targets: map[string]dalec.Target{ - "phony": { - Frontend: &dalec.Frontend{ - Image: phonyRef, + }, + Build: dalec.ArtifactBuild{ + Steps: []dalec.BuildStep{ + { + Command: "/bin/true", }, }, - }} + }, + Artifacts: dalec.Artifacts{ + Binaries: map[string]dalec.ArtifactConfig{ + "foo": {}, + }, + }, + } - sr := newSolveRequest(withSpec(ctx, t, spec), withBuildTarget("phony/check")) + sr := newSolveRequest(withSpec(ctx, t, &spec), withBuildTarget("mariner2/rpm")) res, err := gwc.Solve(ctx, sr) if err != nil { t.Fatal(err) } - dt := readFile(ctx, t, "hello", res) - expect := []byte("phony hello") - if !bytes.Equal(dt, expect) { - t.Fatalf("expected %q, got %q", expect, string(dt)) - } - // In this case we want to make sure that any targets that are registered by the frontend are namespaced by our target name prefix. - // This is to ensure that the frontend is not overwriting any other targets. - // Technically I suppose the target in the user-supplied spec could technically interfere with the base frontend, but that's not really a concern. - // e.g. if a user-supplied target was called "debug" it could overwrite the "debug/resolve" target in the base frontend. + tgt := readFile(ctx, t, "/target", res) + cfg := readFile(ctx, t, "/config.json", res) - sr = newSolveRequest(withSpec(ctx, t, spec), withBuildTarget("debug/resolve")) - res, err = gwc.Solve(ctx, sr) - if err != nil { - return nil, err + if string(tgt) != "mariner2" { + t.Fatal(fmt.Errorf("target incorrect; either not sent to signer or not received back from signer")) } - // The builtin debug/resolve target adds the resolved spec to /spec.yml, so check that its there. - statFile(ctx, t, "spec.yml", res) - - sr = newSolveRequest(withSpec(ctx, t, spec), withBuildTarget("phony/debug/resolve")) - res, err = gwc.Solve(ctx, sr) - if err != nil { - return nil, err + if !strings.Contains(string(cfg), "LinuxSign") { + t.Fatal(fmt.Errorf("configuration incorrect")) } - // The phony/debug/resolve target creates a file with the contents "phony resolve". - // Check that its there to ensure we got the expected target. - checkFile(ctx, t, "resolve", res, []byte("phony resolve")) return gwclient.NewResult(), nil }) }) - t.Run("target not found", func(t *testing.T) { + t.Run("test windows signing", func(t *testing.T) { t.Parallel() runTest(t, func(ctx context.Context, gwc gwclient.Client) (*gwclient.Result, error) { - spec := &dalec.Spec{ + spec := fillMetadata("foo", &dalec.Spec{ Targets: map[string]dalec.Target{ - "phony": { - Frontend: &dalec.Frontend{ - Image: phonyRef, + "windowscross": { + PackageConfig: &dalec.PackageConfig{ + Signer: &dalec.Frontend{ + Image: phonySignerRef, + }, + }, + }, + }, + Sources: map[string]dalec.Source{ + "foo": { + Inline: &dalec.SourceInline{ + File: &dalec.SourceInlineFile{ + Contents: "#!/usr/bin/env bash\necho \"hello, world!\"\n", + }, + }, + }, + }, + Build: dalec.ArtifactBuild{ + Steps: []dalec.BuildStep{ + { + Command: "/bin/true", }, }, }, + Artifacts: dalec.Artifacts{ + Binaries: map[string]dalec.ArtifactConfig{ + "foo": {}, + }, + }, + }) + + zipperSpec := fillMetadata("bar", &dalec.Spec{ + Dependencies: &dalec.PackageDependencies{ + Runtime: map[string][]string{ + "unzip": {}, + }, + }, + }) + + sr := newSolveRequest(withSpec(ctx, t, zipperSpec), withBuildTarget("mariner2/container")) + zipper := reqToState(ctx, gwc, sr, t) + + sr = newSolveRequest(withSpec(ctx, t, spec), withBuildTarget("windowscross/zip")) + st := reqToState(ctx, gwc, sr, t) + + st = zipper.Run(llb.Args([]string{"bash", "-c", `for f in ./*.zip; do unzip "$f"; done`}), llb.Dir("/tmp/mnt")). + AddMount("/tmp/mnt", st) + + def, err := st.Marshal(ctx) + if err != nil { + t.Fatal(err) } - sr := newSolveRequest(withBuildTarget("phony/does-not-exist"), withSpec(ctx, t, spec)) - _, err := gwc.Solve(ctx, sr) - expect := "no such handler for target" - if err == nil || !strings.Contains(err.Error(), expect) { - t.Fatalf("expected error %q, got %v", expect, err) + res, err := gwc.Solve(ctx, gwclient.SolveRequest{ + Definition: def.ToPB(), + }) + + tgt := readFile(ctx, t, "/target", res) + cfg := readFile(ctx, t, "/config.json", res) + + if string(tgt) != "windowscross" { + t.Fatal(fmt.Errorf("target incorrect; either not sent to signer or not received back from signer")) + } + + if !strings.Contains(string(cfg), "NotaryCoseSign") { + t.Fatal(fmt.Errorf("configuration incorrect")) } + return gwclient.NewResult(), nil }) }) } + +func reqToState(ctx context.Context, gwc gwclient.Client, sr gwclient.SolveRequest, t *testing.T) llb.State { + res, err := gwc.Solve(ctx, sr) + if err != nil { + t.Fatal(err) + } + + ref, err := res.SingleRef() + if err != nil { + t.Fatal(err) + } + + st, err := ref.ToState() + if err != nil { + t.Fatal(err) + } + + return st +} + +func fillMetadata(fakename string, s *dalec.Spec) *dalec.Spec { + s.Name = "bar" + s.Version = "v0.0.1" + s.Description = "foo bar baz" + s.Website = "https://foo.bar.baz" + s.Revision = "1" + s.License = "MIT" + s.Vendor = "nothing" + s.Packager = "Bill Spummins" + + return s +} From fdb5098b74684ae8555abaf90e5f5278d6296a51 Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Mon, 29 Apr 2024 10:14:37 -0400 Subject: [PATCH 12/28] Use "context" as key for frontend input Signed-off-by: Peter Engelbert --- frontend/request.go | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/frontend/request.go b/frontend/request.go index 47ac69bb..28845557 100644 --- a/frontend/request.go +++ b/frontend/request.go @@ -10,7 +10,6 @@ import ( "github.com/moby/buildkit/frontend/dockerfile/parser" "github.com/moby/buildkit/frontend/dockerui" gwclient "github.com/moby/buildkit/frontend/gateway/client" - "github.com/moby/buildkit/identity" "github.com/moby/buildkit/solver/pb" ocispecs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" @@ -130,7 +129,6 @@ func ForwardToSigner(ctx context.Context, client gwclient.Client, platform *ocis ) opts := client.BuildOpts().Opts - id := identity.NewID() req, err := newSolveRequest(toFrontend(cfg)) if err != nil { @@ -165,8 +163,8 @@ func ForwardToSigner(ctx context.Context, client gwclient.Client, platform *ocis return llb.Scratch(), err } - req.FrontendOpt[contextKey] = compound(inputKey, id) - req.FrontendInputs[id] = stateDef.ToPB() + req.FrontendOpt[contextKey] = compound(inputKey, contextKey) + req.FrontendInputs[contextKey] = stateDef.ToPB() req.FrontendOpt["dalec.target"] = opts["dalec.target"] req.FrontendOpt["find.pattern"] = filePattern From ceb78f5ca3e704e09bfad676163a473ddd66a999 Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Mon, 29 Apr 2024 11:29:28 -0400 Subject: [PATCH 13/28] Remove holdovers from debugging I had been using debugging symbols to fix some issues in the integration tests. Signed-off-by: Peter Engelbert --- test/fixtures/phony.go | 2 +- test/fixtures/signer.go | 2 +- test/testenv/buildx.go | 2 -- test/testenv/builld.go | 6 ++---- 4 files changed, 4 insertions(+), 8 deletions(-) diff --git a/test/fixtures/phony.go b/test/fixtures/phony.go index 02c3d737..b7dadbb2 100644 --- a/test/fixtures/phony.go +++ b/test/fixtures/phony.go @@ -30,7 +30,7 @@ func PhonyFrontend(ctx context.Context, gwc gwclient.Client) (*gwclient.Result, st := llb.Image("golang:1.21", llb.WithMetaResolver(gwc)). Run( - llb.Args([]string{"go", "build", "-gcflags=all=-N -l", "-o=/build/out", "./test/fixtures/phony"}), + llb.Args([]string{"go", "build", "-o=/build/out", "./test/fixtures/phony"}), llb.AddEnv("CGO_ENABLED", "0"), goModCache, goBuildCache, diff --git a/test/fixtures/signer.go b/test/fixtures/signer.go index 83f2ef95..677440f3 100644 --- a/test/fixtures/signer.go +++ b/test/fixtures/signer.go @@ -25,7 +25,7 @@ func PhonySigner(ctx context.Context, gwc gwclient.Client) (*gwclient.Result, er st := llb.Image("golang:1.21", llb.WithMetaResolver(gwc)). Run( - llb.Args([]string{"go", "build", "-gcflags=all=-N -l", "-o=/build/out", "./test/fixtures/signer"}), + llb.Args([]string{"go", "build", "-o=/build/out", "./test/fixtures/signer"}), llb.AddEnv("CGO_ENABLED", "0"), goModCache, goBuildCache, diff --git a/test/testenv/buildx.go b/test/testenv/buildx.go index 7f3d0a8d..63451da6 100644 --- a/test/testenv/buildx.go +++ b/test/testenv/buildx.go @@ -11,7 +11,6 @@ import ( "path/filepath" "sync" "testing" - "time" "github.com/cpuguy83/dockercfg" "github.com/cpuguy83/go-docker" @@ -190,7 +189,6 @@ func (b *BuildxEnv) bootstrap(ctx context.Context) (retErr error) { } c, err := client.New(ctx, "", client.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) { - conn2.SetDeadline(time.Now().Add(time.Hour * 24)) return conn2, nil })) if err != nil { diff --git a/test/testenv/builld.go b/test/testenv/builld.go index 223c5898..a548ce27 100644 --- a/test/testenv/builld.go +++ b/test/testenv/builld.go @@ -46,10 +46,8 @@ func buildBaseFrontend(ctx context.Context, c gwclient.Client) (*gwclient.Result defPB := def.ToPB() return c.Solve(ctx, gwclient.SolveRequest{ - Frontend: "dockerfile.v0", - FrontendOpt: map[string]string{ - "build-arg:GOFLAGS": "\"-gcflags=all=-N -l\"", - }, + Frontend: "dockerfile.v0", + FrontendOpt: map[string]string{}, FrontendInputs: map[string]*pb.Definition{ dockerui.DefaultLocalNameContext: defPB, dockerui.DefaultLocalNameDockerfile: dockerfileDef.ToPB(), From 6c494ead39b8601b7b0d1d9bb1a4e712d6e34c02 Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Mon, 29 Apr 2024 13:00:54 -0400 Subject: [PATCH 14/28] Slight changes to please the linter Signed-off-by: Peter Engelbert --- docs/spec.schema.json | 15 +++++++++++++++ frontend/mariner2/handle_rpm.go | 2 -- frontend/request.go | 2 -- test/signer_test.go | 3 +++ 4 files changed, 18 insertions(+), 4 deletions(-) diff --git a/docs/spec.schema.json b/docs/spec.schema.json index 93ea97b9..cfad86f3 100644 --- a/docs/spec.schema.json +++ b/docs/spec.schema.json @@ -358,6 +358,17 @@ "type": "object", "description": "ImageConfig is the configuration for the output image." }, + "PackageConfig": { + "properties": { + "signer": { + "$ref": "#/$defs/Frontend", + "description": "Signer is the configuration to use for signing packages" + } + }, + "additionalProperties": false, + "type": "object", + "description": "PackageConfig encapsulates the configuration for artifact targets" + }, "PackageDependencies": { "properties": { "build": { @@ -858,6 +869,10 @@ }, "type": "array", "description": "Tests are the list of tests to run which are specific to the target.\nTests are appended to the list of tests in the main [Spec]" + }, + "package_config": { + "$ref": "#/$defs/PackageConfig", + "description": "PackageConfig is the configuration to use for artifact targets, such as\nrpms, debs, or zip files containing Windows binaries" } }, "additionalProperties": false, diff --git a/frontend/mariner2/handle_rpm.go b/frontend/mariner2/handle_rpm.go index 38847e16..3b1ac746 100644 --- a/frontend/mariner2/handle_rpm.go +++ b/frontend/mariner2/handle_rpm.go @@ -17,8 +17,6 @@ import ( const ( marinerRef = "mcr.microsoft.com/cbl-mariner/base/core:2.0" - initialState = "initialstate" - tdnfCacheDir = "/var/cache/tdnf" tdnfCacheName = "mariner2-tdnf-cache" ) diff --git a/frontend/request.go b/frontend/request.go index 28845557..521bb644 100644 --- a/frontend/request.go +++ b/frontend/request.go @@ -124,8 +124,6 @@ func ForwardToSigner(ctx context.Context, client gwclient.Client, platform *ocis sourceKey = "source" contextKey = "context" inputKey = "input" - - gatewayFrontend = "gateway.v0" ) opts := client.BuildOpts().Opts diff --git a/test/signer_test.go b/test/signer_test.go index cdb1a729..1379555e 100644 --- a/test/signer_test.go +++ b/test/signer_test.go @@ -143,6 +143,9 @@ func TestSignerForwarding(t *testing.T) { res, err := gwc.Solve(ctx, gwclient.SolveRequest{ Definition: def.ToPB(), }) + if err != nil { + t.Fatal(err) + } tgt := readFile(ctx, t, "/target", res) cfg := readFile(ctx, t, "/config.json", res) From 467148036f2c800b5feea6fbb55dfba28a94faac Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Tue, 30 Apr 2024 09:43:42 -0400 Subject: [PATCH 15/28] Add changes to signing image before removal Signed-off-by: Peter Engelbert --- cmd/signer/main.go | 54 +++++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/cmd/signer/main.go b/cmd/signer/main.go index c8bcb430..2c7734ae 100644 --- a/cmd/signer/main.go +++ b/cmd/signer/main.go @@ -79,17 +79,25 @@ func main() { signOp := "" params := []ParameterKV{} + findPattern := llb.Scratch() switch target { - case "windowscross", "windows", "mariner2": + case "windowscross", "windows": signOp = windowsSignOp params = append(params, ParameterKV{ ParameterName: "CoseFlags", ParameterValue: "chainunprotected", }) - - default: + findPattern = base. + Run(llb.Args([]string{"bash", "-c", + `find /artifacts -type f -regextype posix-egrep regex '.*\.(exe|ps1)$' -print0 > /tmp/output/artifacts_list`})). + AddMount("/tmp/output/", llb.Scratch()) + case "mariner2", "jammy": signOp = linuxSignOp + findPattern = base.Run(llb.Args([]string{"bash", "-c", + `find /artifacts -type f -regextype posix-egrep regex '.*\.(rpm)$' -print0 > /tmp/output/artifacts_list`})). + AddMount("/tmp/output/", llb.Scratch()) + } config := Config{ @@ -117,21 +125,23 @@ func main() { return nil, err } - findPattern := bopts["find.pattern"] - // In order for this signing image to work, we need base = base.File(llb.Mkfile("/script.sh", 0o777, []byte(` #!/usr/bin/env bash set -exuo pipefail - export KEYVAULT_NAME=$(< /run/secrets/KEYVAULT_NAME) - export AZURE_CLIENT_SECRET=$(< /run/secrets/AZURE_CLIENT_SECRET) - export AZURE_CLIENT_ID=$(< /run/secrets/AZURE_CLIENT_ID) - export ESRP_KEYCODE=$(< /run/secrets/ESRP_KEYCODE) - export AZURE_TENANT_ID=$(< /run/secrets/AZURE_TENANT_ID) - export BUILDER_EMAIL=$(< /run/secrets/BUILDER_EMAIL) - envsubst < /config_template.json > /config.json - - az login --service-principal -u "$AZURE_CLIENT_ID" -p "$AZURE_CLIENT_SECRET" --tenant "$AZURE_TENANT_ID" --allow-no-subscriptions + : "${FIND_PATTERN}" + + ( + # avoid leaking secrets + set +x + envsubst < /config_template.json > /config.json + az login --service-principal \ + -username "$AZURE_CLIENT_ID" \ + -password "$AZURE_CLIENT_SECRET" \ + --tenant "$AZURE_TENANT_ID" \ + --allow-no-subscriptions + ) + readarray -d '' artifacts < <(find /artifacts -type f ${FIND_PATTERN:+-name "$FIND_PATTERN"} -print0) for f in "${artifacts[@]}"; do az xsign sign-file --file-name "$f" --config-file /config.json @@ -142,12 +152,12 @@ func main() { output := base.Run(llb.Args([]string{"bash", "/script.sh"}), llb.AddEnv("FIND_PATTERN", findPattern), - llb.AddSecret("/run/secrets/AZURE_CLIENT_ID", llb.SecretID("AZURE_CLIENT_ID")), - llb.AddSecret("/run/secrets/KEYVAULT_NAME", llb.SecretID("KEYVAULT_NAME")), - llb.AddSecret("/run/secrets/AZURE_CLIENT_SECRET", llb.SecretID("AZURE_CLIENT_SECRET")), - llb.AddSecret("/run/secrets/AZURE_TENANT_ID", llb.SecretID("AZURE_TENANT_ID")), - llb.AddSecret("/run/secrets/ESRP_KEYCODE", llb.SecretID("ESRP_KEYCODE")), - llb.AddSecret("/run/secrets/BUILDER_EMAIL", llb.SecretID("BUILDER_EMAIL")), + secretToEnv("AZURE_CLIENT_ID"), + secretToEnv("KEYVAULT_NAME"), + secretToEnv("AZURE_CLIENT_SECRET"), + secretToEnv("AZURE_TENANT_ID"), + secretToEnv("ESRP_KEYCODE"), + secretToEnv("BUILDER_EMAIL"), ).AddMount("/artifacts", artifacts) def, err := output.Marshal(ctx) @@ -163,3 +173,7 @@ func main() { os.Exit(137) } } + +func secretToEnv(secretName string) llb.RunOption { + return llb.AddSecret(secretName, llb.SecretID(secretName), llb.SecretAsEnv(true)) +} From 4a654fee733d365c30a7371d6240289a11eb2a68 Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Tue, 30 Apr 2024 09:44:38 -0400 Subject: [PATCH 16/28] Remove signer image from repo Signed-off-by: Peter Engelbert --- cmd/signer/Dockerfile | 34 -------- cmd/signer/main.go | 179 ------------------------------------------ 2 files changed, 213 deletions(-) delete mode 100644 cmd/signer/Dockerfile delete mode 100644 cmd/signer/main.go diff --git a/cmd/signer/Dockerfile b/cmd/signer/Dockerfile deleted file mode 100644 index 03455260..00000000 --- a/cmd/signer/Dockerfile +++ /dev/null @@ -1,34 +0,0 @@ -# syntax=docker/dockerfile:1.6 - -FROM scratch as creds - -FROM --platform=${BUILDPLATFORM} mcr.microsoft.com/oss/go/microsoft/golang:1.21@sha256:f5fc8bf2873d2fa538b2d87e6e8f9bc6d05a3c419a840147b25f6386855192e9 AS signer-builder -WORKDIR /build -COPY . . -ENV CGO_ENABLED=0 -ARG TARGETARCH TARGETOS GOFLAGS=-trimpath - -ENV GOOS=${TARGETOS} GOARCH=${TARGETARCH} GOFLAGS=${GOFLAGS} -RUN \ - --mount=type=cache,target=/go/pkg/mod \ - --mount=type=cache,target=/root/.cache/go-build \ - go build -o /signer ./cmd/signer - -FROM mcr.microsoft.com/azure-cli:cbl-mariner2.0 as az-base -WORKDIR /root -COPY ./tmp/xsignextension-0.46-py3-none-any.whl . -ARG XSIGNEXTENSION_VERSION=0.46 -RUN < /tmp/output/artifacts_list`})). - AddMount("/tmp/output/", llb.Scratch()) - case "mariner2", "jammy": - signOp = linuxSignOp - findPattern = base.Run(llb.Args([]string{"bash", "-c", - `find /artifacts -type f -regextype posix-egrep regex '.*\.(rpm)$' -print0 > /tmp/output/artifacts_list`})). - AddMount("/tmp/output/", llb.Scratch()) - - } - - config := Config{ - ClientId: "${AZURE_CLIENT_ID}", - GatewayAPI: "https://api.esrp.microsoft.com", - RequestSigningCert: map[string]string{ - "subject": "esrp-prss", - "vaultName": "${KEYVAULT_NAME}", - }, - DRIEmail: []string{"${BUILDER_EMAIL}"}, - SigningOperations: []SigningOperation{ - { - KeyCode: "${ESRP_KEYCODE}", - OperationSetCode: signOp, - Parameters: params, - ToolName: "sign", - ToolVersion: "1.0", - }, - }, - HashType: "sha256", - } - - configBytes, err := json.Marshal(&config) - if err != nil { - return nil, err - } - - // In order for this signing image to work, we need - base = base.File(llb.Mkfile("/script.sh", 0o777, []byte(` - #!/usr/bin/env bash - set -exuo pipefail - : "${FIND_PATTERN}" - - ( - # avoid leaking secrets - set +x - envsubst < /config_template.json > /config.json - az login --service-principal \ - -username "$AZURE_CLIENT_ID" \ - -password "$AZURE_CLIENT_SECRET" \ - --tenant "$AZURE_TENANT_ID" \ - --allow-no-subscriptions - ) - - readarray -d '' artifacts < <(find /artifacts -type f ${FIND_PATTERN:+-name "$FIND_PATTERN"} -print0) - for f in "${artifacts[@]}"; do - az xsign sign-file --file-name "$f" --config-file /config.json - done - `))) - - base = base.File(llb.Mkfile("/config_template.json", 0o600, configBytes)) - - output := base.Run(llb.Args([]string{"bash", "/script.sh"}), - llb.AddEnv("FIND_PATTERN", findPattern), - secretToEnv("AZURE_CLIENT_ID"), - secretToEnv("KEYVAULT_NAME"), - secretToEnv("AZURE_CLIENT_SECRET"), - secretToEnv("AZURE_TENANT_ID"), - secretToEnv("ESRP_KEYCODE"), - secretToEnv("BUILDER_EMAIL"), - ).AddMount("/artifacts", artifacts) - - def, err := output.Marshal(ctx) - if err != nil { - return nil, err - } - - return c.Solve(ctx, gwclient.SolveRequest{ - Definition: def.ToPB(), - }) - }); err != nil { - bklog.L.WithError(err).Fatal("error running frontend") - os.Exit(137) - } -} - -func secretToEnv(secretName string) llb.RunOption { - return llb.AddSecret(secretName, llb.SecretID(secretName), llb.SecretAsEnv(true)) -} From 68dd06570395f9b3653993dd383e963df59b47e4 Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Tue, 30 Apr 2024 10:08:40 -0400 Subject: [PATCH 17/28] Add documentation on signing. Signed-off-by: Peter Engelbert --- docs/signing.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 docs/signing.md diff --git a/docs/signing.md b/docs/signing.md new file mode 100644 index 00000000..648e1480 --- /dev/null +++ b/docs/signing.md @@ -0,0 +1,32 @@ +# Signing Packages + +Packages can be automatically signed using Dalec. To do this, you will +need to provide a signing frontend. There is an +[example](test/signer/main.go) in the test code. Once that signing image +has been built and tagged, the following can be added to the spec to trigger +the signing operation: + +```yaml +targets: # Distro specific build requirements + mariner2: + package_config: + signer: + image: "ref/to/signing:image" + cmdline: "/signer" +``` + +This will send the artifacts (`.rpm`, `.deb`, or `.exe`) to the +signing frontend as the build context. + +The contract between dalec and the signing image is: + +1. The signing image will contain both the signing frontend, and any +additional tooling necessary to carry out the signing operation. +1. The `llb.State` corresponding the artifacts to be signed will be +provided as the build context. +1. Dalec will provide the value of `dalec.target` to the frontend as a +`FrontendOpt`. In the above example, this will be `mariner2`. +1. The response from the frontend will contain an `llb.State` that is +identical to the input `llb.State` in every way *except* that the +desired artifacts will be signed. + From 9fd9ee060d10b68c15920abe200d66d03700cc76 Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Tue, 30 Apr 2024 11:12:09 -0400 Subject: [PATCH 18/28] Delegate search for artifacts to signer Instead of telling the frontend how to search for artifacts to sign, only provide `dalec.target` and delegate responsibility for finding the artifacts to the signing side. The target should be sufficient to determine what to do. Signed-off-by: Peter Engelbert --- frontend/mariner2/handle_rpm.go | 2 +- frontend/request.go | 3 +-- frontend/windows/handle_container.go | 2 +- frontend/windows/handle_zip.go | 2 +- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/frontend/mariner2/handle_rpm.go b/frontend/mariner2/handle_rpm.go index 3b1ac746..fb16fba4 100644 --- a/frontend/mariner2/handle_rpm.go +++ b/frontend/mariner2/handle_rpm.go @@ -50,7 +50,7 @@ func handleRPM(ctx context.Context, client gwclient.Client) (*gwclient.Result, e t := spec.Targets[targetKey] if frontend.HasSigner(&t) { - signed, err := frontend.ForwardToSigner(ctx, client, platform, t.PackageConfig.Signer, st, "*.rpm") + signed, err := frontend.ForwardToSigner(ctx, client, platform, t.PackageConfig.Signer, st) if err != nil { return nil, nil, err } diff --git a/frontend/request.go b/frontend/request.go index 521bb644..b453fac9 100644 --- a/frontend/request.go +++ b/frontend/request.go @@ -119,7 +119,7 @@ func marshalDockerfile(ctx context.Context, dt []byte, opts ...llb.ConstraintsOp return st.Marshal(ctx) } -func ForwardToSigner(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, cfg *dalec.Frontend, s llb.State, filePattern string) (llb.State, error) { +func ForwardToSigner(ctx context.Context, client gwclient.Client, platform *ocispecs.Platform, cfg *dalec.Frontend, s llb.State) (llb.State, error) { const ( sourceKey = "source" contextKey = "context" @@ -164,7 +164,6 @@ func ForwardToSigner(ctx context.Context, client gwclient.Client, platform *ocis req.FrontendOpt[contextKey] = compound(inputKey, contextKey) req.FrontendInputs[contextKey] = stateDef.ToPB() req.FrontendOpt["dalec.target"] = opts["dalec.target"] - req.FrontendOpt["find.pattern"] = filePattern res, err := client.Solve(ctx, req) if err != nil { diff --git a/frontend/windows/handle_container.go b/frontend/windows/handle_container.go index c596b5be..dfc2dd62 100644 --- a/frontend/windows/handle_container.go +++ b/frontend/windows/handle_container.go @@ -55,7 +55,7 @@ func handleContainer(ctx context.Context, client gwclient.Client) (*gwclient.Res } if t, ok := spec.Targets[targetKey]; ok && frontend.HasSigner(&t) { - signed, err := frontend.ForwardToSigner(ctx, client, platform, t.PackageConfig.Signer, bin, "*.exe") + signed, err := frontend.ForwardToSigner(ctx, client, platform, t.PackageConfig.Signer, bin) if err != nil { return nil, nil, err } diff --git a/frontend/windows/handle_zip.go b/frontend/windows/handle_zip.go index 35a23f22..632f243d 100644 --- a/frontend/windows/handle_zip.go +++ b/frontend/windows/handle_zip.go @@ -44,7 +44,7 @@ func handleZip(ctx context.Context, client gwclient.Client) (*gwclient.Result, e } if t, ok := spec.Targets[targetKey]; ok && frontend.HasSigner(&t) { - signed, err := frontend.ForwardToSigner(ctx, client, platform, t.PackageConfig.Signer, bin, "*.exe") + signed, err := frontend.ForwardToSigner(ctx, client, platform, t.PackageConfig.Signer, bin) if err != nil { return nil, nil, err } From 5b4e7e4a3d69d6071f408cf70f2c25647f331c1f Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Tue, 30 Apr 2024 11:15:00 -0400 Subject: [PATCH 19/28] Move signing.md to `website/docs` Signed-off-by: Peter Engelbert --- {docs => website/docs}/signing.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {docs => website/docs}/signing.md (100%) diff --git a/docs/signing.md b/website/docs/signing.md similarity index 100% rename from docs/signing.md rename to website/docs/signing.md From cc1dee2537c72c5f6065fa083b0c6729bd48b0fb Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Tue, 30 Apr 2024 11:17:35 -0400 Subject: [PATCH 20/28] Rename short variable Signed-off-by: Peter Engelbert --- test/fixtures/signer/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/fixtures/signer/main.go b/test/fixtures/signer/main.go index 51ccdd1e..44b82950 100644 --- a/test/fixtures/signer/main.go +++ b/test/fixtures/signer/main.go @@ -95,12 +95,12 @@ func main() { HashType: "sha256", } - cc, ok := c.(frontend.CurrentFrontend) + curFrontend, ok := c.(frontend.CurrentFrontend) if !ok { return nil, fmt.Errorf("cast to currentFrontend failed") } - basePtr, err := cc.CurrentFrontend() + basePtr, err := curFrontend.CurrentFrontend() if err != nil || basePtr == nil { if err == nil { err = fmt.Errorf("base frontend ptr was nil") From 5b02001a9c297aaa7f95c61f9540dc652167c80f Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Tue, 30 Apr 2024 13:20:12 -0400 Subject: [PATCH 21/28] Fix broken link in docs Signed-off-by: Peter Engelbert --- website/docs/signing.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/website/docs/signing.md b/website/docs/signing.md index 648e1480..414fdbb8 100644 --- a/website/docs/signing.md +++ b/website/docs/signing.md @@ -1,10 +1,10 @@ # Signing Packages Packages can be automatically signed using Dalec. To do this, you will -need to provide a signing frontend. There is an -[example](test/signer/main.go) in the test code. Once that signing image -has been built and tagged, the following can be added to the spec to trigger -the signing operation: +need to provide a signing frontend. There is an example in the test +code `test/signer/main.go`. Once that signing image has been built and +tagged, the following can be added to the spec to trigger the signing +operation: ```yaml targets: # Distro specific build requirements From accc429bd120533192746da73ae75ef82667389a Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Tue, 30 Apr 2024 13:55:03 -0400 Subject: [PATCH 22/28] Add signing entry to website config Signed-off-by: Peter Engelbert --- website/sidebars.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/website/sidebars.ts b/website/sidebars.ts index 5f571a0a..d0a2f030 100644 --- a/website/sidebars.ts +++ b/website/sidebars.ts @@ -19,7 +19,8 @@ const sidebars: SidebarsConfig = { 'intro', 'editor-support', 'sources', - 'testing' + 'testing', + 'signing' ], }, ], From db83add56e17a24457b13d128e620c322e671504 Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Wed, 1 May 2024 10:13:33 -0400 Subject: [PATCH 23/28] Tidy up signing test code * Move signing tests to mariner2_test and windows_test, respectively, since the test is checking 1) that the forwarding happens and 2) that the signing frontend receives sufficient information to determine signing logic. * Simplify the test signing frontend to do less. Signed-off-by: Peter Engelbert --- test/fixtures/signer/main.go | 65 ++---------- test/mariner2_test.go | 70 +++++++++++++ test/signer_test.go | 196 ----------------------------------- test/windows_test.go | 120 +++++++++++++++++++++ 4 files changed, 198 insertions(+), 253 deletions(-) delete mode 100644 test/signer_test.go diff --git a/test/fixtures/signer/main.go b/test/fixtures/signer/main.go index 44b82950..feccc320 100644 --- a/test/fixtures/signer/main.go +++ b/test/fixtures/signer/main.go @@ -18,33 +18,6 @@ import ( "google.golang.org/grpc/grpclog" ) -const ( - linuxSignOp = "LinuxSign" - windowsSignOp = "NotaryCoseSign" -) - -type Config struct { - ClientId string `json:"clientId"` - GatewayAPI string `json:"gatewayApi"` - RequestSigningCert map[string]string `json:"requestSigningCert"` - DRIEmail []string `json:"driEmail"` - SigningOperations []SigningOperation `json:"signingOperations"` - HashType string `json:"hashType"` -} - -type SigningOperation struct { - KeyCode string `json:"keyCode"` - OperationSetCode string `json:"operationSetCode"` - Parameters []ParameterKV `json:"parameters"` - ToolName string `json:"toolName"` - ToolVersion string `json:"toolVersion"` -} - -type ParameterKV struct { - ParameterName string `json:"parameterName"` - ParameterValue string `json:"parameterValue"` -} - func main() { bklog.L.Logger.SetOutput(os.Stderr) grpclog.SetLoggerV2(grpclog.NewLoggerV2WithVerbosity(bklog.L.WriterLevel(logrus.InfoLevel), bklog.L.WriterLevel(logrus.WarnLevel), bklog.L.WriterLevel(logrus.ErrorLevel), 1)) @@ -60,39 +33,17 @@ func main() { return nil, err } - signOp := "" - params := []ParameterKV{} + type config struct { + OS string + } + + cfg := config{} switch target { case "windowscross", "windows": - signOp = windowsSignOp - params = append(params, ParameterKV{ - ParameterName: "CoseFlags", - ParameterValue: "chainunprotected", - }) - + cfg.OS = "windows" default: - signOp = linuxSignOp - } - - config := Config{ - ClientId: "${AZURE_CLIENT_ID}", - GatewayAPI: "https://api.esrp.microsoft.com", - RequestSigningCert: map[string]string{ - "subject": "esrp-prss", - "vaultName": "${KEYVAULT_NAME}", - }, - DRIEmail: []string{"${BUILDER_EMAIL}"}, - SigningOperations: []SigningOperation{ - { - KeyCode: "${ESRP_KEYCODE}", - OperationSetCode: signOp, - Parameters: params, - ToolName: "sign", - ToolVersion: "1.0", - }, - }, - HashType: "sha256", + cfg.OS = "linux" } curFrontend, ok := c.(frontend.CurrentFrontend) @@ -114,7 +65,7 @@ func main() { return nil, fmt.Errorf("no artifact state provided to signer") } - configBytes, err := json.Marshal(&config) + configBytes, err := json.Marshal(&cfg) if err != nil { return nil, err } diff --git a/test/mariner2_test.go b/test/mariner2_test.go index 5655a118..48ca5c2a 100644 --- a/test/mariner2_test.go +++ b/test/mariner2_test.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "os" + "strings" "testing" "github.com/Azure/dalec" @@ -87,6 +88,7 @@ func testLinuxDistro(ctx context.Context, t *testing.T, buildTarget string) { return gwclient.NewResult(), nil }) }) + t.Run("container", func(t *testing.T) { spec := dalec.Spec{ Name: "test-container-build", @@ -304,6 +306,74 @@ echo "$BAR" > bar.txt return gwclient.NewResult(), nil }) }) + + runTest := func(t *testing.T, f gwclient.BuildFunc) { + t.Helper() + ctx := startTestSpan(baseCtx, t) + testEnv.RunTest(ctx, t, f) + } + + t.Run("test mariner2 signing", func(t *testing.T) { + t.Parallel() + runTest(t, func(ctx context.Context, gwc gwclient.Client) (*gwclient.Result, error) { + spec := dalec.Spec{ + Name: "foo", + Version: "v0.0.1", + Description: "foo bar baz", + Website: "https://foo.bar.baz", + Revision: "1", + Targets: map[string]dalec.Target{ + "mariner2": { + PackageConfig: &dalec.PackageConfig{ + Signer: &dalec.Frontend{ + Image: phonySignerRef, + }, + }, + }, + }, + Sources: map[string]dalec.Source{ + "foo": { + Inline: &dalec.SourceInline{ + File: &dalec.SourceInlineFile{ + Contents: "#!/usr/bin/env bash\necho \"hello, world!\"\n", + }, + }, + }, + }, + Build: dalec.ArtifactBuild{ + Steps: []dalec.BuildStep{ + { + Command: "/bin/true", + }, + }, + }, + Artifacts: dalec.Artifacts{ + Binaries: map[string]dalec.ArtifactConfig{ + "foo": {}, + }, + }, + } + + sr := newSolveRequest(withSpec(ctx, t, &spec), withBuildTarget("mariner2/rpm")) + res, err := gwc.Solve(ctx, sr) + if err != nil { + t.Fatal(err) + } + + tgt := readFile(ctx, t, "/target", res) + cfg := readFile(ctx, t, "/config.json", res) + + if string(tgt) != "mariner2" { + t.Fatal(fmt.Errorf("target incorrect; either not sent to signer or not received back from signer")) + } + + if !strings.Contains(string(cfg), "linux") { + t.Fatal(fmt.Errorf("configuration incorrect")) + } + + return gwclient.NewResult(), nil + }) + }) } func validateFilePerms(ctx context.Context, ref gwclient.Reference, p string, expected os.FileMode) error { diff --git a/test/signer_test.go b/test/signer_test.go deleted file mode 100644 index 1379555e..00000000 --- a/test/signer_test.go +++ /dev/null @@ -1,196 +0,0 @@ -package test - -import ( - "context" - "fmt" - "strings" - "testing" - - "github.com/Azure/dalec" - "github.com/moby/buildkit/client/llb" - gwclient "github.com/moby/buildkit/frontend/gateway/client" -) - -// TestHandlerTargetForwarding tests that targets are forwarded to the correct frontend. -// We do this by registering a phony frontend and then forwarding a target to it and checking the outputs. -func TestSignerForwarding(t *testing.T) { - runTest := func(t *testing.T, f gwclient.BuildFunc) { - t.Helper() - ctx := startTestSpan(baseCtx, t) - testEnv.RunTest(ctx, t, f) - } - - t.Run("test mariner2 signing", func(t *testing.T) { - t.Parallel() - runTest(t, func(ctx context.Context, gwc gwclient.Client) (*gwclient.Result, error) { - spec := dalec.Spec{ - Name: "foo", - Version: "v0.0.1", - Description: "foo bar baz", - Website: "https://foo.bar.baz", - Revision: "1", - Targets: map[string]dalec.Target{ - "mariner2": { - PackageConfig: &dalec.PackageConfig{ - Signer: &dalec.Frontend{ - Image: phonySignerRef, - }, - }, - }, - }, - Sources: map[string]dalec.Source{ - "foo": { - Inline: &dalec.SourceInline{ - File: &dalec.SourceInlineFile{ - Contents: "#!/usr/bin/env bash\necho \"hello, world!\"\n", - }, - }, - }, - }, - Build: dalec.ArtifactBuild{ - Steps: []dalec.BuildStep{ - { - Command: "/bin/true", - }, - }, - }, - Artifacts: dalec.Artifacts{ - Binaries: map[string]dalec.ArtifactConfig{ - "foo": {}, - }, - }, - } - - sr := newSolveRequest(withSpec(ctx, t, &spec), withBuildTarget("mariner2/rpm")) - res, err := gwc.Solve(ctx, sr) - if err != nil { - t.Fatal(err) - } - - tgt := readFile(ctx, t, "/target", res) - cfg := readFile(ctx, t, "/config.json", res) - - if string(tgt) != "mariner2" { - t.Fatal(fmt.Errorf("target incorrect; either not sent to signer or not received back from signer")) - } - - if !strings.Contains(string(cfg), "LinuxSign") { - t.Fatal(fmt.Errorf("configuration incorrect")) - } - - return gwclient.NewResult(), nil - }) - }) - - t.Run("test windows signing", func(t *testing.T) { - t.Parallel() - runTest(t, func(ctx context.Context, gwc gwclient.Client) (*gwclient.Result, error) { - spec := fillMetadata("foo", &dalec.Spec{ - Targets: map[string]dalec.Target{ - "windowscross": { - PackageConfig: &dalec.PackageConfig{ - Signer: &dalec.Frontend{ - Image: phonySignerRef, - }, - }, - }, - }, - Sources: map[string]dalec.Source{ - "foo": { - Inline: &dalec.SourceInline{ - File: &dalec.SourceInlineFile{ - Contents: "#!/usr/bin/env bash\necho \"hello, world!\"\n", - }, - }, - }, - }, - Build: dalec.ArtifactBuild{ - Steps: []dalec.BuildStep{ - { - Command: "/bin/true", - }, - }, - }, - Artifacts: dalec.Artifacts{ - Binaries: map[string]dalec.ArtifactConfig{ - "foo": {}, - }, - }, - }) - - zipperSpec := fillMetadata("bar", &dalec.Spec{ - Dependencies: &dalec.PackageDependencies{ - Runtime: map[string][]string{ - "unzip": {}, - }, - }, - }) - - sr := newSolveRequest(withSpec(ctx, t, zipperSpec), withBuildTarget("mariner2/container")) - zipper := reqToState(ctx, gwc, sr, t) - - sr = newSolveRequest(withSpec(ctx, t, spec), withBuildTarget("windowscross/zip")) - st := reqToState(ctx, gwc, sr, t) - - st = zipper.Run(llb.Args([]string{"bash", "-c", `for f in ./*.zip; do unzip "$f"; done`}), llb.Dir("/tmp/mnt")). - AddMount("/tmp/mnt", st) - - def, err := st.Marshal(ctx) - if err != nil { - t.Fatal(err) - } - - res, err := gwc.Solve(ctx, gwclient.SolveRequest{ - Definition: def.ToPB(), - }) - if err != nil { - t.Fatal(err) - } - - tgt := readFile(ctx, t, "/target", res) - cfg := readFile(ctx, t, "/config.json", res) - - if string(tgt) != "windowscross" { - t.Fatal(fmt.Errorf("target incorrect; either not sent to signer or not received back from signer")) - } - - if !strings.Contains(string(cfg), "NotaryCoseSign") { - t.Fatal(fmt.Errorf("configuration incorrect")) - } - - return gwclient.NewResult(), nil - }) - }) -} - -func reqToState(ctx context.Context, gwc gwclient.Client, sr gwclient.SolveRequest, t *testing.T) llb.State { - res, err := gwc.Solve(ctx, sr) - if err != nil { - t.Fatal(err) - } - - ref, err := res.SingleRef() - if err != nil { - t.Fatal(err) - } - - st, err := ref.ToState() - if err != nil { - t.Fatal(err) - } - - return st -} - -func fillMetadata(fakename string, s *dalec.Spec) *dalec.Spec { - s.Name = "bar" - s.Version = "v0.0.1" - s.Description = "foo bar baz" - s.Website = "https://foo.bar.baz" - s.Revision = "1" - s.License = "MIT" - s.Vendor = "nothing" - s.Packager = "Bill Spummins" - - return s -} diff --git a/test/windows_test.go b/test/windows_test.go index c04344d8..7b95c1c9 100644 --- a/test/windows_test.go +++ b/test/windows_test.go @@ -4,9 +4,11 @@ import ( "context" "errors" "fmt" + "strings" "testing" "github.com/Azure/dalec" + "github.com/moby/buildkit/client/llb" gwclient "github.com/moby/buildkit/frontend/gateway/client" moby_buildkit_v1_frontend "github.com/moby/buildkit/frontend/gateway/pb" ) @@ -248,4 +250,122 @@ echo "$BAR" > bar.txt return gwclient.NewResult(), nil }) }) + + runTest := func(t *testing.T, f gwclient.BuildFunc) { + t.Helper() + ctx := startTestSpan(baseCtx, t) + testEnv.RunTest(ctx, t, f) + } + + t.Run("test windows signing", func(t *testing.T) { + t.Parallel() + runTest(t, func(ctx context.Context, gwc gwclient.Client) (*gwclient.Result, error) { + spec := fillMetadata("foo", &dalec.Spec{ + Targets: map[string]dalec.Target{ + "windowscross": { + PackageConfig: &dalec.PackageConfig{ + Signer: &dalec.Frontend{ + Image: phonySignerRef, + }, + }, + }, + }, + Sources: map[string]dalec.Source{ + "foo": { + Inline: &dalec.SourceInline{ + File: &dalec.SourceInlineFile{ + Contents: "#!/usr/bin/env bash\necho \"hello, world!\"\n", + }, + }, + }, + }, + Build: dalec.ArtifactBuild{ + Steps: []dalec.BuildStep{ + { + Command: "/bin/true", + }, + }, + }, + Artifacts: dalec.Artifacts{ + Binaries: map[string]dalec.ArtifactConfig{ + "foo": {}, + }, + }, + }) + + zipperSpec := fillMetadata("bar", &dalec.Spec{ + Dependencies: &dalec.PackageDependencies{ + Runtime: map[string][]string{ + "unzip": {}, + }, + }, + }) + + sr := newSolveRequest(withSpec(ctx, t, zipperSpec), withBuildTarget("mariner2/container")) + zipper := reqToState(ctx, gwc, sr, t) + + sr = newSolveRequest(withSpec(ctx, t, spec), withBuildTarget("windowscross/zip")) + st := reqToState(ctx, gwc, sr, t) + + st = zipper.Run(llb.Args([]string{"bash", "-c", `for f in ./*.zip; do unzip "$f"; done`}), llb.Dir("/tmp/mnt")). + AddMount("/tmp/mnt", st) + + def, err := st.Marshal(ctx) + if err != nil { + t.Fatal(err) + } + + res, err := gwc.Solve(ctx, gwclient.SolveRequest{ + Definition: def.ToPB(), + }) + if err != nil { + t.Fatal(err) + } + + tgt := readFile(ctx, t, "/target", res) + cfg := readFile(ctx, t, "/config.json", res) + + if string(tgt) != "windowscross" { + t.Fatal(fmt.Errorf("target incorrect; either not sent to signer or not received back from signer")) + } + + if !strings.Contains(string(cfg), "windows") { + t.Fatal(fmt.Errorf("configuration incorrect")) + } + + return gwclient.NewResult(), nil + }) + }) +} + +func reqToState(ctx context.Context, gwc gwclient.Client, sr gwclient.SolveRequest, t *testing.T) llb.State { + res, err := gwc.Solve(ctx, sr) + if err != nil { + t.Fatal(err) + } + + ref, err := res.SingleRef() + if err != nil { + t.Fatal(err) + } + + st, err := ref.ToState() + if err != nil { + t.Fatal(err) + } + + return st +} + +func fillMetadata(fakename string, s *dalec.Spec) *dalec.Spec { + s.Name = "bar" + s.Version = "v0.0.1" + s.Description = "foo bar baz" + s.Website = "https://foo.bar.baz" + s.Revision = "1" + s.License = "MIT" + s.Vendor = "nothing" + s.Packager = "Bill Spummins" + + return s } From 2ea0cdb241a617a924e9597a015704a4009ebc14 Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Wed, 1 May 2024 10:43:31 -0400 Subject: [PATCH 24/28] Add `package_config` to root of Spec This allows the same signer to be configured for all distros in the spec. Signed-off-by: Peter Engelbert --- frontend/mariner2/handle_rpm.go | 5 ++--- frontend/request.go | 4 ---- frontend/windows/handle_container.go | 4 ++-- frontend/windows/handle_zip.go | 4 ++-- helpers.go | 18 ++++++++++++++++++ spec.go | 3 +++ 6 files changed, 27 insertions(+), 11 deletions(-) diff --git a/frontend/mariner2/handle_rpm.go b/frontend/mariner2/handle_rpm.go index fb16fba4..c7367f54 100644 --- a/frontend/mariner2/handle_rpm.go +++ b/frontend/mariner2/handle_rpm.go @@ -48,9 +48,8 @@ func handleRPM(ctx context.Context, client gwclient.Client) (*gwclient.Result, e return nil, nil, err } - t := spec.Targets[targetKey] - if frontend.HasSigner(&t) { - signed, err := frontend.ForwardToSigner(ctx, client, platform, t.PackageConfig.Signer, st) + if signer, ok := spec.GetSigner(targetKey); ok { + signed, err := frontend.ForwardToSigner(ctx, client, platform, signer, st) if err != nil { return nil, nil, err } diff --git a/frontend/request.go b/frontend/request.go index b453fac9..570b7b13 100644 --- a/frontend/request.go +++ b/frontend/request.go @@ -181,7 +181,3 @@ func ForwardToSigner(ctx context.Context, client gwclient.Client, platform *ocis func compound(k, v string) string { return fmt.Sprintf("%s:%s", k, v) } - -func HasSigner(t *dalec.Target) bool { - return t != nil && t.PackageConfig != nil && t.PackageConfig.Signer != nil && t.PackageConfig.Signer.Image != "" -} diff --git a/frontend/windows/handle_container.go b/frontend/windows/handle_container.go index dfc2dd62..889d3e57 100644 --- a/frontend/windows/handle_container.go +++ b/frontend/windows/handle_container.go @@ -54,8 +54,8 @@ func handleContainer(ctx context.Context, client gwclient.Client) (*gwclient.Res return nil, nil, fmt.Errorf("unable to build binary %w", err) } - if t, ok := spec.Targets[targetKey]; ok && frontend.HasSigner(&t) { - signed, err := frontend.ForwardToSigner(ctx, client, platform, t.PackageConfig.Signer, bin) + if signer, ok := spec.GetSigner(targetKey); ok { + signed, err := frontend.ForwardToSigner(ctx, client, platform, signer, bin) if err != nil { return nil, nil, err } diff --git a/frontend/windows/handle_zip.go b/frontend/windows/handle_zip.go index 632f243d..8e690c22 100644 --- a/frontend/windows/handle_zip.go +++ b/frontend/windows/handle_zip.go @@ -43,8 +43,8 @@ func handleZip(ctx context.Context, client gwclient.Client) (*gwclient.Result, e return nil, nil, fmt.Errorf("unable to build binaries: %w", err) } - if t, ok := spec.Targets[targetKey]; ok && frontend.HasSigner(&t) { - signed, err := frontend.ForwardToSigner(ctx, client, platform, t.PackageConfig.Signer, bin) + if signer, ok := spec.GetSigner(targetKey); ok { + signed, err := frontend.ForwardToSigner(ctx, client, platform, signer, bin) if err != nil { return nil, nil, err } diff --git a/helpers.go b/helpers.go index 189a2c85..dde62583 100644 --- a/helpers.go +++ b/helpers.go @@ -312,3 +312,21 @@ func (s *Spec) GetSymlinks(target string) map[string]SymlinkTarget { return lm } + +func (s *Spec) GetSigner(targetKey string) (*Frontend, bool) { + if s.Targets != nil { + if t, ok := s.Targets[targetKey]; ok && hasValidSigner(t.PackageConfig) { + return t.PackageConfig.Signer, true + } + } + + if hasValidSigner(s.PackageConfig) { + return s.PackageConfig.Signer, true + } + + return nil, false +} + +func hasValidSigner(pc *PackageConfig) bool { + return pc != nil && pc.Signer != nil && pc.Signer.Image != "" +} diff --git a/spec.go b/spec.go index b45a1dac..f9d75092 100644 --- a/spec.go +++ b/spec.go @@ -82,6 +82,9 @@ type Spec struct { // Dependencies are the different dependencies that need to be specified in the package. // Dependencies are overwritten if specified in the target map for the requested distro. Dependencies *PackageDependencies `yaml:"dependencies,omitempty" json:"dependencies,omitempty"` + // PackageConfig is the configuration to use for artifact targets, such as + // rpms, debs, or zip files containing Windows binaries + PackageConfig *PackageConfig `yaml:"package_config,omitempty" json:"package_config,omitempty"` // Image is the image configuration when the target output is a container image. // This is overwritten if specified in the target map for the requested distro. Image *ImageConfig `yaml:"image,omitempty" json:"image,omitempty"` From fa20d32e0794040273dc464980f1b3beb623678f Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Wed, 1 May 2024 10:46:26 -0400 Subject: [PATCH 25/28] Test specifying signer in root level PackageConfig Signed-off-by: Peter Engelbert --- test/mariner2_test.go | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/test/mariner2_test.go b/test/mariner2_test.go index 48ca5c2a..4b5a6892 100644 --- a/test/mariner2_test.go +++ b/test/mariner2_test.go @@ -322,13 +322,9 @@ echo "$BAR" > bar.txt Description: "foo bar baz", Website: "https://foo.bar.baz", Revision: "1", - Targets: map[string]dalec.Target{ - "mariner2": { - PackageConfig: &dalec.PackageConfig{ - Signer: &dalec.Frontend{ - Image: phonySignerRef, - }, - }, + PackageConfig: &dalec.PackageConfig{ + Signer: &dalec.Frontend{ + Image: phonySignerRef, }, }, Sources: map[string]dalec.Source{ From afb3fc4c8e2b57283b620848c4aa0a4a58a78fa7 Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Wed, 1 May 2024 10:50:06 -0400 Subject: [PATCH 26/28] Run `go generate` Signed-off-by: Peter Engelbert --- docs/spec.schema.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/spec.schema.json b/docs/spec.schema.json index cfad86f3..d7c0979d 100644 --- a/docs/spec.schema.json +++ b/docs/spec.schema.json @@ -802,6 +802,10 @@ "$ref": "#/$defs/PackageDependencies", "description": "Dependencies are the different dependencies that need to be specified in the package.\nDependencies are overwritten if specified in the target map for the requested distro." }, + "package_config": { + "$ref": "#/$defs/PackageConfig", + "description": "PackageConfig is the configuration to use for artifact targets, such as\nrpms, debs, or zip files containing Windows binaries" + }, "image": { "$ref": "#/$defs/ImageConfig", "description": "Image is the image configuration when the target output is a container image.\nThis is overwritten if specified in the target map for the requested distro." From fbc82f1eb4d771dcb2d63da5a177cd24d7a5cd2d Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Wed, 1 May 2024 12:24:41 -0400 Subject: [PATCH 27/28] Make linux distro test generic Signed-off-by: Peter Engelbert --- test/mariner2_test.go | 74 ++++++++++++++++--------------------------- test/signing_test.go | 36 +++++++++++++++++++++ 2 files changed, 63 insertions(+), 47 deletions(-) create mode 100644 test/signing_test.go diff --git a/test/mariner2_test.go b/test/mariner2_test.go index 4b5a6892..ca012b06 100644 --- a/test/mariner2_test.go +++ b/test/mariner2_test.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "os" - "strings" "testing" "github.com/Azure/dalec" @@ -315,60 +314,41 @@ echo "$BAR" > bar.txt t.Run("test mariner2 signing", func(t *testing.T) { t.Parallel() - runTest(t, func(ctx context.Context, gwc gwclient.Client) (*gwclient.Result, error) { - spec := dalec.Spec{ - Name: "foo", - Version: "v0.0.1", - Description: "foo bar baz", - Website: "https://foo.bar.baz", - Revision: "1", - PackageConfig: &dalec.PackageConfig{ - Signer: &dalec.Frontend{ - Image: phonySignerRef, - }, + spec := dalec.Spec{ + Name: "foo", + Version: "v0.0.1", + Description: "foo bar baz", + Website: "https://foo.bar.baz", + Revision: "1", + PackageConfig: &dalec.PackageConfig{ + Signer: &dalec.Frontend{ + Image: phonySignerRef, }, - Sources: map[string]dalec.Source{ - "foo": { - Inline: &dalec.SourceInline{ - File: &dalec.SourceInlineFile{ - Contents: "#!/usr/bin/env bash\necho \"hello, world!\"\n", - }, + }, + Sources: map[string]dalec.Source{ + "foo": { + Inline: &dalec.SourceInline{ + File: &dalec.SourceInlineFile{ + Contents: "#!/usr/bin/env bash\necho \"hello, world!\"\n", }, }, }, - Build: dalec.ArtifactBuild{ - Steps: []dalec.BuildStep{ - { - Command: "/bin/true", - }, + }, + Build: dalec.ArtifactBuild{ + Steps: []dalec.BuildStep{ + { + Command: "/bin/true", }, }, - Artifacts: dalec.Artifacts{ - Binaries: map[string]dalec.ArtifactConfig{ - "foo": {}, - }, + }, + Artifacts: dalec.Artifacts{ + Binaries: map[string]dalec.ArtifactConfig{ + "foo": {}, }, - } - - sr := newSolveRequest(withSpec(ctx, t, &spec), withBuildTarget("mariner2/rpm")) - res, err := gwc.Solve(ctx, sr) - if err != nil { - t.Fatal(err) - } - - tgt := readFile(ctx, t, "/target", res) - cfg := readFile(ctx, t, "/config.json", res) - - if string(tgt) != "mariner2" { - t.Fatal(fmt.Errorf("target incorrect; either not sent to signer or not received back from signer")) - } - - if !strings.Contains(string(cfg), "linux") { - t.Fatal(fmt.Errorf("configuration incorrect")) - } + }, + } - return gwclient.NewResult(), nil - }) + runTest(t, distroSigningTest(t, &spec, "mariner2/rpm")) }) } diff --git a/test/signing_test.go b/test/signing_test.go new file mode 100644 index 00000000..3297f46f --- /dev/null +++ b/test/signing_test.go @@ -0,0 +1,36 @@ +package test + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/Azure/dalec" + gwclient "github.com/moby/buildkit/frontend/gateway/client" +) + +func distroSigningTest(t *testing.T, spec *dalec.Spec, buildTarget string) func(ctx context.Context, gwc gwclient.Client) (*gwclient.Result, error) { + return func(ctx context.Context, gwc gwclient.Client) (*gwclient.Result, error) { + topTgt, _, _ := strings.Cut(buildTarget, "/") + + sr := newSolveRequest(withSpec(ctx, t, spec), withBuildTarget(buildTarget)) + res, err := gwc.Solve(ctx, sr) + if err != nil { + t.Fatal(err) + } + + tgt := readFile(ctx, t, "/target", res) + cfg := readFile(ctx, t, "/config.json", res) + + if string(tgt) != topTgt { + t.Fatal(fmt.Errorf("target incorrect; either not sent to signer or not received back from signer")) + } + + if !strings.Contains(string(cfg), "linux") { + t.Fatal(fmt.Errorf("configuration incorrect")) + } + + return gwclient.NewResult(), nil + } +} From 1af3b0f8c5f541c59478403fe4f758101e32f4a5 Mon Sep 17 00:00:00 2001 From: Peter Engelbert Date: Wed, 1 May 2024 13:11:37 -0400 Subject: [PATCH 28/28] Finish fixing tests Signed-off-by: Peter Engelbert --- test/mariner2_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/mariner2_test.go b/test/mariner2_test.go index ca012b06..c9d584f7 100644 --- a/test/mariner2_test.go +++ b/test/mariner2_test.go @@ -16,10 +16,10 @@ func TestMariner2(t *testing.T) { t.Parallel() ctx := startTestSpan(baseCtx, t) - testLinuxDistro(ctx, t, "mariner2/container") + testLinuxDistro(ctx, t, "mariner2/container", "mariner2/rpm") } -func testLinuxDistro(ctx context.Context, t *testing.T, buildTarget string) { +func testLinuxDistro(ctx context.Context, t *testing.T, buildTarget string, signTarget string) { t.Run("Fail when non-zero exit code during build", func(t *testing.T) { t.Parallel() spec := dalec.Spec{ @@ -312,7 +312,7 @@ echo "$BAR" > bar.txt testEnv.RunTest(ctx, t, f) } - t.Run("test mariner2 signing", func(t *testing.T) { + t.Run("test signing", func(t *testing.T) { t.Parallel() spec := dalec.Spec{ Name: "foo", @@ -348,7 +348,7 @@ echo "$BAR" > bar.txt }, } - runTest(t, distroSigningTest(t, &spec, "mariner2/rpm")) + runTest(t, distroSigningTest(t, &spec, signTarget)) }) }