-
Notifications
You must be signed in to change notification settings - Fork 103
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
Changes from all commits
2ee7698
308ab38
8239d5d
97622d9
212176c
864d02f
ced7149
7fe2cf3
b59d2ab
0ce5224
29ae0d6
96073c4
99cb683
e4fa69f
611c7ec
9886c45
d4ebf25
15655b5
ce0b00c
58f77fe
1282f15
92b37d4
e41a4f9
ee114e0
d1ad0a1
d82d81a
9ae8c3c
cb3753a
66981b8
41847ee
7a4650b
bd2cb6f
7986154
c18e994
6644944
e66b948
6d2f4b4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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] { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
} |
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") | ||
} |
There was a problem hiding this comment.
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)