Skip to content

Commit

Permalink
Windows Signing (#489)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
directionless authored May 16, 2019
1 parent e415bd1 commit 9a39edb
Show file tree
Hide file tree
Showing 13 changed files with 322 additions and 49 deletions.
15 changes: 12 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
4 changes: 2 additions & 2 deletions cmd/package-builder/package-builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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,
Expand Down
67 changes: 67 additions & 0 deletions pkg/packagekit/authenticode/authenticode.go
Original file line number Diff line number Diff line change
@@ -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
}
9 changes: 9 additions & 0 deletions pkg/packagekit/authenticode/authenticode_dummy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// +build !windows

package authenticode

import "context"

func Sign(ctx context.Context, file string, opts ...SigntoolOpt) error {
return nil
}
68 changes: 68 additions & 0 deletions pkg/packagekit/authenticode/authenticode_test.go
Original file line number Diff line number Diff line change
@@ -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")

}
99 changes: 99 additions & 0 deletions pkg/packagekit/authenticode/authenticode_windows.go
Original file line number Diff line number Diff line change
@@ -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
}
5 changes: 4 additions & 1 deletion pkg/packagekit/package.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
4 changes: 2 additions & 2 deletions pkg/packagekit/package_pkg.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
8 changes: 4 additions & 4 deletions pkg/packagekit/package_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down
33 changes: 31 additions & 2 deletions pkg/packagekit/package_wix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 9a39edb

Please sign in to comment.