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 all 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 @@ -152,6 +152,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
207 changes: 207 additions & 0 deletions cmd/launcher/secure_enclave_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,207 @@
//go:build darwin
// +build darwin

package main

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

"github.com/kolide/kit/ulid"
"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/vmihailenco/msgpack/v5"
)

const secureEnclaveTimestampValiditySeconds = 150

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

// runSecureEnclave performs either a create-key or sign 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. To perform an operation, root launcher needs to
// include a challenge signed by a known server. See ee/secureenclavesigner for command data
// structure.
func runSecureEnclave(args []string) error {
if len(args) < 2 {
return errors.New("not enough arguments, expect create_key <request> or sign <sign_request>")
}

if err := loadServerKeys(); err != nil {
return fmt.Errorf("loading server keys: %w", err)
}

if args[1] == "" {
return errors.New("missing request")
}

switch args[0] {
Copy link
Contributor

Choose a reason for hiding this comment

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

Thinking about deletion/uninstall.

As it stands today, the uninstall message is not signed. So how do we uninstall these keys? Do we need to?

If we need to delete them, we probably need a signed deletion message.

If we don't need to, we could do somewhere where launcher records a nonce in the launcherdb, and then we pass the nonce along to the sign command. (so that we can find the correct key). (And by nonce, I probably mean the public key, we could stash it in the launcher db. Though that will be weird cross users. Probably still correct)

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not sure it's a usability win, but I wonder if we can discern what kind of request it is from the signed blob.

case secureenclavesigner.CreateKeyCmd:
return createSecureEnclaveKey(args[1])

case secureenclavesigner.SignCmd:
return signWithSecureEnclave(args[1])

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

func loadServerKeys() error {
if secureenclavesigner.Undertest {
Copy link
Contributor

Choose a reason for hiding this comment

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

As this should never trigger in real use, I kinda want a giant log warning for this condition. (But I see slogger isn't here yet)

if secureenclavesigner.TestServerPubKey == "" {
return errors.New("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
}

return nil
}

func createSecureEnclaveKey(requestB64 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'm not sure I know enough to know, but do we need to do any kind of tracking around which k2 key requested this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure how or where we could track this? We could put it in logs and the way we do with desktop

Copy link
Contributor

Choose a reason for hiding this comment

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

Talking aloud... If we create a key for a developer environment, and then get a signing request for the prod environment, what do we do? (or vice versa)

I'm not sure there's a security issue here, but I'm not sure how launcher picks the correct key to sign the response with.

Copy link
Contributor

Choose a reason for hiding this comment

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

(This ends up tying up to the deletion comments above)

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

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

if err := verifySecureEnclaveChallenge(createKeyRequest.SecureEnclaveRequest); 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)
}

os.Stdout.Write(secureEnclavePubDer)
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.SecureEnclaveRequest); 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)
}

seSigner, err := secureenclave.New(secureEnclavePubKey)
if err != nil {
return fmt.Errorf("creating secure enclave signer: %w", err)
}

// tag the ends of the data to sign, this is intended to ensure that launcher wont
// sign arbitrary things, any party verifying the signature will need to
// handle these tags
dataToSign := []byte(fmt.Sprintf("kolide:%s:kolide", signRequest.Data))

innerSignResponse := secureenclavesigner.SignResponseInner{
Nonce: fmt.Sprintf("%s%s", signRequest.BaseNonce, ulid.New()),
Timestamp: time.Now().UTC().Unix(),
Data: dataToSign,
}

innerResponseBytes, err := msgpack.Marshal(innerSignResponse)
if err != nil {
return fmt.Errorf("marshalling inner response: %w", err)
}

digest, err := echelper.HashForSignature(innerResponseBytes)
if err != nil {
return fmt.Errorf("hashing data for signature: %w", err)
}

sig, err := seSigner.Sign(rand.Reader, digest, crypto.SHA256)
if err != nil {
return fmt.Errorf("signing request: %w", err)
}

outerResponseBytes, err := msgpack.Marshal(secureenclavesigner.SignResponseOuter{
Msg: innerResponseBytes,
Sig: sig,
})

if err != nil {
return fmt.Errorf("marshalling outer response: %w", err)
}

os.Stdout.Write([]byte(base64.StdEncoding.EncodeToString(outerResponseBytes)))
return nil
}

func verifySecureEnclaveChallenge(request secureenclavesigner.SecureEnclaveRequest) error {
challengeUnmarshalled, err := challenge.UnmarshalChallenge(request.Challenge)
if err != nil {
return fmt.Errorf("unmarshaling challenge: %w", err)
}

serverPubKey, ok := serverPubKeys[string(request.ServerPubKey)]
directionless marked this conversation as resolved.
Show resolved Hide resolved
if !ok {
return errors.New("server public key not found")
}

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

// Check the timestamp, this prevents people from saving a challenge and then
// reusing it a bunch. However, it will fail if the clocks are too far out of sync.
James-Pickett marked this conversation as resolved.
Show resolved Hide resolved
timestampDelta := time.Now().Unix() - challengeUnmarshalled.Timestamp()
if timestampDelta > secureEnclaveTimestampValiditySeconds || timestampDelta < -secureEnclaveTimestampValiditySeconds {
return fmt.Errorf("timestamp delta %d is outside of validity range %d", timestampDelta, secureEnclaveTimestampValiditySeconds)
}

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