Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for collecitng multiple user keys on via secure enclave #1644

Merged
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/launcher/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])
}
Expand Down
48 changes: 48 additions & 0 deletions cmd/launcher/secure_enclave_darwin.go
Original file line number Diff line number Diff line change
@@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot decide, but we could try to examine the parent process to see if launcher is in there. Though the parent is just going to be sudo, so this may not be feasible.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Early on we considered trying to do this, but decided not to since it was easy to spoof ppid. If we're feeling different now and we do go this route, we could even exec codesign and verify that the calling binary is signed as we expect.

// 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
}
10 changes: 10 additions & 0 deletions cmd/launcher/secure_enclave_other.go
Original file line number Diff line number Diff line change
@@ -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")
}
148 changes: 148 additions & 0 deletions cmd/launcher/secure_enclave_test.go
Original file line number Diff line number Diff line change
@@ -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) {
directionless marked this conversation as resolved.
Show resolved Hide resolved
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))
}
6 changes: 0 additions & 6 deletions ee/agent/keys.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import (
"crypto"
"fmt"
"log/slog"
"runtime"
"time"

"github.com/kolide/launcher/ee/agent/keys"
Expand Down Expand Up @@ -40,11 +39,6 @@ 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.
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved
if runtime.GOOS == "darwin" {
return nil
}

err = backoff.WaitFor(func() error {
hwKeys, err := setupHardwareKeys(slogger, store)
if err != nil {
Expand Down
49 changes: 14 additions & 35 deletions ee/agent/keys_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,45 +5,24 @@ package agent

import (
"errors"
"fmt"
"log/slog"

"github.com/kolide/launcher/ee/agent/types"
"github.com/kolide/launcher/ee/secureenclavesigner"
)

// 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")

/*
_, pubData, err := fetchKeyData(store)
if err != nil {
return nil, 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
*/
ses, err := secureenclavesigner.New(slogger, store)
if err != nil {
return nil, fmt.Errorf("creating secureenclave signer: %w", err)
}

// this is kind of weird, but we need to call public to ensure the key is generated
// it's done this way to do satisfying signer interface which doesn't return an error
if ses.Public() == nil {
return nil, errors.New("public key was not be created")
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved
}

return ses, nil
}
Loading
Loading