From 9a39edb1e99b66030f506297b56aa9f57c248eab Mon Sep 17 00:00:00 2001 From: seph Date: Thu, 16 May 2019 14:31:57 -0400 Subject: [PATCH] Windows Signing (#489) Add package-kit support for authenticode signing through signtool.exe. This should be seen as an initial pass at getting general APIs and structure right. Tools may change here. Reworks the options. Signing keys now have platform specific configuration Makefile updated to include launcher code push snippets. These are Kolide specific, and part of how we update notary. Makefile updated to use osslsigncode to sign windows binaries --- Makefile | 15 ++- cmd/package-builder/package-builder.go | 4 +- pkg/packagekit/authenticode/authenticode.go | 67 +++++++++++++ .../authenticode/authenticode_dummy.go | 9 ++ .../authenticode/authenticode_test.go | 68 +++++++++++++ .../authenticode/authenticode_windows.go | 99 +++++++++++++++++++ pkg/packagekit/package.go | 5 +- pkg/packagekit/package_pkg.go | 4 +- pkg/packagekit/package_test.go | 8 +- pkg/packagekit/package_wix.go | 33 ++++++- pkg/packagekit/wix/wix.go | 26 ++--- pkg/packagekit/wix/wix_test.go | 12 +-- pkg/packaging/packaging.go | 21 ++-- 13 files changed, 322 insertions(+), 49 deletions(-) create mode 100644 pkg/packagekit/authenticode/authenticode.go create mode 100644 pkg/packagekit/authenticode/authenticode_dummy.go create mode 100644 pkg/packagekit/authenticode/authenticode_test.go create mode 100644 pkg/packagekit/authenticode/authenticode_windows.go diff --git a/Makefile b/Makefile index 5f847e826..bfc6ea792 100644 --- a/Makefile +++ b/Makefile @@ -60,7 +60,15 @@ codesign-darwin: xp codesign --force -s "${CODESIGN_IDENTITY}" -v ./build/darwin/launcher codesign --force -s "${CODESIGN_IDENTITY}" -v ./build/darwin/osquery-extension.ext -codesign: codesign-darwin +# Using the `osslsigncode` we can sign windows binaries from +# non-windows platforms. +codesign-windows: codesign-windows-launcher.exe codesign-windows-osquery-extension.exe +codesign-windows-%: xp + @if [ -z "${AUTHENTICODE_PASSPHRASE}" ]; then echo "Missing AUTHENTICODE_PASSPHRASE"; exit 1; fi + osslsigncode -in build/windows/$* -out build/windows/$* -i https://kolide.com -h sha1 -t http://timestamp.verisign.com/scripts/timstamp.dll -pkcs12 ~/Documents/kolide-codesigning-2019.p12 -pass "${AUTHENTICODE_PASSPHRASE}" + osslsigncode -in build/windows/$* -out build/windows/$* -i https://kolide.com -h sha256 -nest -ts http://sha256timestamp.ws.symantec.com/sha256/timestamp -pkcs12 ~/Documents/kolide-codesigning-2019.p12 -pass "${AUTHENTICODE_PASSPHRASE}" + +codesign: codesign-darwin codesign-windows package-builder: .pre-build deps go run cmd/make/make.go -targets=package-builder -linkstamp @@ -167,7 +175,8 @@ dockerpush-%: docker-% # Porter is a kolide tool to update notary, part of the update framework porter-%: codesign + @if [ -z "${NOTARY_DELEGATION_PASSPHRASE}" ]; then echo "Missing NOTARY_DELEGATION_PASSPHRASE"; exit 1; fi for p in darwin linux windows; do \ - echo porter mirror -debug -channel $* -platform $$p -launcher-all; \ - echo porter mirror -debug -channel $* -platform $$p -extension-tarball -extension-upload; \ + porter mirror -debug -channel $* -platform $$p -launcher-all; \ + porter mirror -debug -channel $* -platform $$p -extension-tarball -extension-upload; \ done diff --git a/cmd/package-builder/package-builder.go b/cmd/package-builder/package-builder.go index 2e6d25429..d1dceedc8 100644 --- a/cmd/package-builder/package-builder.go +++ b/cmd/package-builder/package-builder.go @@ -67,7 +67,7 @@ func runMake(args []string) error { flSigningKey = flagset.String( "mac_package_signing_key", env.String("SIGNING_KEY", ""), - "The name of the key that should be used to packages. Behavior is platform and packaging specific", + "The name of the key that should be used for signing on apple platforms", ) flTransport = flagset.String( "transport", @@ -188,7 +188,7 @@ func runMake(args []string) error { ExtensionVersion: *flExtensionVersion, Hostname: *flHostname, Secret: *flEnrollSecret, - SigningKey: *flSigningKey, + AppleSigningKey: *flSigningKey, Transport: *flTransport, Insecure: *flInsecure, InsecureTransport: *flInsecureTransport, diff --git a/pkg/packagekit/authenticode/authenticode.go b/pkg/packagekit/authenticode/authenticode.go new file mode 100644 index 000000000..b708b1ff0 --- /dev/null +++ b/pkg/packagekit/authenticode/authenticode.go @@ -0,0 +1,67 @@ +package authenticode + +import ( + "bytes" + "context" + "os/exec" + "strings" + + "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/contexts/ctxlog" + "github.com/pkg/errors" +) + +// signtoolOptions are the options for how we call signtool.exe. These +// are *not* the tool options, but instead our own representation of +// the arguments.w +type signtoolOptions struct { + extraArgs []string + subjectName string // If present, use this as the `/n` argument + skipValidation bool + signtoolPath string + timestampServer string + rfc3161Server string + + execCC func(context.Context, string, ...string) *exec.Cmd // Allows test overrides + +} + +type SigntoolOpt func(*signtoolOptions) + +func SkipValidation() SigntoolOpt { + return func(so *signtoolOptions) { + so.skipValidation = true + } +} + +// WithExtraArgs set additional arguments for signtool. Common ones +// may be {`\n`, "subject name"} +func WithExtraArgs(args []string) SigntoolOpt { + return func(so *signtoolOptions) { + so.extraArgs = args + } +} + +func WithSigntoolPath(path string) SigntoolOpt { + return func(so *signtoolOptions) { + so.signtoolPath = path + } +} + +func (so *signtoolOptions) execOut(ctx context.Context, argv0 string, args ...string) (string, string, error) { + logger := ctxlog.FromContext(ctx) + + cmd := so.execCC(ctx, argv0, args...) + + level.Debug(logger).Log( + "msg", "execing", + "cmd", strings.Join(cmd.Args, " "), + ) + + stdout, stderr := new(bytes.Buffer), new(bytes.Buffer) + cmd.Stdout, cmd.Stderr = stdout, stderr + if err := cmd.Run(); err != nil { + return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), errors.Wrapf(err, "run command %s %v, stderr=%s", argv0, args, stderr) + } + return strings.TrimSpace(stdout.String()), strings.TrimSpace(stderr.String()), nil +} diff --git a/pkg/packagekit/authenticode/authenticode_dummy.go b/pkg/packagekit/authenticode/authenticode_dummy.go new file mode 100644 index 000000000..5f8c3fbf8 --- /dev/null +++ b/pkg/packagekit/authenticode/authenticode_dummy.go @@ -0,0 +1,9 @@ +// +build !windows + +package authenticode + +import "context" + +func Sign(ctx context.Context, file string, opts ...SigntoolOpt) error { + return nil +} diff --git a/pkg/packagekit/authenticode/authenticode_test.go b/pkg/packagekit/authenticode/authenticode_test.go new file mode 100644 index 000000000..f27fe9442 --- /dev/null +++ b/pkg/packagekit/authenticode/authenticode_test.go @@ -0,0 +1,68 @@ +package authenticode + +import ( + "context" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "runtime" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +const ( + srcExe = `C:\Windows\System32\netmsg.dll` + signtoolPath = `C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x64\signtool.exe` +) + +func TestSign(t *testing.T) { + t.Parallel() + + if runtime.GOOS != "windows" { + t.Skip("not windows") + } + + // create a signtoolOptions object so we can call the exec method + so := &signtoolOptions{ + execCC: exec.CommandContext, + } + + ctx, ctxCancel := context.WithTimeout(context.Background(), 120*time.Second) + defer ctxCancel() + + tmpDir, err := ioutil.TempDir("", "packagekit-authenticode-signing") + defer os.RemoveAll(tmpDir) + require.NoError(t, err) + + testExe := filepath.Join(tmpDir, "test.exe") + + // copy our test file + data, err := ioutil.ReadFile(srcExe) + require.NoError(t, err) + err = ioutil.WriteFile(testExe, data, 0755) + require.NoError(t, err) + + // confirm that we _don't_ have a sig on this file + _, verifyInitial, err := so.execOut(ctx, signtoolPath, "verify", "/pa", testExe) + require.Error(t, err, "no initial signature") + require.Contains(t, verifyInitial, "No signature found", "no initial signature") + + // Sign it! + err = Sign(ctx, testExe, WithSigntoolPath(signtoolPath)) + require.NoError(t, err) + + // verify, as an explicit test. Gotta check both indexes manually. + verifyOut0, _, err := so.execOut(ctx, signtoolPath, "verify", "/pa", "/ds", "0", testExe) + require.NoError(t, err, "verify signature position 0") + require.Contains(t, verifyOut0, "sha1", "contains algorithm verify output") + require.Contains(t, verifyOut0, "Authenticode", "contains timestamp verify output") + + verifyOut1, _, err := so.execOut(ctx, signtoolPath, "verify", "/pa", "/ds", "1", testExe) + require.NoError(t, err, "verify signature position 1") + require.Contains(t, verifyOut1, "sha256", "contains algorithm verify output") + require.Contains(t, verifyOut1, "RFC3161", "contains timestamp verify output") + +} diff --git a/pkg/packagekit/authenticode/authenticode_windows.go b/pkg/packagekit/authenticode/authenticode_windows.go new file mode 100644 index 000000000..8b8818271 --- /dev/null +++ b/pkg/packagekit/authenticode/authenticode_windows.go @@ -0,0 +1,99 @@ +// +build windows + +// Authenticode is a light wrapper around signing code under windows. +// +// See +// +// https://docs.microsoft.com/en-us/dotnet/framework/tools/signtool-exe + +package authenticode + +import ( + "context" + "os/exec" + "strings" + + "github.com/go-kit/kit/log" + "github.com/go-kit/kit/log/level" + "github.com/kolide/launcher/pkg/contexts/ctxlog" + "github.com/pkg/errors" + "go.opencensus.io/trace" +) + +// Sign uses signtool to add authenticode signatures. It supports +// optional arguments to allow cert specification +func Sign(ctx context.Context, file string, opts ...SigntoolOpt) error { + ctx, span := trace.StartSpan(ctx, "authenticode.Sign") + defer span.End() + + logger := log.With(ctxlog.FromContext(ctx), "caller", "authenticode.Sign") + + level.Debug(logger).Log( + "msg", "signing file", + "file", file, + ) + + so := &signtoolOptions{ + signtoolPath: "signtool.exe", + timestampServer: "http://timestamp.verisign.com/scripts/timstamp.dll", + rfc3161Server: "http://sha256timestamp.ws.symantec.com/sha256/timestamp", + execCC: exec.CommandContext, + } + + for _, opt := range opts { + opt(so) + } + + // signtool.exe can be called multiple times to apply multiple + // signatures. _But_ it uses different arguments for the subsequent + // signatures. So, multiple calls. + // + // _However_ it's not clear this is supported for MSIs, which maybe + // only have a single slot for signing. + // + // References: + // https://knowledge.digicert.com/generalinformation/INFO2274.html + if strings.HasSuffix(file, ".msi") { + if err := so.signtoolSign(ctx, file, "/ph", "/fd", "sha256", "/td", "sha256", "/tr", so.rfc3161Server); err != nil { + return errors.Wrap(err, "signing msi with sha256") + } + } else { + if err := so.signtoolSign(ctx, file, "/ph", "/fd", "sha1", "/t", so.timestampServer); err != nil { + return errors.Wrap(err, "signing file with sha1") + } + + if err := so.signtoolSign(ctx, file, "/as", "/ph", "/fd", "sha256", "/td", "sha256", "/tr", so.rfc3161Server); err != nil { + return errors.Wrap(err, "signing file with sha256") + } + } + + if so.skipValidation { + return nil + } + + _, _, err := so.execOut(ctx, so.signtoolPath, "verify", "/pa", "/v", file) + if err != nil { + return errors.Wrap(err, "verify") + } + + return nil +} + +// signtoolSign appends some arguments and execs +func (so *signtoolOptions) signtoolSign(ctx context.Context, file string, args ...string) error { + ctx, span := trace.StartSpan(ctx, "signtoolSign") + defer span.End() + + args = append([]string{"sign"}, args...) + + if so.extraArgs != nil { + args = append(args, so.extraArgs...) + } + + args = append(args, file) + + if _, _, err := so.execOut(ctx, so.signtoolPath, args...); err != nil { + return errors.Wrap(err, "calling signtool") + } + return nil +} diff --git a/pkg/packagekit/package.go b/pkg/packagekit/package.go index 72ae2484b..4281b1b10 100644 --- a/pkg/packagekit/package.go +++ b/pkg/packagekit/package.go @@ -7,7 +7,10 @@ type PackageOptions struct { Name string // What's the name for this package (eg: launcher) Root string // source directory to package Scripts string // directory of packaging scripts (postinst, prerm, etc) - SigningKey string // key to sign packages with (platform specific behaviors) Version string // package version FlagFile string // Path to the flagfile for configuration + + AppleSigningKey string // apple signing key + WindowsUseSigntool bool // whether to use signtool.exe on windows + WindowsSigntoolArgs []string // Extra args for signtool. May be needed for finding a key } diff --git a/pkg/packagekit/package_pkg.go b/pkg/packagekit/package_pkg.go index 5a72988c7..9d58977d4 100644 --- a/pkg/packagekit/package_pkg.go +++ b/pkg/packagekit/package_pkg.go @@ -46,8 +46,8 @@ func PackagePkg(ctx context.Context, w io.Writer, po *PackageOptions) error { args = append(args, "--scripts", po.Scripts) } - if po.SigningKey != "" { - args = append(args, "--sign", po.SigningKey) + if po.AppleSigningKey != "" { + args = append(args, "--sign", po.AppleSigningKey) } args = append(args, outputPath) diff --git a/pkg/packagekit/package_test.go b/pkg/packagekit/package_test.go index 28e655842..4dee2cf1c 100644 --- a/pkg/packagekit/package_test.go +++ b/pkg/packagekit/package_test.go @@ -22,10 +22,10 @@ func TestPackageTrivial(t *testing.T) { require.NoError(t, err) po := &PackageOptions{ - Name: "test-empty", - Version: "0.0.0", - Root: inputDir, - SigningKey: "Developer ID Installer: Kolide Inc (YZ3EM74M78)", + Name: "test-empty", + Version: "0.0.0", + Root: inputDir, + AppleSigningKey: "Developer ID Installer: Kolide Inc (YZ3EM74M78)", } err = PackageFPM(context.TODO(), ioutil.Discard, po, AsTar()) diff --git a/pkg/packagekit/package_wix.go b/pkg/packagekit/package_wix.go index b65344314..01781d791 100644 --- a/pkg/packagekit/package_wix.go +++ b/pkg/packagekit/package_wix.go @@ -5,11 +5,13 @@ import ( "context" "fmt" "io" + "os" "runtime" "strings" "text/template" "github.com/google/uuid" + "github.com/kolide/launcher/pkg/packagekit/authenticode" "github.com/kolide/launcher/pkg/packagekit/internal" "github.com/kolide/launcher/pkg/packagekit/wix" "github.com/pkg/errors" @@ -18,6 +20,10 @@ import ( //go:generate go-bindata -nometadata -nocompress -pkg internal -o internal/assets.go internal/assets/ +const ( + signtoolPath = `C:\Program Files (x86)\Windows Kits\10\bin\10.0.18362.0\x64\signtool.exe` +) + func PackageWixMSI(ctx context.Context, w io.Writer, po *PackageOptions, includeService bool) error { ctx, span := trace.StartSpan(ctx, "packagekit.PackageWixMSI") defer span.End() @@ -82,8 +88,31 @@ func PackageWixMSI(ctx context.Context, w io.Writer, po *PackageOptions, include defer wixTool.Cleanup() // Use wix to compile into an MSI - if err := wixTool.Package(ctx, w); err != nil { - return errors.Wrap(err, "running light") + msiFile, err := wixTool.Package(ctx) + if err != nil { + return errors.Wrap(err, "wix packaging") + } + + // Sign? + if po.WindowsUseSigntool { + if err := authenticode.Sign( + ctx, msiFile, + authenticode.WithExtraArgs(po.WindowsSigntoolArgs), + authenticode.WithSigntoolPath(signtoolPath), + ); err != nil { + return errors.Wrap(err, "authenticode signing") + } + } + + // Copy MSI into our filehandle + msiFH, err := os.Open(msiFile) + if err != nil { + return errors.Wrap(err, "opening msi output file") + } + defer msiFH.Close() + + if _, err := io.Copy(w, msiFH); err != nil { + return errors.Wrap(err, "copying output") } return nil diff --git a/pkg/packagekit/wix/wix.go b/pkg/packagekit/wix/wix.go index f08991b14..3bdb819b3 100644 --- a/pkg/packagekit/wix/wix.go +++ b/pkg/packagekit/wix/wix.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "fmt" - "io" "io/ioutil" "os" "os/exec" @@ -132,36 +131,25 @@ func (wo *wixTool) Cleanup() { } // Package will run through the wix steps to produce a resulting -// package. This package will be written into the provided io.Writer, -// facilitating export to a file, buffer, or other storage backends. -func (wo *wixTool) Package(ctx context.Context, pkgOutput io.Writer) error { +// package. The path for the resultant package will be returned. +func (wo *wixTool) Package(ctx context.Context) (string, error) { if err := wo.heat(ctx); err != nil { - return errors.Wrap(err, "running heat") + return "", errors.Wrap(err, "running heat") } if err := wo.addServices(ctx); err != nil { - return errors.Wrap(err, "adding services") + return "", errors.Wrap(err, "adding services") } if err := wo.candle(ctx); err != nil { - return errors.Wrap(err, "running candle") + return "", errors.Wrap(err, "running candle") } if err := wo.light(ctx); err != nil { - return errors.Wrap(err, "running light") + return "", errors.Wrap(err, "running light") } - msiFH, err := os.Open(filepath.Join(wo.buildDir, "out.msi")) - if err != nil { - return errors.Wrap(err, "opening msi output file") - } - defer msiFH.Close() - - if _, err := io.Copy(pkgOutput, msiFH); err != nil { - return errors.Wrap(err, "copying output") - } - - return nil + return filepath.Join(wo.buildDir, "out.msi"), nil } // addServices adds service definitions into the wix configs. diff --git a/pkg/packagekit/wix/wix_test.go b/pkg/packagekit/wix/wix_test.go index ed20ae224..2c80dbdd4 100644 --- a/pkg/packagekit/wix/wix_test.go +++ b/pkg/packagekit/wix/wix_test.go @@ -39,10 +39,6 @@ func TestWixPackage(t *testing.T) { err = setupPackageRoot(packageRoot) require.NoError(t, err) - outMsi, err := ioutil.TempFile("", "wix-test-*.msi") - require.NoError(t, err) - defer os.Remove(outMsi.Name()) - mainWxsContent, err := testdata.Asset("testdata/assets/product.wxs") require.NoError(t, err) @@ -57,7 +53,7 @@ func TestWixPackage(t *testing.T) { require.NoError(t, err) defer wixTool.Cleanup() - err = wixTool.Package(ctx, outMsi) + outMsi, err := wixTool.Package(ctx) require.NoError(t, err) verifyMsi(ctx, t, outMsi) @@ -65,16 +61,16 @@ func TestWixPackage(t *testing.T) { // verifyMSI attempts to very MSI correctness. It leverages 7zip, // which can mostly read MSI files. -func verifyMsi(ctx context.Context, t *testing.T, outMsi *os.File) { +func verifyMsi(ctx context.Context, t *testing.T, outMsi string) { // Use the wix struct for its execOut execWix := &wixTool{execCC: exec.CommandContext} - fileContents, err := execWix.execOut(ctx, "7z", "x", "-so", outMsi.Name()) + fileContents, err := execWix.execOut(ctx, "7z", "x", "-so", outMsi) require.NoError(t, err) require.Contains(t, fileContents, "Hello") require.Contains(t, fileContents, "Vroom Vroom") - listOutput, err := execWix.execOut(ctx, "7z", "l", outMsi.Name()) + listOutput, err := execWix.execOut(ctx, "7z", "l", outMsi) require.NoError(t, err) require.Contains(t, listOutput, "Path = go.cab") require.Contains(t, listOutput, "2 files") diff --git a/pkg/packaging/packaging.go b/pkg/packaging/packaging.go index e11872498..0b8c639be 100644 --- a/pkg/packaging/packaging.go +++ b/pkg/packaging/packaging.go @@ -32,7 +32,6 @@ type PackageOptions struct { ExtensionVersion string Hostname string Secret string - SigningKey string Transport string Insecure bool InsecureTransport bool @@ -46,6 +45,10 @@ type PackageOptions struct { RootPEM string CacheDir string + AppleSigningKey string // apple signing key + WindowsUseSigntool bool // whether to use signtool.exe on windows + WindowsSigntoolArgs []string // Extra args for signtool. May be needed for finding a key + target Target // Target build platform initOptions *packagekit.InitOptions // options we'll pass to the packagekit renderers packagekitops *packagekit.PackageOptions // options for packagekit packagers @@ -243,13 +246,15 @@ func (p *PackageOptions) Build(ctx context.Context, packageWriter io.Writer, tar } p.packagekitops = &packagekit.PackageOptions{ - Name: "launcher", - Identifier: p.Identifier, - Root: p.packageRoot, - Scripts: p.scriptRoot, - SigningKey: p.SigningKey, - Version: p.PackageVersion, - FlagFile: p.canonicalizePath(flagFilePath), + Name: "launcher", + Identifier: p.Identifier, + Root: p.packageRoot, + Scripts: p.scriptRoot, + AppleSigningKey: p.AppleSigningKey, + WindowsUseSigntool: p.WindowsUseSigntool, + WindowsSigntoolArgs: p.WindowsSigntoolArgs, + Version: p.PackageVersion, + FlagFile: p.canonicalizePath(flagFilePath), } if err := p.makePackage(ctx); err != nil {