diff --git a/cmd/launcher/main.go b/cmd/launcher/main.go index 422cf7e55..78edaae23 100644 --- a/cmd/launcher/main.go +++ b/cmd/launcher/main.go @@ -159,6 +159,8 @@ func runSubcommands() error { run = runDownloadOsquery case "uninstall": run = runUninstall + case "secure-enclave": + run = runSecureEnclave default: return fmt.Errorf("unknown subcommand %s", os.Args[1]) } diff --git a/cmd/launcher/secure_enclave_darwin.go b/cmd/launcher/secure_enclave_darwin.go new file mode 100644 index 000000000..90ce67dd1 --- /dev/null +++ b/cmd/launcher/secure_enclave_darwin.go @@ -0,0 +1,48 @@ +//go:build darwin +// +build darwin + +package main + +import ( + "errors" + "fmt" + "os" + + "github.com/kolide/krypto/pkg/echelper" + "github.com/kolide/krypto/pkg/secureenclave" + "github.com/kolide/launcher/ee/secureenclavesigner" +) + +// runSecureEnclave performs either a create-key operation using the secure enclave. +// It's available as a separate command because launcher runs as root by default and since it's +// not in a user security context, it can't use the secure enclave directly. However, this command +// can be run in the user context using launchctl. +func runSecureEnclave(args []string) error { + // currently we are just creating key, but plan to add sign command in future + if len(args) < 1 { + return errors.New("not enough arguments, expect create_key") + } + + switch args[0] { + case secureenclavesigner.CreateKeyCmd: + return createSecureEnclaveKey() + + default: + return fmt.Errorf("unknown command %s", args[0]) + } +} + +func createSecureEnclaveKey() error { + secureEnclavePubKey, err := secureenclave.CreateKey() + if err != nil { + return fmt.Errorf("creating secure enclave key: %w", err) + } + + secureEnclavePubDer, err := echelper.PublicEcdsaToB64Der(secureEnclavePubKey) + if err != nil { + return fmt.Errorf("marshalling public key to der: %w", err) + } + + os.Stdout.Write(secureEnclavePubDer) + return nil +} diff --git a/cmd/launcher/secure_enclave_other.go b/cmd/launcher/secure_enclave_other.go new file mode 100644 index 000000000..6192b77e4 --- /dev/null +++ b/cmd/launcher/secure_enclave_other.go @@ -0,0 +1,10 @@ +//go:build !darwin +// +build !darwin + +package main + +import "errors" + +func runSecureEnclave(args []string) error { + return errors.New("not implemented on non darwin platforms") +} diff --git a/cmd/launcher/secure_enclave_test.go b/cmd/launcher/secure_enclave_test.go new file mode 100644 index 000000000..debec4942 --- /dev/null +++ b/cmd/launcher/secure_enclave_test.go @@ -0,0 +1,148 @@ +//go:build darwin +// +build darwin + +package main + +import ( + "bytes" + "context" + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/kolide/krypto/pkg/echelper" + "github.com/kolide/launcher/ee/secureenclavesigner" + "github.com/stretchr/testify/require" +) + +const ( + testWrappedEnvVarKey = "SECURE_ENCLAVE_TEST_WRAPPED" + macOsAppResourceDir = "../../ee/secureenclavesigner/test_app_resources" +) + +// TestSecureEnclaveTestRunner creates a MacOS app with the binary of this packages tests, then signs the app with entitlements and runs the tests. +// This is done because in order to access secure enclave to run tests, we need MacOS entitlements. +// #nosec G306 -- Need readable files +func TestSecureEnclaveTestRunner(t *testing.T) { + t.Parallel() + + if os.Getenv("CI") != "" { + t.Skipf("\nskipping because %s env var was not empty, this is being run in a CI environment without access to secure enclave", testWrappedEnvVarKey) + } + + if os.Getenv(testWrappedEnvVarKey) != "" { + t.Skipf("\nskipping because %s env var was not empty, this is the execution of the codesigned app with entitlements", testWrappedEnvVarKey) + } + + t.Log("\nexecuting wrapped tests with codesigned app and entitlements") + + // set up app bundle + rootDir := t.TempDir() + appRoot := filepath.Join(rootDir, "launcher_test.app") + + // make required dirs launcher_test.app/Contents/MacOS and add files + require.NoError(t, os.MkdirAll(filepath.Join(appRoot, "Contents", "MacOS"), 0700)) + copyFile(t, filepath.Join(macOsAppResourceDir, "Info.plist"), filepath.Join(appRoot, "Contents", "Info.plist")) + copyFile(t, filepath.Join(macOsAppResourceDir, "embedded.provisionprofile"), filepath.Join(appRoot, "Contents", "embedded.provisionprofile")) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + // build an executable containing the tests into the app bundle + executablePath := filepath.Join(appRoot, "Contents", "MacOS", "launcher_test") + out, err := exec.CommandContext( //nolint:forbidigo // Only used in test, don't want as standard allowedcmd + ctx, + "go", + "test", + "-c", + "--cover", + "--race", + "./", + "-o", + executablePath, + ).CombinedOutput() + + require.NoError(t, ctx.Err()) + require.NoError(t, err, string(out)) + + // sign app bundle + signApp(t, appRoot) + + // run app bundle executable + cmd := exec.CommandContext(ctx, executablePath, "-test.v") //nolint:forbidigo // Only used in test, don't want as standard allowedcmd + cmd.Env = append(os.Environ(), fmt.Sprintf("%s=%s", testWrappedEnvVarKey, "true")) + out, err = cmd.CombinedOutput() + require.NoError(t, ctx.Err()) + require.NoError(t, err, string(out)) + + // ensure the test ran successfully + require.Contains(t, string(out), "PASS: TestSecureEnclaveCmd") + require.NotContains(t, string(out), "FAIL") +} + +func TestSecureEnclaveCmd(t *testing.T) { //nolint:paralleltest + if os.Getenv(testWrappedEnvVarKey) == "" { + t.Skipf("\nskipping because %s env var was empty, test not being run from codesigned app with entitlements", testWrappedEnvVarKey) + } + + t.Log("\nrunning wrapped tests with codesigned app and entitlements") + + oldStdout := os.Stdout + defer func() { + os.Stdout = oldStdout + }() + + // create a pipe to capture stdout + pipeReader, pipeWriter, err := os.Pipe() + require.NoError(t, err) + + os.Stdout = pipeWriter + + require.NoError(t, runSecureEnclave([]string{secureenclavesigner.CreateKeyCmd})) + require.NoError(t, pipeWriter.Close()) + + var buf bytes.Buffer + _, err = buf.ReadFrom(pipeReader) + require.NoError(t, err) + + // convert response to public key + createKeyResponse := buf.Bytes() + secureEnclavePubKey, err := echelper.PublicB64DerToEcdsaKey(createKeyResponse) + require.NoError(t, err) + require.NotNil(t, secureEnclavePubKey, "should be able to get public key") +} + +// #nosec G306 -- Need readable files +func copyFile(t *testing.T, source, destination string) { + bytes, err := os.ReadFile(source) + require.NoError(t, err) + require.NoError(t, os.WriteFile(destination, bytes, 0700)) +} + +// #nosec G204 -- This triggers due to using env var in cmd, making exception for test +func signApp(t *testing.T, appRootDir string) { + codeSignId := os.Getenv("MACOS_CODESIGN_IDENTITY") + require.NotEmpty(t, codeSignId, "need MACOS_CODESIGN_IDENTITY env var to sign app, such as [Mac Developer: Jane Doe (ABCD123456)]") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cmd := exec.CommandContext( //nolint:forbidigo // Only used in test, don't want as standard allowedcmd + ctx, + "codesign", + "--deep", + "--force", + "--options", "runtime", + "--entitlements", filepath.Join(macOsAppResourceDir, "entitlements"), + "--sign", codeSignId, + "--timestamp", + appRootDir, + ) + + out, err := cmd.CombinedOutput() + require.NoError(t, ctx.Err()) + require.NoError(t, err, string(out)) +} diff --git a/ee/agent/keys.go b/ee/agent/keys.go index 23bb69637..b88914b66 100644 --- a/ee/agent/keys.go +++ b/ee/agent/keys.go @@ -5,12 +5,12 @@ import ( "crypto" "fmt" "log/slog" - "runtime" "time" "github.com/kolide/launcher/ee/agent/keys" "github.com/kolide/launcher/ee/agent/types" "github.com/kolide/launcher/pkg/backoff" + "github.com/kolide/launcher/pkg/traces" ) type keyInt interface { @@ -29,7 +29,10 @@ func LocalDbKeys() keyInt { return localDbKeys } -func SetupKeys(slogger *slog.Logger, store types.GetterSetterDeleter) error { +func SetupKeys(ctx context.Context, slogger *slog.Logger, store types.GetterSetterDeleter, skipHardwareKeys bool) error { + ctx, span := traces.StartSpan(ctx) + defer span.End() + slogger = slogger.With("component", "agentkeys") var err error @@ -40,13 +43,12 @@ func SetupKeys(slogger *slog.Logger, store types.GetterSetterDeleter) error { return fmt.Errorf("setting up local db keys: %w", err) } - // Secure Enclave is not currently supported, so don't spend startup time waiting for it to work -- see keys_darwin.go for more details. - if runtime.GOOS == "darwin" { + if skipHardwareKeys { return nil } err = backoff.WaitFor(func() error { - hwKeys, err := setupHardwareKeys(slogger, store) + hwKeys, err := setupHardwareKeys(ctx, slogger, store) if err != nil { return err } diff --git a/ee/agent/keys_darwin.go b/ee/agent/keys_darwin.go index 1f8fd4ecb..891969f21 100644 --- a/ee/agent/keys_darwin.go +++ b/ee/agent/keys_darwin.go @@ -4,46 +4,24 @@ package agent import ( - "errors" + "context" + "fmt" "log/slog" "github.com/kolide/launcher/ee/agent/types" + "github.com/kolide/launcher/ee/secureenclavesigner" + "github.com/kolide/launcher/pkg/traces" ) -// nolint:unused -func setupHardwareKeys(slogger *slog.Logger, store types.GetterSetterDeleter) (keyInt, error) { - // We're seeing issues where launcher hangs (and does not complete startup) on the - // Sonoma Beta 2 release when trying to interact with the secure enclave below, on - // CreateKey. Since we don't expect this to work at the moment anyway, we are - // short-circuiting and returning early for now. - return nil, errors.New("secure enclave is not currently supported") +func setupHardwareKeys(ctx context.Context, slogger *slog.Logger, store types.GetterSetterDeleter) (keyInt, error) { + ctx, span := traces.StartSpan(ctx) + defer span.End() - /* - _, pubData, err := fetchKeyData(store) - if err != nil { - return nil, err - } + ses, err := secureenclavesigner.New(ctx, slogger, store) + if err != nil { + traces.SetError(span, fmt.Errorf("creating secureenclave signer: %w", err)) + return nil, fmt.Errorf("creating secureenclave signer: %w", err) + } - if pubData == nil { - level.Info(logger).Log("msg", "Generating new keys") - - var err error - pubData, err = secureenclave.CreateKey() - if err != nil { - return nil, fmt.Errorf("creating key: %w", err) - } - - if err := storeKeyData(store, nil, pubData); err != nil { - clearKeyData(logger, store) - return nil, fmt.Errorf("storing key: %w", err) - } - } - - k, err := secureenclave.New(pubData) - if err != nil { - return nil, fmt.Errorf("creating secureenclave signer: %w", err) - } - - return k, nil - */ + return ses, nil } diff --git a/ee/agent/keys_tpm.go b/ee/agent/keys_tpm.go index 330cb5a8f..82d057ca2 100644 --- a/ee/agent/keys_tpm.go +++ b/ee/agent/keys_tpm.go @@ -10,10 +10,13 @@ import ( "github.com/kolide/krypto/pkg/tpm" "github.com/kolide/launcher/ee/agent/types" + "github.com/kolide/launcher/pkg/traces" ) -// nolint:unused -func setupHardwareKeys(slogger *slog.Logger, store types.GetterSetterDeleter) (keyInt, error) { +func setupHardwareKeys(ctx context.Context, slogger *slog.Logger, store types.GetterSetterDeleter) (keyInt, error) { + _, span := traces.StartSpan(ctx) + defer span.End() + priData, pubData, err := fetchKeyData(store) if err != nil { return nil, err @@ -28,17 +31,24 @@ func setupHardwareKeys(slogger *slog.Logger, store types.GetterSetterDeleter) (k priData, pubData, err = tpm.CreateKey() if err != nil { clearKeyData(slogger, store) + traces.SetError(span, fmt.Errorf("creating key: %w", err)) return nil, fmt.Errorf("creating key: %w", err) } + span.AddEvent("new_key_created") + if err := storeKeyData(store, priData, pubData); err != nil { clearKeyData(slogger, store) + traces.SetError(span, fmt.Errorf("storing key: %w", err)) return nil, fmt.Errorf("storing key: %w", err) } + + span.AddEvent("new_key_stored") } k, err := tpm.New(priData, pubData) if err != nil { + traces.SetError(span, fmt.Errorf("creating tpm signer: from new key: %w", err)) return nil, fmt.Errorf("creating tpm signer: from new key: %w", err) } diff --git a/ee/control/client_http.go b/ee/control/client_http.go index d8b195960..4c163e861 100644 --- a/ee/control/client_http.go +++ b/ee/control/client_http.go @@ -11,6 +11,7 @@ import ( "io" "net/http" "net/url" + "runtime" "time" "github.com/kolide/krypto/pkg/echelper" @@ -97,7 +98,9 @@ func (c *HTTPClient) GetConfig() (io.Reader, error) { // Calculate second signature if available hardwareKeys := agent.HardwareKeys() - if hardwareKeys.Public() != nil { + + // hardware signing is not implemented for darwin + if runtime.GOOS != "darwin" && hardwareKeys.Public() != nil { key2, err := echelper.PublicEcdsaToB64Der(hardwareKeys.Public().(*ecdsa.PublicKey)) if err != nil { return nil, fmt.Errorf("could not get key header from hardware keys: %w", err) diff --git a/ee/debug/shipper/shipper.go b/ee/debug/shipper/shipper.go index 7dad9584e..41e1ef983 100644 --- a/ee/debug/shipper/shipper.go +++ b/ee/debug/shipper/shipper.go @@ -13,6 +13,7 @@ import ( "net/url" "os" "os/user" + "runtime" "strings" "sync" "time" @@ -206,7 +207,11 @@ func signHttpRequest(req *http.Request, body []byte) { } sign(agent.LocalDbKeys(), control.HeaderKey, control.HeaderSignature, req) - sign(agent.HardwareKeys(), control.HeaderKey2, control.HeaderSignature2, req) + + // hardware signing is not implemented for darwin + if runtime.GOOS != "darwin" { + sign(agent.HardwareKeys(), control.HeaderKey2, control.HeaderSignature2, req) + } } func launcherData(k types.Knapsack, note string) ([]byte, error) { diff --git a/ee/debug/shipper/shipper_test.go b/ee/debug/shipper/shipper_test.go index 49ea7e7b7..58953a90a 100644 --- a/ee/debug/shipper/shipper_test.go +++ b/ee/debug/shipper/shipper_test.go @@ -1,6 +1,7 @@ package shipper import ( + "context" "encoding/json" "fmt" "io" @@ -56,7 +57,7 @@ func TestShip(t *testing.T) { //nolint:paralleltest name: "happy path with signing keys and enroll secret", mockKnapsack: func(t *testing.T) *typesMocks.Knapsack { configStore := inmemory.NewStore() - agent.SetupKeys(multislogger.NewNopLogger(), configStore) + agent.SetupKeys(context.TODO(), multislogger.NewNopLogger(), configStore, true) k := typesMocks.NewKnapsack(t) k.On("EnrollSecret").Return("enroll_secret_value") diff --git a/ee/localserver/krypto-ec-middleware.go b/ee/localserver/krypto-ec-middleware.go index 4efe5e604..ca9ed15ee 100644 --- a/ee/localserver/krypto-ec-middleware.go +++ b/ee/localserver/krypto-ec-middleware.go @@ -13,6 +13,7 @@ import ( "log/slog" "net/http" "net/url" + "runtime" "strings" "time" @@ -303,7 +304,8 @@ func (e *kryptoEcMiddleware) Wrap(next http.Handler) http.Handler { // it's possible the keys will be noop keys, then they will error or give nil when crypto.Signer funcs are called // krypto library has a nil check for the object but not the funcs, so if are getting nil from the funcs, just // pass nil to krypto - if e.hardwareSigner != nil && e.hardwareSigner.Public() != nil { + // hardware signing is not implemented for darwin + if runtime.GOOS != "darwin" && e.hardwareSigner != nil && e.hardwareSigner.Public() != nil { response, err = challengeBox.Respond(e.localDbSigner, e.hardwareSigner, bhr.Bytes()) } else { response, err = challengeBox.Respond(e.localDbSigner, nil, bhr.Bytes()) diff --git a/ee/secureenclavesigner/secureenclavesigner_darwin.go b/ee/secureenclavesigner/secureenclavesigner_darwin.go new file mode 100644 index 000000000..381e804d0 --- /dev/null +++ b/ee/secureenclavesigner/secureenclavesigner_darwin.go @@ -0,0 +1,295 @@ +//go:build darwin +// +build darwin + +package secureenclavesigner + +import ( + "context" + "crypto" + "crypto/ecdsa" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "os" + "os/user" + "strings" + "sync" + + "github.com/kolide/krypto/pkg/echelper" + "github.com/kolide/launcher/ee/agent/types" + "github.com/kolide/launcher/ee/allowedcmd" + "github.com/kolide/launcher/ee/consoleuser" + "github.com/kolide/launcher/pkg/traces" +) + +const ( + CreateKeyCmd = "create-key" + PublicEccDataKey = "publicEccData" +) + +type opt func(*secureEnclaveSigner) + +type secureEnclaveSigner struct { + uidPubKeyMap map[string]*ecdsa.PublicKey + pathToLauncherBinary string + store types.GetterSetterDeleter + slogger *slog.Logger + mux *sync.Mutex +} + +func New(ctx context.Context, slogger *slog.Logger, store types.GetterSetterDeleter, opts ...opt) (*secureEnclaveSigner, error) { + ctx, span := traces.StartSpan(ctx) + defer span.End() + + ses := &secureEnclaveSigner{ + uidPubKeyMap: make(map[string]*ecdsa.PublicKey), + store: store, + slogger: slogger.With("component", "secureenclavesigner"), + mux: &sync.Mutex{}, + } + + data, err := store.Get([]byte(PublicEccDataKey)) + if err != nil { + traces.SetError(span, fmt.Errorf("getting public ecc data from store: %w", err)) + return nil, fmt.Errorf("getting public ecc data from store: %w", err) + } + + if data != nil { + if err := json.Unmarshal(data, ses); err != nil { + traces.SetError(span, fmt.Errorf("unmarshaling secure enclave signer: %w", err)) + ses.slogger.Log(ctx, slog.LevelError, + "unable to unmarshal secure enclave signer, data may be corrupt, wiping", + "err", err, + ) + + if err := store.Delete([]byte(PublicEccDataKey)); err != nil { + traces.SetError(span, fmt.Errorf("deleting corrupt public ecc data: %w", err)) + return nil, fmt.Errorf("deleting corrupt public ecc data: %w", err) + } + } + } + + for _, opt := range opts { + opt(ses) + } + + if ses.pathToLauncherBinary == "" { + p, err := os.Executable() + if err != nil { + traces.SetError(span, fmt.Errorf("getting path to launcher binary: %w", err)) + return nil, fmt.Errorf("getting path to launcher binary: %w", err) + } + + ses.pathToLauncherBinary = p + } + + // get current console user key to make sure it's available + if _, err := ses.currentConsoleUserKey(ctx); err != nil { + traces.SetError(span, fmt.Errorf("getting current console user key: %w", err)) + ses.slogger.Log(ctx, slog.LevelError, + "getting current console user key", + "err", err, + ) + + // intentionally not returning error here, because this runs on start up + // and maybe the console user or secure enclave is not available yet + } + + return ses, nil +} + +// Public returns the public key of the current console user +// creating and peristing a new one if needed +func (ses *secureEnclaveSigner) Public() crypto.PublicKey { + k, err := ses.currentConsoleUserKey(context.TODO()) + if err != nil { + ses.slogger.Log(context.TODO(), slog.LevelError, + "getting public key", + "err", err, + ) + return nil + } + + return k +} + +func (ses *secureEnclaveSigner) Type() string { + return "secure_enclave" +} + +func (ses *secureEnclaveSigner) Sign(rand io.Reader, digest []byte, opts crypto.SignerOpts) ([]byte, error) { + return nil, fmt.Errorf("not implemented") +} + +type keyData struct { + Uid string `json:"uid"` + PubKey string `json:"pub_key"` +} + +func (ses *secureEnclaveSigner) currentConsoleUserKey(ctx context.Context) (*ecdsa.PublicKey, error) { + ctx, span := traces.StartSpan(ctx) + defer span.End() + + ses.mux.Lock() + defer ses.mux.Unlock() + + cu, err := firstConsoleUser(ctx) + if err != nil { + traces.SetError(span, fmt.Errorf("getting first console user: %w", err)) + return nil, fmt.Errorf("getting first console user: %w", err) + } + + key, ok := ses.uidPubKeyMap[cu.Uid] + if ok { + span.AddEvent("found_existing_key_for_console_user") + return key, nil + } + + key, err = ses.createKey(ctx, cu) + if err != nil { + traces.SetError(span, fmt.Errorf("creating key: %w", err)) + return nil, fmt.Errorf("creating key: %w", err) + } + + span.AddEvent("created_new_key_for_console_user") + + ses.uidPubKeyMap[cu.Uid] = key + if err := ses.save(); err != nil { + delete(ses.uidPubKeyMap, cu.Uid) + traces.SetError(span, fmt.Errorf("saving secure enclave signer: %w", err)) + return nil, fmt.Errorf("saving secure enclave signer: %w", err) + } + + span.AddEvent("saved_key_for_console_user") + return key, nil +} + +func (ses *secureEnclaveSigner) MarshalJSON() ([]byte, error) { + var keyDatas []keyData + + for uid, pubKey := range ses.uidPubKeyMap { + pubKeyBytes, err := echelper.PublicEcdsaToB64Der(pubKey) + if err != nil { + return nil, fmt.Errorf("converting public key to b64 der: %w", err) + } + + keyDatas = append(keyDatas, keyData{ + Uid: uid, + PubKey: string(pubKeyBytes), + }) + + } + + return json.Marshal(keyDatas) +} + +func (ses *secureEnclaveSigner) UnmarshalJSON(data []byte) error { + if ses.uidPubKeyMap == nil { + ses.uidPubKeyMap = make(map[string]*ecdsa.PublicKey) + } + + var keyDatas []keyData + if err := json.Unmarshal(data, &keyDatas); err != nil { + return fmt.Errorf("unmarshalling key data: %w", err) + } + + for _, kd := range keyDatas { + pubKey, err := echelper.PublicB64DerToEcdsaKey([]byte(kd.PubKey)) + if err != nil { + return fmt.Errorf("converting public key to ecdsa: %w", err) + } + + ses.uidPubKeyMap[kd.Uid] = pubKey + } + + return nil +} + +func (ses *secureEnclaveSigner) createKey(ctx context.Context, u *user.User) (*ecdsa.PublicKey, error) { + ctx, span := traces.StartSpan(ctx) + defer span.End() + + cmd, err := allowedcmd.Launchctl( + ctx, + "asuser", + u.Uid, + "sudo", + "--preserve-env", + "-u", + u.Username, + ses.pathToLauncherBinary, + "secure-enclave", + CreateKeyCmd, + ) + + if err != nil { + traces.SetError(span, fmt.Errorf("creating command to create key: %w", err)) + return nil, fmt.Errorf("creating command to create key: %w", err) + } + + // skip updates since we have full path of binary + cmd.Env = append(cmd.Environ(), fmt.Sprintf("%s=%s", "LAUNCHER_SKIP_UPDATES", "true")) + out, err := cmd.CombinedOutput() + if err != nil { + traces.SetError(span, fmt.Errorf("executing launcher binary to create key: %w: %s", err, string(out))) + return nil, fmt.Errorf("executing launcher binary to create key: %w: %s", err, string(out)) + } + + pubKey, err := echelper.PublicB64DerToEcdsaKey([]byte(lastLine(out))) + if err != nil { + traces.SetError(span, fmt.Errorf("converting public key to ecdsa: %w", err)) + return nil, fmt.Errorf("converting public key to ecdsa: %w", err) + } + + return pubKey, nil +} + +// lastLine returns the last line of the out. +// This is needed because laucher sets up a logger by default. +// The last line of the output is the public key or signature. +func lastLine(out []byte) string { + outStr := string(out) + + // get last line of outstr + lastLine := "" + for _, line := range strings.Split(outStr, "\n") { + if line != "" { + lastLine = line + } + } + + return lastLine +} + +func firstConsoleUser(ctx context.Context) (*user.User, error) { + ctx, span := traces.StartSpan(ctx) + defer span.End() + + c, err := consoleuser.CurrentUsers(ctx) + if err != nil { + traces.SetError(span, fmt.Errorf("getting current users: %w", err)) + return nil, fmt.Errorf("getting current users: %w", err) + } + + if len(c) == 0 { + traces.SetError(span, errors.New("no console users found")) + return nil, errors.New("no console users found") + } + + return c[0], nil +} + +func (ses *secureEnclaveSigner) save() error { + json, err := json.Marshal(ses) + if err != nil { + return fmt.Errorf("marshaling secure enclave signer: %w", err) + } + + if err := ses.store.Set([]byte(PublicEccDataKey), json); err != nil { + return fmt.Errorf("setting public ecc data: %w", err) + } + + return nil +} diff --git a/ee/secureenclavesigner/secureenclavesigner_test.go b/ee/secureenclavesigner/secureenclavesigner_test.go new file mode 100644 index 000000000..ab834e84d --- /dev/null +++ b/ee/secureenclavesigner/secureenclavesigner_test.go @@ -0,0 +1,166 @@ +//go:build darwin +// +build darwin + +package secureenclavesigner + +import ( + "context" + "crypto/ecdsa" + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/kolide/krypto/pkg/echelper" + "github.com/kolide/launcher/ee/agent/storage/inmemory" + "github.com/kolide/launcher/pkg/log/multislogger" + "github.com/stretchr/testify/require" +) + +const ( + testWrappedEnvVarKey = "SECURE_ENCLAVE_TEST_WRAPPED" + macOsAppResourceDir = "./test_app_resources" +) + +func WithBinaryPath(p string) opt { + return func(ses *secureEnclaveSigner) { + ses.pathToLauncherBinary = p + } +} + +// #nosec G306 -- Need readable files +func TestSecureEnclaveSigner(t *testing.T) { + t.Parallel() + + if os.Getenv("CI") != "" { + t.Skipf("\nskipping because %s env var was not empty, this is being run in a CI environment without access to secure enclave", testWrappedEnvVarKey) + } + + // put the root dir somewhere else if you want to persist the signed macos app bundle + // should build this into make at some point + rootDir := "/tmp/secure_enclave_test" + + // rootDir := t.TempDir() + appRoot := filepath.Join(rootDir, "launcher_test.app") + + // make required dirs krypto_test.app/Contents/MacOS and add files + require.NoError(t, os.MkdirAll(filepath.Join(appRoot, "Contents", "MacOS"), 0777)) + copyFile(t, filepath.Join(macOsAppResourceDir, "Info.plist"), filepath.Join(appRoot, "Contents", "Info.plist")) + copyFile(t, filepath.Join(macOsAppResourceDir, "embedded.provisionprofile"), filepath.Join(appRoot, "Contents", "embedded.provisionprofile")) + + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + serverPrivKey, err := echelper.GenerateEcdsaKey() + require.NoError(t, err) + + serverPubKeyDer, err := echelper.PublicEcdsaToB64Der(serverPrivKey.Public().(*ecdsa.PublicKey)) + require.NoError(t, err) + + // build the executable + executablePath := filepath.Join(appRoot, "Contents", "MacOS", "launcher_test") + out, err := exec.CommandContext( //nolint:forbidigo // Only used in test, don't want as standard allowedcmd + ctx, + "go", + "build", + "-ldflags", + fmt.Sprintf("-X github.com/kolide/launcher/ee/secureenclavesigner.TestServerPubKey=%s", string(serverPubKeyDer)), + "-tags", + "secure_enclave_test", + "-o", + executablePath, + "../../cmd/launcher", + ).CombinedOutput() + + require.NoError(t, ctx.Err()) + require.NoError(t, err, string(out)) + + // sign app bundle + signApp(t, appRoot) + + store := inmemory.NewStore() + + // create brand new signer without existing key + // ask for public first to trigger key generation + ses, err := New(context.TODO(), multislogger.NewNopLogger(), store, WithBinaryPath(executablePath)) + require.NoError(t, err, + "should be able to create secure enclave signer", + ) + + pubKey := ses.Public() + require.NotNil(t, pubKey, + "should be able to create brand new public key", + ) + + pubEcdsaKey := pubKey.(*ecdsa.PublicKey) + require.NotNil(t, pubEcdsaKey, + "public key should convert to ecdsa key", + ) + + pubKeySame := ses.Public() + require.NotNil(t, pubKeySame, + "should be able to get public key again", + ) + + pubEcdsaKeySame := pubKeySame.(*ecdsa.PublicKey) + require.NotNil(t, pubEcdsaKeySame, + "public key should convert to ecdsa key", + ) + + require.Equal(t, pubEcdsaKey, pubEcdsaKeySame, + "asking for the same public key should return the same key", + ) + + existingDataSes, err := New(context.TODO(), multislogger.NewNopLogger(), store, WithBinaryPath(executablePath)) + require.NoError(t, err, + "should be able to create secure enclave signer with existing key", + ) + + pubKeyUnmarshalled := existingDataSes.Public() + require.NotNil(t, pubKeyUnmarshalled, + "should be able to get public key from unmarshalled secure enclave signer", + ) + + pubEcdsaKeyUnmarshalled := pubKeyUnmarshalled.(*ecdsa.PublicKey) + require.NotNil(t, pubEcdsaKeyUnmarshalled, + "public key should convert to ecdsa key", + ) + + require.Equal(t, pubEcdsaKey, pubEcdsaKeyUnmarshalled, + "unmarshalled public key should be the same as original public key", + ) +} + +// #nosec G306 -- Need readable files +func copyFile(t *testing.T, source, destination string) { + bytes, err := os.ReadFile(source) + require.NoError(t, err) + require.NoError(t, os.WriteFile(destination, bytes, 0700)) +} + +// #nosec G204 -- This triggers due to using env var in cmd, making exception for test +func signApp(t *testing.T, appRootDir string) { + codeSignId := os.Getenv("MACOS_CODESIGN_IDENTITY") + require.NotEmpty(t, codeSignId, "need MACOS_CODESIGN_IDENTITY env var to sign app, such as [Mac Developer: Jane Doe (ABCD123456)]") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + cmd := exec.CommandContext( //nolint:forbidigo // Only used in test, don't want as standard allowcmd + ctx, + "codesign", + "--deep", + "--force", + "--options", "runtime", + "--entitlements", filepath.Join(macOsAppResourceDir, "entitlements"), + "--sign", codeSignId, + "--timestamp", + appRootDir, + ) + + out, err := cmd.CombinedOutput() + require.NoError(t, ctx.Err()) + require.NoError(t, err, string(out)) +} diff --git a/ee/secureenclavesigner/test_app_resources/embedded.provisionprofile b/ee/secureenclavesigner/test_app_resources/embedded.provisionprofile new file mode 100644 index 000000000..148e28081 Binary files /dev/null and b/ee/secureenclavesigner/test_app_resources/embedded.provisionprofile differ diff --git a/ee/secureenclavesigner/test_app_resources/entitlements b/ee/secureenclavesigner/test_app_resources/entitlements new file mode 100644 index 000000000..6969bc4e3 --- /dev/null +++ b/ee/secureenclavesigner/test_app_resources/entitlements @@ -0,0 +1,8 @@ + + + keychain-access-groups + + X98UFR7HA3.com.kolide.agent + + + diff --git a/ee/secureenclavesigner/test_app_resources/info.plist b/ee/secureenclavesigner/test_app_resources/info.plist new file mode 100644 index 000000000..fe801acec --- /dev/null +++ b/ee/secureenclavesigner/test_app_resources/info.plist @@ -0,0 +1,20 @@ + + + + + CFBundleExecutable + launcher_test + CFBundleIdentifier + com.kolide.agent + CFBundleName + launcher_test + LSUIElement + + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.1 + CFBundleVersion + 0.1 + + diff --git a/ee/secureenclavesigner/test_app_resources/readme.md b/ee/secureenclavesigner/test_app_resources/readme.md new file mode 100644 index 000000000..bd2d3578f --- /dev/null +++ b/ee/secureenclavesigner/test_app_resources/readme.md @@ -0,0 +1,25 @@ +# Running Tests + +The files in this directory are used only for testing. + +The secure enclave keyer requires apple entitlements in order to be able to access the secure enclave to generate keys and perform cryptographic operations. In order to do this we build the secure enclave go tests to a binary, sign that binary with the required MacOS entitlements, then execute the binary and inspect the output. This is all done via the `TestSecureEnclaveTestRunner` function. + +In order to add entitlements we first need to create a MacOS app with the following structure: + +```sh +launcher_test.app + └── Contents + ├── Info.plist + ├── MacOS + │ └── launcher_test # <- this is the go test binary mentioned above + └── embedded.provisionprofile +``` + +Then we pass the top level directory to the MacOS codsign utility. + +In order to succesfully sign the app with entitlements, there are a few steps that must be completed on the machine in order to run the tests. + +1. Download and install a certificate from the Apple Developer account of type "Mac Development" https://developer.apple.com/account/resources/certificates/list +2. Add you device to the developer account using the "Provisioning UDID" found at Desktop Menu Applie Icon> About This Mac > More Info > System Report https://developer.apple.com/account/resources/devices/list +3. Create a provisioing profile that includes the device https://developer.apple.com/account/resources/profiles/list ... should probably include all devices on the team and be updated in the repo +4. Replace the `embedded.provisionprofile` file with the new profile diff --git a/go.mod b/go.mod index d448538c6..7be660131 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/groob/plist v0.0.0-20190114192801-a99fbe489d03 github.com/knightsc/system_policy v1.1.1-0.20211029142728-5f4c0d5419cc github.com/kolide/kit v0.0.0-20221107170827-fb85e3d59eab - github.com/kolide/krypto v0.1.1-0.20231219012048-5859599c50aa + github.com/kolide/krypto v0.1.1-0.20231229162826-db516b7e0121 github.com/mat/besticon v3.9.0+incompatible github.com/mattn/go-sqlite3 v1.14.19 github.com/mixer/clock v0.0.0-20170901150240-b08e6b4da7ea diff --git a/go.sum b/go.sum index 7c731641b..8c7faed19 100644 --- a/go.sum +++ b/go.sum @@ -167,8 +167,8 @@ github.com/knightsc/system_policy v1.1.1-0.20211029142728-5f4c0d5419cc h1:g2S0GQ github.com/knightsc/system_policy v1.1.1-0.20211029142728-5f4c0d5419cc/go.mod h1:5e34JEkxWsOeAd9jvcxkz01tAY/JAGFuabGnNBJ6TT4= github.com/kolide/kit v0.0.0-20221107170827-fb85e3d59eab h1:KVR7cs+oPyy85i+8t1ZaNSy1bymCy5FuWyt51pdrXu4= github.com/kolide/kit v0.0.0-20221107170827-fb85e3d59eab/go.mod h1:OYYulo9tUqRadRLwB0+LE914sa1ui2yL7OrcU3Q/1XY= -github.com/kolide/krypto v0.1.1-0.20231219012048-5859599c50aa h1:3agrIh6HWiEZAvH3ubVpfsaRFsg1Ux+1S5HU+HE5pPI= -github.com/kolide/krypto v0.1.1-0.20231219012048-5859599c50aa/go.mod h1:/0sxd3OIxciTlMTeZI/9WTaUHsx/K/+3f+NbD5dywTY= +github.com/kolide/krypto v0.1.1-0.20231229162826-db516b7e0121 h1:f7APX9VNsCkD/tdlAjbU4A22FyfTOCF6QadlvnzZElg= +github.com/kolide/krypto v0.1.1-0.20231229162826-db516b7e0121/go.mod h1:/0sxd3OIxciTlMTeZI/9WTaUHsx/K/+3f+NbD5dywTY= github.com/kolide/systray v1.10.4 h1:eBhnVfhW0fGal1KBkBZC9fzRs4yrxUymgiXuQh5MBSg= github.com/kolide/systray v1.10.4/go.mod h1:FwK9yUmU3JO+vA7TOLQSFRgEQ3euLxOqic5qlBtFrik= github.com/kolide/toast v1.0.2 h1:BQlIfO3wbKIEWfF0c8v4UkdhSIZYnSWaKkZl+Yarptk= diff --git a/pkg/osquery/extension.go b/pkg/osquery/extension.go index 583b03064..294340006 100644 --- a/pkg/osquery/extension.go +++ b/pkg/osquery/extension.go @@ -92,6 +92,9 @@ type ExtensionOpts struct { // RunDifferentialQueriesImmediately allows the client to execute a new query the first time it sees it, // bypassing the scheduler. RunDifferentialQueriesImmediately bool + // skipHardwareKeysSetup is a flag to indicate if we should skip setting up hardware keys. + // This is useful for testing environments where we don't have required hardware. + skipHardwareKeysSetup bool } // NewExtension creates a new Extension from the provided service.KolideService @@ -125,7 +128,7 @@ func NewExtension(ctx context.Context, client service.KolideService, k types.Kna return nil, fmt.Errorf("setting up initial launcher keys: %w", err) } - if err := agent.SetupKeys(slogger, configStore); err != nil { + if err := agent.SetupKeys(ctx, slogger, configStore, opts.skipHardwareKeysSetup); err != nil { return nil, fmt.Errorf("setting up agent keys: %w", err) } diff --git a/pkg/osquery/extension_test.go b/pkg/osquery/extension_test.go index a9fa0ed98..07de49f04 100644 --- a/pkg/osquery/extension_test.go +++ b/pkg/osquery/extension_test.go @@ -70,7 +70,9 @@ func TestNewExtensionEmptyEnrollSecret(t *testing.T) { m.On("ReadEnrollSecret").Maybe().Return("", errors.New("test")) // We should be able to make an extension despite an empty enroll secret - e, err := NewExtension(context.TODO(), &mock.KolideService{}, m, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), &mock.KolideService{}, m, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) assert.Nil(t, err) assert.NotNil(t, e) } @@ -100,7 +102,9 @@ func TestNewExtensionDatabaseError(t *testing.T) { m.On("ConfigStore").Return(agentbbolt.NewStore(multislogger.NewNopLogger(), db, storage.ConfigStore.String())) m.On("Slogger").Return(multislogger.NewNopLogger()).Maybe() - e, err := NewExtension(context.TODO(), &mock.KolideService{}, m, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), &mock.KolideService{}, m, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) assert.NotNil(t, err) assert.Nil(t, e) } @@ -110,7 +114,9 @@ func TestGetHostIdentifier(t *testing.T) { defer cleanup() k := makeKnapsack(t, db) - e, err := NewExtension(context.TODO(), &mock.KolideService{}, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), &mock.KolideService{}, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) ident, err := e.getHostIdentifier() @@ -125,7 +131,9 @@ func TestGetHostIdentifier(t *testing.T) { db, cleanup = makeTempDB(t) defer cleanup() k = makeKnapsack(t, db) - e, err = NewExtension(context.TODO(), &mock.KolideService{}, k, ExtensionOpts{}) + e, err = NewExtension(context.TODO(), &mock.KolideService{}, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) ident, err = e.getHostIdentifier() @@ -140,7 +148,9 @@ func TestGetHostIdentifierCorruptedData(t *testing.T) { db, cleanup := makeTempDB(t) defer cleanup() k := makeKnapsack(t, db) - e, err := NewExtension(context.TODO(), &mock.KolideService{}, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), &mock.KolideService{}, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) // Put garbage UUID in DB @@ -169,7 +179,9 @@ func TestExtensionEnrollTransportError(t *testing.T) { defer cleanup() k := makeKnapsack(t, db) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) key, invalid, err := e.Enroll(context.Background()) @@ -189,7 +201,9 @@ func TestExtensionEnrollSecretInvalid(t *testing.T) { db, cleanup := makeTempDB(t) k := makeKnapsack(t, db) defer cleanup() - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) key, invalid, err := e.Enroll(context.Background()) @@ -218,7 +232,9 @@ func TestExtensionEnroll(t *testing.T) { expectedEnrollSecret := "foo_secret" k.On("ReadEnrollSecret").Maybe().Return(expectedEnrollSecret, nil) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) key, invalid, err := e.Enroll(context.Background()) @@ -237,7 +253,9 @@ func TestExtensionEnroll(t *testing.T) { assert.Equal(t, expectedNodeKey, key) assert.Equal(t, expectedEnrollSecret, gotEnrollSecret) - e, err = NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err = NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) // Still should not re-enroll (because node key stored in DB) key, invalid, err = e.Enroll(context.Background()) @@ -271,7 +289,9 @@ func TestExtensionGenerateConfigsTransportError(t *testing.T) { defer cleanup() k := makeKnapsack(t, db) k.ConfigStore().Set([]byte(nodeKeyKey), []byte("some_node_key")) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) configs, err := e.GenerateConfigs(context.Background()) @@ -292,7 +312,9 @@ func TestExtensionGenerateConfigsCaching(t *testing.T) { db, cleanup := makeTempDB(t) defer cleanup() k := makeKnapsack(t, db) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) configs, err := e.GenerateConfigs(context.Background()) @@ -329,7 +351,9 @@ func TestExtensionGenerateConfigsEnrollmentInvalid(t *testing.T) { db, cleanup := makeTempDB(t) defer cleanup() k := makeKnapsack(t, db) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) e.NodeKey = "bad_node_key" @@ -356,7 +380,9 @@ func TestGenerateConfigs_CannotEnrollYet(t *testing.T) { k.On("Slogger").Return(multislogger.NewNopLogger()) k.On("ReadEnrollSecret").Maybe().Return("", errors.New("test")) - e, err := NewExtension(context.TODO(), s, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), s, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) configs, err := e.GenerateConfigs(context.Background()) @@ -385,7 +411,9 @@ func TestExtensionGenerateConfigs(t *testing.T) { db, cleanup := makeTempDB(t) defer cleanup() k := makeKnapsack(t, db) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) configs, err := e.GenerateConfigs(context.Background()) @@ -404,7 +432,9 @@ func TestExtensionWriteLogsTransportError(t *testing.T) { db, cleanup := makeTempDB(t) defer cleanup() k := makeKnapsack(t, db) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) err = e.writeLogsWithReenroll(context.Background(), logger.LogTypeSnapshot, []string{"foobar"}, true) @@ -428,7 +458,9 @@ func TestExtensionWriteLogsEnrollmentInvalid(t *testing.T) { db, cleanup := makeTempDB(t) defer cleanup() k := makeKnapsack(t, db) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) e.NodeKey = "bad_node_key" @@ -457,7 +489,9 @@ func TestExtensionWriteLogs(t *testing.T) { db, cleanup := makeTempDB(t) defer cleanup() k := makeKnapsack(t, db) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) e.NodeKey = expectedNodeKey @@ -533,7 +567,9 @@ func TestExtensionWriteBufferedLogsEmpty(t *testing.T) { k.On("Slogger").Return(multislogger.NewNopLogger()).Maybe() k.On("ReadEnrollSecret").Maybe().Return("enroll_secret", nil) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) // No buffered logs should result in success and no remote action being @@ -572,7 +608,9 @@ func TestExtensionWriteBufferedLogs(t *testing.T) { k.On("Slogger").Return(multislogger.NewNopLogger()).Maybe() k.On("ReadEnrollSecret").Maybe().Return("enroll_secret", nil) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) e.LogString(context.Background(), logger.LogTypeStatus, "status foo") @@ -642,7 +680,9 @@ func TestExtensionWriteBufferedLogsEnrollmentInvalid(t *testing.T) { k.On("Slogger").Return(multislogger.NewNopLogger()) k.On("ReadEnrollSecret").Maybe().Return("enroll_secret", nil) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) e.LogString(context.Background(), logger.LogTypeStatus, "status foo") @@ -687,7 +727,8 @@ func TestExtensionWriteBufferedLogsLimit(t *testing.T) { k.On("Slogger").Return(multislogger.NewNopLogger()) e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ - MaxBytesPerBatch: 100, + MaxBytesPerBatch: 100, + skipHardwareKeysSetup: true, }) require.Nil(t, err) @@ -760,7 +801,8 @@ func TestExtensionWriteBufferedLogsDropsBigLog(t *testing.T) { k.On("Slogger").Return(multislogger.NewNopLogger()) e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ - MaxBytesPerBatch: 15, + MaxBytesPerBatch: 15, + skipHardwareKeysSetup: true, }) require.Nil(t, err) @@ -844,9 +886,10 @@ func TestExtensionWriteLogsLoop(t *testing.T) { mockClock := clock.NewMockClock() expectedLoggingInterval := 10 * time.Second e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ - MaxBytesPerBatch: 200, - Clock: mockClock, - LoggingInterval: expectedLoggingInterval, + MaxBytesPerBatch: 200, + Clock: mockClock, + LoggingInterval: expectedLoggingInterval, + skipHardwareKeysSetup: true, }) require.Nil(t, err) @@ -972,7 +1015,10 @@ func TestExtensionPurgeBufferedLogs(t *testing.T) { k.On("Slogger").Return(multislogger.NewNopLogger()) max := 10 - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{MaxBufferedLogs: max}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + MaxBufferedLogs: max, + skipHardwareKeysSetup: true, + }) require.Nil(t, err) var expectedStatusLogs, expectedResultLogs []string @@ -1009,7 +1055,9 @@ func TestExtensionGetQueriesTransportError(t *testing.T) { db, cleanup := makeTempDB(t) defer cleanup() k := makeKnapsack(t, db) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) queries, err := e.GetQueries(context.Background()) @@ -1039,7 +1087,9 @@ func TestExtensionGetQueriesEnrollmentInvalid(t *testing.T) { k.On("Slogger").Return(multislogger.NewNopLogger()) k.On("ReadEnrollSecret").Return("enroll_secret", nil) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) e.NodeKey = "bad_node_key" @@ -1067,7 +1117,9 @@ func TestExtensionGetQueries(t *testing.T) { db, cleanup := makeTempDB(t) defer cleanup() k := makeKnapsack(t, db) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) queries, err := e.GetQueries(context.Background()) @@ -1086,7 +1138,9 @@ func TestExtensionWriteResultsTransportError(t *testing.T) { db, cleanup := makeTempDB(t) defer cleanup() k := makeKnapsack(t, db) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) err = e.WriteResults(context.Background(), []distributed.Result{}) @@ -1110,7 +1164,9 @@ func TestExtensionWriteResultsEnrollmentInvalid(t *testing.T) { db, cleanup := makeTempDB(t) defer cleanup() k := makeKnapsack(t, db) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) e.NodeKey = "bad_node_key" @@ -1133,7 +1189,9 @@ func TestExtensionWriteResults(t *testing.T) { db, cleanup := makeTempDB(t) defer cleanup() k := makeKnapsack(t, db) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.Nil(t, err) expectedResults := []distributed.Result{ @@ -1161,7 +1219,9 @@ func TestLauncherRsaKeys(t *testing.T) { k.On("ConfigStore").Return(configStore) k.On("Slogger").Return(multislogger.NewNopLogger()) - _, err = NewExtension(context.TODO(), m, k, ExtensionOpts{}) + _, err = NewExtension(context.TODO(), m, k, ExtensionOpts{ + skipHardwareKeysSetup: true, + }) require.NoError(t, err) key, err := PrivateRSAKeyFromDB(configStore) diff --git a/pkg/osquery/log_publication_state_test.go b/pkg/osquery/log_publication_state_test.go index a4f129a77..f00c82532 100644 --- a/pkg/osquery/log_publication_state_test.go +++ b/pkg/osquery/log_publication_state_test.go @@ -23,7 +23,10 @@ func TestExtensionLogPublicationHappyPath(t *testing.T) { db, cleanup := makeTempDB(t) defer cleanup() k := makeKnapsack(t, db) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{MaxBytesPerBatch: startingBatchLimitBytes}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + MaxBytesPerBatch: startingBatchLimitBytes, + skipHardwareKeysSetup: true, + }) require.Nil(t, err) // issue a few successful calls, expect that the batch limit is unchanged from the original opts @@ -57,7 +60,10 @@ func TestExtensionLogPublicationRespondsToNetworkTimeouts(t *testing.T) { db, cleanup := makeTempDB(t) defer cleanup() k := makeKnapsack(t, db) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{MaxBytesPerBatch: startingBatchLimitBytes}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + MaxBytesPerBatch: startingBatchLimitBytes, + skipHardwareKeysSetup: true, + }) require.Nil(t, err) // expect each subsequent failed call to reduce the batch size until the min threshold is reached @@ -106,7 +112,10 @@ func TestExtensionLogPublicationIgnoresNonTimeoutErrors(t *testing.T) { db, cleanup := makeTempDB(t) defer cleanup() k := makeKnapsack(t, db) - e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{MaxBytesPerBatch: startingBatchLimitBytes}) + e, err := NewExtension(context.TODO(), m, k, ExtensionOpts{ + MaxBytesPerBatch: startingBatchLimitBytes, + skipHardwareKeysSetup: true, + }) require.Nil(t, err) // issue a few calls that error immediately, expect that the batch limit is unchanged from the original opts diff --git a/pkg/osquery/table/launcher_info.go b/pkg/osquery/table/launcher_info.go index 4369e44a3..efbfb4b17 100644 --- a/pkg/osquery/table/launcher_info.go +++ b/pkg/osquery/table/launcher_info.go @@ -5,6 +5,8 @@ import ( "context" "crypto/x509" "encoding/base64" + "encoding/json" + "fmt" "runtime" "github.com/kolide/kit/version" @@ -98,6 +100,17 @@ func generateLauncherInfoTable(store types.GetterSetter) table.GenerateFunc { return results, nil } + if runtime.GOOS == "darwin" && agent.HardwareKeys() != nil && agent.HardwareKeys().Public() != nil { + jsonBytes, err := json.Marshal(agent.HardwareKeys()) + if err != nil { + return nil, fmt.Errorf("marshalling hardware keys: %w", err) + } + results[0]["hardware_key"] = string(jsonBytes) + results[0]["hardware_key_source"] = agent.HardwareKeys().Type() + + return results, nil + } + if hardwareKeyDer, err := x509.MarshalPKIXPublicKey(agent.HardwareKeys().Public()); err == nil { // der is a binary format, so convert to b64 results[0]["hardware_key"] = base64.StdEncoding.EncodeToString(hardwareKeyDer)