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 {