-
Notifications
You must be signed in to change notification settings - Fork 6
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #42 from tkhq/olivia/enclave-encrypt
Add enclave encrypt package and use go 1.21
- Loading branch information
Showing
16 changed files
with
938 additions
and
35 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,9 +14,9 @@ jobs: | |
- uses: actions/checkout@v2 | ||
|
||
- name: Set up Go | ||
uses: actions/setup-go@v2 | ||
uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0 | ||
with: | ||
go-version: 1.19 | ||
go-version: '1.21' | ||
|
||
- name: Get | ||
run: go get -v | ||
|
@@ -25,7 +25,7 @@ jobs: | |
uses: golangci/[email protected] | ||
with: | ||
args: ./... | ||
version: v1.53.2 | ||
version: v1.55.2 | ||
|
||
- name: Build | ||
run: go build -v ./... | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
module github.com/tkhq/go-sdk | ||
|
||
go 1.20 | ||
go 1.21 | ||
|
||
require ( | ||
github.com/go-openapi/errors v0.20.4 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
go 1.21.0 | ||
|
||
use ( | ||
. | ||
./pkg/enclave_encrypt | ||
) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= | ||
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= | ||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= | ||
go.opentelemetry.io/otel/sdk v1.14.0/go.mod h1:bwIC5TjrNG6QDCHNWvW4HLHtUQ4I+VQDsnjhvyZCALM= | ||
golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= | ||
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= | ||
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= | ||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.PHONY: test | ||
test: | ||
go test ./... |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# Enclave One Shot Encryption | ||
|
||
This package hosts a set of primitives for sending and encrypting message from a user to an enclave or vice versa. | ||
|
||
N.B. Neither the client or the server should ever be reused to send/receive more then one message. We want to avoid the recipient target key being used more then once in order to improve forward secrecy; see [security profile](#security-profile) section for more details. | ||
|
||
## Terms | ||
|
||
- Encapsulated ("Encapped") Key - the public key of the sender used for ECDH. | ||
- Target Key Pair - the key pair of the receiver that the sender encrypts to the public key of. Only one message should ever be encrypted to the public key. | ||
- Server - a server inside of the enclave; normally an enclave application. | ||
- Client - a client outside of the enclave; normally a turnkey end user. | ||
- Enclave Auth Key Pair - a key pair derived from the quorum master seed specifically for the purpose of authentication with clients. | ||
|
||
## Overview | ||
|
||
This protocol builds on top of the HPKE standard ([RFC 9180](https://datatracker.ietf.org/doc/html/rfc9180)) by adding recipient pre-flight authentication so the client can verify it is sending ciphertext to a turnkey controlled enclave and the enclave can verify its sending ciphertext to the correct client. See the [security profile](#security-profile) section more details. | ||
|
||
## HPKE Configuration | ||
|
||
KEM: KEM_P256_HKDF_SHA256 | ||
KDF: KDF_HKDF_SHA256 | ||
AEAD: AEAD_AES256GCM | ||
INFO: b"turnkey_hpke" | ||
ADDITIONAL ASSOCIATED DATA: EncappedPublicKey||ReceiverPublicKey | ||
|
||
## Protocol | ||
|
||
### Server to Client | ||
|
||
1. Client generates target pair and sends clientTargetPub key to server. The authenticity of the clientTargetPub is assumed to have been verified by the Ump policy engine. | ||
1. Server computes ciphertext, serverEncappedPub = ENCRYPT(plaintext, clientTargetPub) and clears clientTargetPub from memory. | ||
1. Server computes serverEncappedPub_sig_enclaveAuthPriv = SIGN(serverEncappedPub, enclaveAuthPriv). | ||
1. Server sends (ciphertext, serverEncappedPub, serverEncappedPub_sig_enclaveAuthPriv) to client. | ||
1. Client runs VERIFY(serverEncappedPub, serverEncappedPub_sig_enclaveAuthPriv). | ||
1. Client recovers plaintext by computing DECRYPT(ciphertext, serverEncappedPub, clientTargetPriv) and the client target pair is cleared from memory. If the target pair is used multiple times we increase the count of messages that an attacker with the compromised target private key can decrypt. | ||
|
||
Note there is no mechanism to prevent a faulty client from resubmitting the same target public key. | ||
|
||
### Client to Server | ||
|
||
1. Client sends request to server for target key. | ||
1. Server generates server target pair and computes serverTargetPub_sig_enclaveAuthPriv = SIGN(serverTargetPub, enclaveAuthPriv). | ||
1. Server sends (serverTargetPub, serverTargetPub_sig_enclaveAuthPriv) to client. | ||
1. Client runs VERIFY(serverTargetPub, serverTargetPub_sig_enclaveAuthPriv). | ||
1. Client computes ciphertext, clientEncappedPub = ENCRYPT(plaintext, serverTargetPub) and clears serverTargetPub from memory. | ||
1. Client sends (ciphertext, clientEncappedPub) to server and the client is cleared from memory. | ||
1. Server assumes the authenticity of clientEncappedPub has been verified by the Ump policy engine. | ||
1. Server recovers plaintext by computing DECRYPT(ciphertext, clientEncappedPub, clientTargetPriv) and server target pair is cleared from memory. If the target pair is used multiple times we increase the count of messages that an attacker with the compromised target private key can decrypt. | ||
|
||
## Security profile | ||
|
||
- Receiver pre-flight authentication: we achieve recipient authentication for both the server and client: | ||
- Client to Server: client verifies that the server's target key is signed by the enclaveAuth key. | ||
- Server to Client: server relies on upstream checks by Ump + activity signing scheme to enforce rules that guarantee authenticity of the clients target key. Specifically, when the client "sends" clientTargetPub it actually submits a signed payload (activity), and that payload must be signed with an existing credential persisted in org data. | ||
- Forward secrecy: the underlying HPKE spec does not provide forward secrecy on the recipient side since the target key can be long lived. To improve forward secrecy we specify that the a target key should only be used once by the sender and receiver. | ||
- Sender authentication: we use OpMode Base and forgo authentication that the sender possessed a given KEM private key. In order for this to be taken advantage of, an attacker would need to compromise the receivers target private key, intercept the message, decrypt it, and then re-encrypt with different plaintext. In our use case, if the attacker intercepts the receivers target private key, everything is already broken so the extra level of authentication is not necessary. Read more about HPKE asymmetric authentication [here](https://datatracker.ietf.org/doc/html/rfc9180#name-authentication-using-an-asy). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,136 @@ | ||
package enclave_encrypt | ||
|
||
import ( | ||
"crypto/ecdsa" | ||
"crypto/elliptic" | ||
"crypto/sha256" | ||
"errors" | ||
"fmt" | ||
"reflect" | ||
|
||
"github.com/btcsuite/btcutil/base58" | ||
"github.com/cloudflare/circl/kem" | ||
) | ||
|
||
// An instance of the client side for enclave encrypt protocol. This should only be used for either | ||
// a SINGLE send or a single receive. | ||
type EnclaveEncryptClient struct { | ||
enclaveAuthKey *ecdsa.PublicKey | ||
targetPrivate kem.PrivateKey | ||
} | ||
|
||
// Create a client from the quorum public key. | ||
func NewEnclaveEncryptClient(enclaveAuthKey *ecdsa.PublicKey) (*EnclaveEncryptClient, error) { | ||
_, targetPrivate, err := KemId.Scheme().GenerateKeyPair() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
return &EnclaveEncryptClient{ | ||
enclaveAuthKey, | ||
targetPrivate, | ||
}, nil | ||
} | ||
|
||
// Create a client from the quorum public key and target key pair. | ||
func NewEnclaveEncryptClientFromTargetKey(enclaveAuthKey *ecdsa.PublicKey, targetPrivateKey *kem.PrivateKey) (*EnclaveEncryptClient, error) { | ||
return &EnclaveEncryptClient{ | ||
enclaveAuthKey, | ||
*targetPrivateKey, | ||
}, nil | ||
} | ||
|
||
// Encrypt some plaintext to the given server target key. | ||
func (c *EnclaveEncryptClient) Encrypt(plaintext Bytes, msg ServerTargetMsg) (*ClientSendMsg, error) { | ||
if !P256Verify(c.enclaveAuthKey, *msg.TargetPublic, *msg.TargetPublicSignature) { | ||
return nil, errors.New("invalid enclave auth key signature") | ||
} | ||
targetPublic, err := KemId.Scheme().UnmarshalBinaryPublicKey((*msg.TargetPublic)[:]) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
ciphertext, encappedPublic, err := encrypt( | ||
&targetPublic, | ||
plaintext, | ||
) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
enc := Bytes(encappedPublic) | ||
ciph := Bytes(ciphertext) | ||
return &ClientSendMsg{ | ||
EncappedPublic: &enc, | ||
Ciphertext: &ciph, | ||
}, nil | ||
} | ||
|
||
// Decrypt a message from the server. This is used in private key and wallet export flows. | ||
func (c *EnclaveEncryptClient) Decrypt(msg ServerSendMsg) (plaintext []byte, err error) { | ||
if !P256Verify(c.enclaveAuthKey, *msg.EncappedPublic, *msg.EncappedPublicSignature) { | ||
return nil, errors.New("invalid enclave auth key signature") | ||
} | ||
|
||
return decrypt( | ||
*msg.EncappedPublic, | ||
c.targetPrivate, | ||
*msg.Ciphertext, | ||
) | ||
} | ||
|
||
// Get this clients target public key. | ||
func (c *EnclaveEncryptClient) TargetPublic() ([]byte, error) { | ||
return c.targetPrivate.Public().MarshalBinary() | ||
} | ||
|
||
// Decrypt a base58-encoded payload from the server. This is used in email authentication and email recovery flows. | ||
func (c *EnclaveEncryptClient) AuthDecrypt(payload string) (plaintext []byte, err error) { | ||
payloadBytes := base58.Decode(payload) | ||
err = ValidateChecksum(payloadBytes) | ||
if err != nil { | ||
return nil, err | ||
} | ||
// Trim the checksum | ||
payloadBytes = payloadBytes[:len(payloadBytes)-4] | ||
|
||
if len(payloadBytes) < 33 { | ||
return nil, errors.New("payload is less then 33 bytes, the length of the expected public key") | ||
} | ||
compressedKey := payloadBytes[0:33] | ||
ciphertext := payloadBytes[33:] | ||
|
||
x, y := elliptic.UnmarshalCompressed(elliptic.P256(), compressedKey) | ||
|
||
// FIXME: `elliptic.Unmarshal` is deprecated, but scm does not know how to replace it. | ||
// nolint:staticcheck | ||
encappedPublic := elliptic.Marshal(elliptic.P256(), x, y) | ||
|
||
return decrypt( | ||
encappedPublic, | ||
c.targetPrivate, | ||
ciphertext, | ||
) | ||
} | ||
|
||
// Validates that a payload has a valid checksum in the last four bytes. | ||
func ValidateChecksum(payload []byte) error { | ||
if len(payload) < 5 { | ||
return fmt.Errorf("payload length is < 5 (length: %d)", len(payload)) | ||
} | ||
expected := checksum(payload[:len(payload)-4]) | ||
if !reflect.DeepEqual(expected[:], payload[len(payload)-4:]) { | ||
return fmt.Errorf("checksum mismatch for payload %02x: %v (computed) != %v (last four bytes)", payload, expected, payload[len(payload)-4:]) | ||
} | ||
return nil | ||
} | ||
|
||
// Takes a payload and return a checksum (4 bytes) | ||
// The double-hash operation is dictated by the base58check standard | ||
// See https://en.bitcoin.it/wiki/Base58Check_encoding#Creating_a_Base58Check_string | ||
func checksum(payload []byte) (checkSum [4]byte) { | ||
h := sha256.Sum256(payload) | ||
h2 := sha256.Sum256(h[:]) | ||
copy(checkSum[:], h2[:4]) | ||
return checkSum | ||
} |
Oops, something went wrong.