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 1 commit
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 or sign operation using the secure enclave.
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved
// 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)

Check failure on line 41 in cmd/launcher/secure_enclave_darwin.go

View workflow job for this annotation

GitHub Actions / lint (macos-latest)

cannot use secureEnclavePubKey (variable of type []byte) as *ecdsa.PublicKey value in argument to echelper.PublicEcdsaToB64Der (typecheck)

Check failure on line 41 in cmd/launcher/secure_enclave_darwin.go

View workflow job for this annotation

GitHub Actions / lint (macos-latest)

cannot use secureEnclavePubKey (variable of type []byte) as *ecdsa.PublicKey value in argument to echelper.PublicEcdsaToB64Der (typecheck)

Check failure on line 41 in cmd/launcher/secure_enclave_darwin.go

View workflow job for this annotation

GitHub Actions / launcher (macos-12)

cannot use secureEnclavePubKey (variable of type []byte) as *ecdsa.PublicKey value in argument to echelper.PublicEcdsaToB64Der

Check failure on line 41 in cmd/launcher/secure_enclave_darwin.go

View workflow job for this annotation

GitHub Actions / launcher (macos-12)

cannot use secureEnclavePubKey (variable of type []byte) as *ecdsa.PublicKey value in argument to echelper.PublicEcdsaToB64Der

Check failure on line 41 in cmd/launcher/secure_enclave_darwin.go

View workflow job for this annotation

GitHub Actions / govulncheck (macos-latest)

cannot use secureEnclavePubKey (variable of type []byte) as *ecdsa.PublicKey value in argument to echelper.PublicEcdsaToB64Der
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
71 changes: 42 additions & 29 deletions ee/agent/keys_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,46 +4,59 @@
package agent

import (
"context"
"encoding/json"
"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
}
// persister satisfies the persister interface for secureenclavesigner
type persister struct {
store types.GetterSetterDeleter
}

if pubData == nil {
level.Info(logger).Log("msg", "Generating new keys")
func (p *persister) Persist(data []byte) error {
return storeKeyData(p.store, nil, data)
}

var err error
pubData, err = secureenclave.CreateKey()
if err != nil {
return nil, fmt.Errorf("creating key: %w", err)
}
func setupHardwareKeys(slogger *slog.Logger, store types.GetterSetterDeleter) (keyInt, error) {

if err := storeKeyData(store, nil, pubData); err != nil {
clearKeyData(logger, store)
return nil, fmt.Errorf("storing key: %w", err)
// fetch any existing key data
_, pubData, err := fetchKeyData(store)
if err != nil {
return nil, err
}

ses, err := secureenclavesigner.New(slogger, &persister{store: store})
if err != nil {
return nil, fmt.Errorf("creating secureenclave signer: %w", err)
}

if pubData != nil {
if err := json.Unmarshal(pubData, &ses); err != nil {
// data is corrupt or not in the expected format, clear it
slogger.Log(context.TODO(), slog.LevelError,
"could not unmarshal stored key data, clearing key data and generating new keys",
"err", err,
)
clearKeyData(slogger, store)

ses, err = secureenclavesigner.New(slogger, &persister{store: store})
if err != nil {
return nil, fmt.Errorf("creating secureenclave signer: %w", err)
}
}
}

k, err := secureenclave.New(pubData)
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 k, nil
*/
return ses, nil
}
38 changes: 38 additions & 0 deletions ee/secureenclavesigner/mocks/persister.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading