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

james/secure enclave cmd signer #1514

Closed
Closed
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
2ee7698
set up create key secure enclave command with tests
James-Pickett Dec 12, 2023
308ab38
add secure enclave signer
James-Pickett Dec 15, 2023
8239d5d
add non darwin files
James-Pickett Dec 15, 2023
97622d9
Merge branch 'main' into james/secure-enclave-cmd-signer
James-Pickett Dec 15, 2023
212176c
add server cert validation and tests
James-Pickett Dec 21, 2023
864d02f
Merge branch 'main' into james/secure-enclave-cmd-signer
James-Pickett Dec 22, 2023
ced7149
comments, tweaks, consolidation
James-Pickett Dec 22, 2023
7fe2cf3
cleaner test key building
James-Pickett Dec 22, 2023
b59d2ab
replace go mod to point at krypto branch
James-Pickett Dec 22, 2023
0ce5224
fix imports
James-Pickett Dec 22, 2023
29ae0d6
put slash back
James-Pickett Dec 22, 2023
96073c4
spelling
James-Pickett Dec 22, 2023
99cb683
use allowedcmd, spelling
James-Pickett Dec 27, 2023
e4fa69f
no lint execs in test files
James-Pickett Dec 27, 2023
611c7ec
more exec lint exceptions in tests
James-Pickett Dec 27, 2023
9886c45
add validation tests
James-Pickett Dec 27, 2023
d4ebf25
feedback, cleanup
James-Pickett Dec 28, 2023
15655b5
spelling
James-Pickett Dec 28, 2023
ce0b00c
point go.mod back act kolide repo
James-Pickett Dec 29, 2023
58f77fe
go mod tidy
James-Pickett Dec 29, 2023
1282f15
Merge branch 'main' into james/secure-enclave-cmd-signer
James-Pickett Dec 29, 2023
92b37d4
Merge branch 'main' into james/secure-enclave-cmd-signer
James-Pickett Jan 2, 2024
e41a4f9
Merge branch 'main' into james/secure-enclave-cmd-signer
James-Pickett Jan 8, 2024
ee114e0
clean up, update provision profile
James-Pickett Jan 18, 2024
d1ad0a1
add data structure validaiton to secure enclave cmd
James-Pickett Jan 18, 2024
d82d81a
load server keys to func
James-Pickett Jan 18, 2024
9ae8c3c
validate data format sooner, add comment
James-Pickett Jan 19, 2024
cb3753a
fix typo in comment
James-Pickett Jan 22, 2024
66981b8
Merge branch 'main' into james/secure-enclave-cmd-signer
James-Pickett Jan 22, 2024
41847ee
Merge branch 'main' into james/secure-enclave-cmd-signer
James-Pickett Jan 23, 2024
7a4650b
Merge branch 'main' into james/secure-enclave-cmd-signer
James-Pickett Feb 27, 2024
bd2cb6f
just sign challenge msg after verifying
James-Pickett Feb 27, 2024
7986154
unexport unneeded export, go mod tidy
James-Pickett Feb 27, 2024
c18e994
add, fix comments
James-Pickett Feb 27, 2024
6644944
add kolide tags and double nonce logic to secure enclave signer, upda…
James-Pickett Mar 5, 2024
e66b948
remove unneeded change
James-Pickett Mar 5, 2024
6d2f4b4
add timestamp to user signer
James-Pickett Mar 5, 2024
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 @@ -175,6 +175,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
153 changes: 153 additions & 0 deletions cmd/launcher/secure_enclave_dawin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//go:build darwin
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved
// +build darwin

package main

import (
"crypto"
"crypto/ecdsa"
"crypto/rand"
"encoding/base64"
"fmt"
"time"

"github.com/kolide/krypto/pkg/challenge"
"github.com/kolide/krypto/pkg/echelper"
"github.com/kolide/krypto/pkg/secureenclave"
"github.com/kolide/launcher/ee/agent/certs"
"github.com/kolide/launcher/ee/secureenclavesigner"
"github.com/kolide/launcher/pkg/backoff"
"github.com/vmihailenco/msgpack/v5"
)

var serverPubKeys = make(map[string]*ecdsa.PublicKey)

func runSecureEnclave(args []string) error {
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved
if len(args) < 2 {
return fmt.Errorf("not enough arguments, expect create_key <request> or sign <sign_request>")
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved
}

if secureenclavesigner.Undertest {
if secureenclavesigner.TestServerPubKey == "" {
return fmt.Errorf("test server public key not set")
}

k, err := echelper.PublicB64DerToEcdsaKey([]byte(secureenclavesigner.TestServerPubKey))
if err != nil {
return fmt.Errorf("parsing test server public key: %w", err)
}

serverPubKeys[string(secureenclavesigner.TestServerPubKey)] = k
}

for _, keyStr := range []string{certs.K2EccServerCert, certs.ReviewEccServerCert, certs.LocalhostEccServerCert} {
key, err := echelper.PublicPemToEcdsaKey([]byte(keyStr))
if err != nil {
return fmt.Errorf("parsing server public key from pem: %w", err)
}

pubB64Der, err := echelper.PublicEcdsaToB64Der(key)
if err != nil {
return fmt.Errorf("marshalling server public key to b64 der: %w", err)
}

serverPubKeys[string(pubB64Der)] = key
}

switch args[0] {
case "create-key":
return createSecureEnclaveKey(args[1])

case "sign":
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved
return signWithSecureEnclave(args[1])

default:
return fmt.Errorf("unknown command %s", args[0])
}
}

func createSecureEnclaveKey(requestB64 string) error {
b, err := base64.StdEncoding.DecodeString(requestB64)
if err != nil {
return fmt.Errorf("decoding b64 request: %w", err)
}

var request secureenclavesigner.Request
if err := msgpack.Unmarshal(b, &request); err != nil {
return fmt.Errorf("unmarshaling msgpack request: %w", err)
}

if err := verifySecureEnclaveChallenge(request); err != nil {
return fmt.Errorf("verifying challenge: %w", err)
}

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)
}

fmt.Println(string(secureEnclavePubDer))
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved
return nil
}

func signWithSecureEnclave(signRequestB64 string) error {
b, err := base64.StdEncoding.DecodeString(signRequestB64)
if err != nil {
return fmt.Errorf("decoding b64 sign request: %w", err)
}

var signRequest secureenclavesigner.SignRequest
if err := msgpack.Unmarshal(b, &signRequest); err != nil {
return fmt.Errorf("unmarshaling msgpack sign request: %w", err)
}

if err := verifySecureEnclaveChallenge(signRequest.Request); err != nil {
return fmt.Errorf("verifying challenge: %w", err)
}

secureEnclavePubKey, err := echelper.PublicB64DerToEcdsaKey(signRequest.SecureEnclavePubKey)
if err != nil {
return fmt.Errorf("marshalling b64 der to public key: %w", err)
}

ses, err := secureenclave.New(secureEnclavePubKey)
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return fmt.Errorf("creating secure enclave signer: %w", err)
}
var sig []byte
backoff.WaitFor(func() error {
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved
sig, err = ses.Sign(rand.Reader, signRequest.Digest, crypto.SHA256)
return err
}, 250*time.Millisecond, 2*time.Second)

if err != nil {
return fmt.Errorf("signing request: %w", err)
}

fmt.Print(base64.StdEncoding.EncodeToString(sig))
return nil
}

func verifySecureEnclaveChallenge(request secureenclavesigner.Request) error {
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved
c, err := challenge.UnmarshalChallenge(request.Challenge)
if err != nil {
return fmt.Errorf("unmarshaling challenge: %w", err)
}

serverPubKey, ok := serverPubKeys[string(request.ServerPubKey)]
if !ok {
return fmt.Errorf("server public key not found")
}

if err := c.Verify(*serverPubKey); err != nil {
return fmt.Errorf("verifying challenge: %w", err)
}

// TODO verify time stamp
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved
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")
}
204 changes: 204 additions & 0 deletions cmd/launcher/secure_enclave_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
//go:build darwin
// +build darwin

package main

import (
"bytes"
"context"
"encoding/base64"
"fmt"
"os"
"os/exec"
"path/filepath"
"testing"
"time"

"github.com/kolide/kit/ulid"
"github.com/kolide/krypto/pkg/challenge"
"github.com/kolide/krypto/pkg/echelper"
"github.com/kolide/launcher/ee/secureenclavesigner"
"github.com/stretchr/testify/require"
"github.com/vmihailenco/msgpack/v5"
)

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) {
James-Pickett 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 krypto_test.app/Contents/MacOS and add files
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved
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(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")
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
require.Contains(t, string(out), "PASS: TestSecureEnclaveCmd")
require.NotContains(t, string(out), "FAIL")
t.Log(string(out))
}

func TestSecureEnclaveCmd(t *testing.T) {
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved
t.Parallel()

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 test server private key
testServerPrivKey, err := echelper.GenerateEcdsaKey()
require.NoError(t, err)

testServerPubKeyB64Der, err := echelper.PublicEcdsaToB64Der(&testServerPrivKey.PublicKey)
require.NoError(t, err)

// add the test server private key to the map of server public keys
serverPubKeys[string(testServerPubKeyB64Der)] = &testServerPrivKey.PublicKey

someData := []byte(ulid.New())
challenge, _, err := challenge.Generate(testServerPrivKey, someData, someData, someData)
require.NoError(t, err)

requestBytes, err := msgpack.Marshal(secureenclavesigner.Request{
Challenge: challenge,
ServerPubKey: testServerPubKeyB64Der,
})
require.NoError(t, err)

// create a pipe to capture stdout
pipeReader, pipeWriter, err := os.Pipe()
require.NoError(t, err)

os.Stdout = pipeWriter

require.NoError(t, runSecureEnclave([]string{"create-key", base64.StdEncoding.EncodeToString(requestBytes)}))
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")

dataToSign := []byte(ulid.New())
digest, err := echelper.HashForSignature(dataToSign)
require.NoError(t, err)

signRequestBytes, err := msgpack.Marshal(secureenclavesigner.SignRequest{
Request: secureenclavesigner.Request{
Challenge: challenge,
ServerPubKey: testServerPubKeyB64Der,
},
Digest: digest,
SecureEnclavePubKey: createKeyResponse,
})
require.NoError(t, err)

pipeReader, pipeWriter, err = os.Pipe()
require.NoError(t, err)

os.Stdout = pipeWriter
require.NoError(t, runSecureEnclave([]string{"sign", base64.StdEncoding.EncodeToString(signRequestBytes)}))
require.NoError(t, pipeWriter.Close())

buf = bytes.Buffer{}
_, err = buf.ReadFrom(pipeReader)
require.NoError(t, err)

sig, err := base64.StdEncoding.DecodeString(string(buf.Bytes()))
require.NoError(t, err)

require.NoError(t, echelper.VerifySignature(secureEnclavePubKey, dataToSign, sig))
}

// #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(
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))
}
Loading
Loading