diff --git a/rolling-shutter/.gitignore b/rolling-shutter/.gitignore index 48c2ef121..7217e5455 100644 --- a/rolling-shutter/.gitignore +++ b/rolling-shutter/.gitignore @@ -9,3 +9,7 @@ /godoc.idx /go.work /go.work.sum + +enclave.json +private.pem +public.pem \ No newline at end of file diff --git a/rolling-shutter/go.mod b/rolling-shutter/go.mod index eb34adb6d..a78f3517e 100644 --- a/rolling-shutter/go.mod +++ b/rolling-shutter/go.mod @@ -10,6 +10,7 @@ require ( github.com/bitwurx/jrpc2 v0.0.0-20220302204700-52c6dbbeb536 github.com/deckarep/golang-set/v2 v2.6.0 github.com/deepmap/oapi-codegen v1.9.1 + github.com/edgelesssys/ego v1.7.0 github.com/ethereum/go-ethereum v1.15.11 github.com/ferranbt/fastssz v0.1.3 github.com/getkin/kin-openapi v0.87.0 @@ -93,6 +94,7 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gballet/go-libpcsclite v0.0.0-20191108122812-4678299bea08 // indirect github.com/ghodss/yaml v1.0.0 // indirect + github.com/go-jose/go-jose/v4 v4.0.4 // indirect github.com/go-kit/kit v0.12.0 // indirect github.com/go-kit/log v0.2.1 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect diff --git a/rolling-shutter/go.sum b/rolling-shutter/go.sum index d32151b08..e3aa42dce 100644 --- a/rolling-shutter/go.sum +++ b/rolling-shutter/go.sum @@ -195,6 +195,8 @@ github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDD github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/edgelesssys/ego v1.7.0 h1:NzCiKZKalHbeRgG7+11xgADgOPR8laSxxazOH303QVw= +github.com/edgelesssys/ego v1.7.0/go.mod h1:xBU369lGvxWTuMLlJv3tT6bI66hrpkCFn/292zzl7U4= github.com/elastic/gosigar v0.12.0/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= github.com/elastic/gosigar v0.14.3 h1:xwkKwPia+hSfg9GqrCUKYdId102m9qTJIIr7egmK/uo= github.com/elastic/gosigar v0.14.3/go.mod h1:iXRIGg2tLnu7LBdpqzyQfGDEidKCfWcCMS0WKyPWoMs= @@ -250,6 +252,8 @@ github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= +github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= +github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= github.com/go-kit/kit v0.12.0 h1:e4o3o3IsBfAKQh5Qbbiqyfu97Ku7jrO/JbohvztANh4= github.com/go-kit/kit v0.12.0/go.mod h1:lHd+EkCZPIwYItmGDDRdhinkzX2A1sj+M9biaEaizzs= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= diff --git a/rolling-shutter/keyper/database/extend.go b/rolling-shutter/keyper/database/extend.go index 2822ea7a9..d9c477493 100644 --- a/rolling-shutter/keyper/database/extend.go +++ b/rolling-shutter/keyper/database/extend.go @@ -12,6 +12,7 @@ import ( "github.com/shutter-network/rolling-shutter/rolling-shutter/p2pmsg" "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" "github.com/shutter-network/rolling-shutter/rolling-shutter/shmsg" + "github.com/shutter-network/rolling-shutter/rolling-shutter/tee" ) func GetKeyperIndex(addr common.Address, keypers []string) (uint64, bool) { @@ -31,10 +32,16 @@ func (bc *TendermintBatchConfig) KeyperIndex(addr common.Address) (uint64, bool) func (q *Queries) InsertDecryptionKeysMsg(ctx context.Context, msg *p2pmsg.DecryptionKeys) error { for _, key := range msg.Keys { identityPreimage := identitypreimage.IdentityPreimage(key.IdentityPreimage) + + sealedKey, err := tee.SealSecret(key.Key) + if err != nil { + return err + } + tag, err := q.InsertDecryptionKey(ctx, InsertDecryptionKeyParams{ Eon: int64(msg.Eon), EpochID: identityPreimage.Bytes(), - DecryptionKey: key.Key, + DecryptionKey: sealedKey, }) if err != nil { return errors.Wrapf(err, "failed to insert decryption key for identity %s", identityPreimage) @@ -49,11 +56,16 @@ func (q *Queries) InsertDecryptionKeysMsg(ctx context.Context, msg *p2pmsg.Decry func (q *Queries) InsertDecryptionKeySharesMsg(ctx context.Context, msg *p2pmsg.DecryptionKeyShares) error { for _, share := range msg.GetShares() { - err := q.InsertDecryptionKeyShare(ctx, InsertDecryptionKeyShareParams{ + sealedShare, err := tee.SealSecret(share.Share) + if err != nil { + return err + } + + err = q.InsertDecryptionKeyShare(ctx, InsertDecryptionKeyShareParams{ Eon: int64(msg.Eon), EpochID: share.IdentityPreimage, KeyperIndex: int64(msg.KeyperIndex), - DecryptionKeyShare: share.Share, + DecryptionKeyShare: sealedShare, }) if err != nil { return errors.Wrapf( diff --git a/rolling-shutter/keyper/smobserver/smstate.go b/rolling-shutter/keyper/smobserver/smstate.go index 62a4542ac..2e634ca89 100644 --- a/rolling-shutter/keyper/smobserver/smstate.go +++ b/rolling-shutter/keyper/smobserver/smstate.go @@ -28,6 +28,7 @@ import ( "github.com/shutter-network/rolling-shutter/rolling-shutter/medley" "github.com/shutter-network/rolling-shutter/rolling-shutter/shdb" "github.com/shutter-network/rolling-shutter/rolling-shutter/shmsg" + "github.com/shutter-network/rolling-shutter/rolling-shutter/tee" ) type Config interface { @@ -216,7 +217,13 @@ func (st *ShuttermintState) sendPolyEvals(ctx context.Context, queries *database if err != nil { return fmt.Errorf("failed to decode encryption public key for %s from db: %w", eval.ReceiverAddress, err) } - encrypted, err := ecies.Encrypt(rand.Reader, pubkey, eval.Eval, nil, nil) + + plainEval, err := tee.UnsealSecret(eval.Eval) + if err != nil { + return err + } + + encrypted, err := ecies.Encrypt(rand.Reader, pubkey, plainEval, nil, nil) if err != nil { return err } @@ -390,10 +397,15 @@ func (st *ShuttermintState) startPhase1Dealing( ).Inc() for _, eval := range polyEvals { + sealedEval, err := tee.SealSecret(shdb.EncodeBigint(eval.Eval)) + if err != nil { + return err + } + err = queries.InsertPolyEval(ctx, database.InsertPolyEvalParams{ Eon: int64(eon), ReceiverAddress: shdb.EncodeAddress(dkg.keypers[eval.Receiver]), - Eval: shdb.EncodeBigint(eval.Eval), + Eval: sealedEval, }) if err != nil { return err diff --git a/rolling-shutter/medley/chainsync/syncer/unsafehead.go b/rolling-shutter/medley/chainsync/syncer/unsafehead.go index 649c6b724..acdf10c6a 100644 --- a/rolling-shutter/medley/chainsync/syncer/unsafehead.go +++ b/rolling-shutter/medley/chainsync/syncer/unsafehead.go @@ -3,6 +3,7 @@ package syncer import ( "context" "errors" + "math/big" "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/log" @@ -11,37 +12,138 @@ import ( "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/chainsync/event" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/encodeable/number" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/service" + "github.com/shutter-network/rolling-shutter/rolling-shutter/tee" ) type UnsafeHeadSyncer struct { + // Not used when eVerify is true Client client.Client Log log.Logger Handler event.BlockHandler + // Not used when eVerify is true. newLatestHeadCh chan *types.Header } func (s *UnsafeHeadSyncer) Start(ctx context.Context, runner service.Runner) error { + // Extended/External/Enclave verification + // This can be true when not running in SGX, too. + // We currently assume the runner (separate process that contains the enclave) is already running. + // It might be possible to start it from within the enclave, but I am not sure about that. + eVerify := true + if s.Handler == nil { return errors.New("no handler registered") } - s.newLatestHeadCh = make(chan *types.Header, 1) - subs, err := s.Client.SubscribeNewHead(ctx, s.newLatestHeadCh) - if err != nil { - return err - } - runner.Go(func() error { - err := s.watchLatestUnsafeHead(ctx, subs.Err()) + // Difference between SubscribeNewHead and the ethereum-enclave: + // - SubscribeNewHead returns all headers (no gaps, potentially with duplicates) + // - On a reorg: SubscribeNeHead returns all headers (starting at the reorg) + // - ethereum-enclave (finalized) returns all headers if AFTER the start blocknum (no gaps) + // - ethereum-enclave (finalized) has gaps BEFORE the start blocknum + // - ethereum-enclave (finalized) will panic if there is a reorg (major consensus failure and slashing) + // - ethereum-enclave (optimistic) has gaps + if eVerify { + // We don't want to trust the geth client. This provides a channel containing blocks. + // The tee.Config allows configuring which external verifiers should be considered valid. + // It is only relevant when we're running in SGX, too. See comments. + // conn.Attestation() contains additional configuration like the hash the etheruem-enclave started at (genesis). + // It should be checked or forwarded in a remote attestation to make sure we're not following the wrong chain. + conn, err := tee.DialVerifiedChainDataChannel("127.0.0.1:8001", tee.Config{ + // // Configuration options for SGX verification: + // SameSigner: false, // Require that the etheruem-enclave is signed with the same key as we are + // SignerID: []byte{}, // Alternative: Specify the SignerID + // MrEnclave: []byte{}, // Hash of the code the ethereum-enclave is running + // ProductID: new(uint16), // Number by the signer to identify the ethereum-enclave to distinguish different products/binaries + // MinISVSVN: 0, // Minimum (security) version number specified when signing the ethereum enclave + + // Currently we do not use events verified by the ethereum-enclave, so we can just tell it to not extract them. + EventExtractionStartBlocknum: ^uint64(0), + Contracts: nil, // For now: We are not interested in events + }) + if err != nil { + return err + } + runner.Go(func() error { + err := s.watchLatestUnsafeHeadEVerify(ctx, conn) + if err != nil { + s.Log.Error("error watching latest unsafe head with e-verify", err.Error()) + } + conn.Close() + return err + }) + } else { + s.newLatestHeadCh = make(chan *types.Header, 1) + subs, err := s.Client.SubscribeNewHead(ctx, s.newLatestHeadCh) if err != nil { - s.Log.Error("error watching latest unsafe head", err.Error()) + return err } - subs.Unsubscribe() - return err - }) + runner.Go(func() error { + err := s.watchLatestUnsafeHead(ctx, subs.Err()) + if err != nil { + s.Log.Error("error watching latest unsafe head", err.Error()) + } + subs.Unsubscribe() + return err + }) + } + return nil } +func (s *UnsafeHeadSyncer) watchLatestUnsafeHeadEVerify(ctx context.Context, conn *tee.Connection) error { + for { + select { + case verified, ok := <-conn.Headers(): + // We have three options: + // - Fully trust the go-ethereum node (problematic/insecure) + // - Use the Finalized header (about 21 minutes delay but no reorgs unless there is a major chain failure) + // - Use the optimistic header + // + // I do not know for sure which one is best suited: + // - The existing code accounts for reorgs, so the optimistic header should be fine. + // - But on the other hand it might result in leaking/exposing/outputting key material too early (which cannot be undone). + header := verified.Optimistic + + if !ok { + return nil + } + num := big.NewInt(0) + num.SetUint64(header.Number) + + // The ethereum enclave (external verifier) primarily uses beacon-chain data. Thus it + // has the execution layer header data in a different format and not exactly the same data. + // However, shutter does not need the majority of that data. + // The fields below can be extended by other data from ExecutionPayloadHeader if needed. + // I could not find any place in this repo that uses the other fields. + ev := &event.LatestBlock{ + Number: number.BigToBlockNumber(num), + BlockHash: header.BlockHash, + Header: &types.Header{ + ParentHash: header.ParentHash, + Number: num, + Time: header.Timestamp, + }, + } + err := s.Handler(ctx, ev) + if err != nil { + s.Log.Error( + "handler for `NewLatestBlock` errored", + "error", + err.Error(), + ) + } + case err := <-conn.Errors(): + if err != nil { + s.Log.Error("subscription error for watchLatestUnsafeHead", err.Error()) + return err + } + case <-ctx.Done(): + return ctx.Err() + } + } +} + func (s *UnsafeHeadSyncer) watchLatestUnsafeHead(ctx context.Context, subsErr <-chan error) error { for { select { diff --git a/rolling-shutter/medley/encodeable/keys/ecdsa.go b/rolling-shutter/medley/encodeable/keys/ecdsa.go index 26f6bf016..0e339a614 100644 --- a/rolling-shutter/medley/encodeable/keys/ecdsa.go +++ b/rolling-shutter/medley/encodeable/keys/ecdsa.go @@ -11,6 +11,7 @@ import ( "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/encodeable" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/encodeable/hex" + "github.com/shutter-network/rolling-shutter/rolling-shutter/tee" ) func GenerateECDSAKey(src io.Reader) (*ECDSAPrivate, error) { @@ -60,7 +61,7 @@ func (k *ECDSAPrivate) Equal(b *ECDSAPrivate) bool { } func (k *ECDSAPrivate) UnmarshalText(b []byte) error { - dec, err := hex.DecodeHex(b) + dec, err := tee.UnsealSecretFromHex(string(b)) if err != nil { return err } @@ -73,7 +74,8 @@ func (k *ECDSAPrivate) UnmarshalText(b []byte) error { } func (k *ECDSAPrivate) MarshalText() ([]byte, error) { - return hex.EncodeHex(k.Bytes()), nil + str, err := tee.SealSecretAsHex(k.Bytes()) + return []byte(str), err } func (k *ECDSAPrivate) String() string { diff --git a/rolling-shutter/medley/encodeable/keys/ed25519.go b/rolling-shutter/medley/encodeable/keys/ed25519.go index e0ad85730..ecc271267 100644 --- a/rolling-shutter/medley/encodeable/keys/ed25519.go +++ b/rolling-shutter/medley/encodeable/keys/ed25519.go @@ -8,6 +8,7 @@ import ( "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/encodeable" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/encodeable/hex" + "github.com/shutter-network/rolling-shutter/rolling-shutter/tee" ) type nullReader struct{} @@ -69,7 +70,7 @@ func (k *Ed25519Private) Equal(b *Ed25519Private) bool { } func (k *Ed25519Private) UnmarshalText(b []byte) error { - seed, err := hex.DecodeHex(b) + seed, err := tee.UnsealSecretFromHex(string(b)) if err != nil { return err } @@ -85,7 +86,8 @@ func (k *Ed25519Private) UnmarshalText(b []byte) error { } func (k *Ed25519Private) MarshalText() ([]byte, error) { - return hex.EncodeHex(k.Key.Seed()), nil + str, err := tee.SealSecretAsHex(k.Key.Seed()) + return []byte(str), err } func (k *Ed25519Private) String() string { diff --git a/rolling-shutter/medley/encodeable/keys/libp2p.go b/rolling-shutter/medley/encodeable/keys/libp2p.go index 86c17c28a..dee79c491 100644 --- a/rolling-shutter/medley/encodeable/keys/libp2p.go +++ b/rolling-shutter/medley/encodeable/keys/libp2p.go @@ -10,6 +10,7 @@ import ( "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/encodeable" "github.com/shutter-network/rolling-shutter/rolling-shutter/medley/encodeable/address" + "github.com/shutter-network/rolling-shutter/rolling-shutter/tee" ) func GenerateLibp2pPrivate(src io.Reader) (*Libp2pPrivate, error) { @@ -103,7 +104,7 @@ func (k *Libp2pPrivate) Equal(b *Libp2pPrivate) bool { } func (k *Libp2pPrivate) UnmarshalText(b []byte) error { - dec, err := crypto.ConfigDecodeKey(string(b)) + dec, err := tee.UnsealSecretFromCustomText(string(b), crypto.ConfigDecodeKey) if err != nil { return errors.Wrap(errFailedUnmarshalLibp2pPrivate, err.Error()) } @@ -127,8 +128,8 @@ func (k *Libp2pPrivate) MarshalText() ([]byte, error) { if err != nil { return []byte{}, errors.Wrap(errFailedMarshalLibp2pPrivate, err.Error()) } - enc := crypto.ConfigEncodeKey(b) - return []byte(enc), nil + str, err := tee.SealSecretAsCustomText(b, crypto.ConfigEncodeKey) + return []byte(str), err } func (k *Libp2pPrivate) String() string { diff --git a/rolling-shutter/shdb/db.go b/rolling-shutter/shdb/db.go index 6842f6c25..eea9d93fc 100644 --- a/rolling-shutter/shdb/db.go +++ b/rolling-shutter/shdb/db.go @@ -15,6 +15,7 @@ import ( "github.com/rs/zerolog" "github.com/rs/zerolog/log" + "github.com/shutter-network/rolling-shutter/rolling-shutter/tee" "github.com/shutter-network/shutter/shlib/puredkg" "github.com/shutter-network/shutter/shlib/shcrypto" ) @@ -90,13 +91,18 @@ func EncodePureDKG(p *puredkg.PureDKG) ([]byte, error) { if err != nil { return nil, err } - return buff.Bytes(), nil + return tee.SealSecret(buff.Bytes()) } func DecodePureDKG(data []byte) (*puredkg.PureDKG, error) { - buf := bytes.NewBuffer(data) + unsealed, err := tee.UnsealSecret(data) + if err != nil { + return nil, err + } + + buf := bytes.NewBuffer(unsealed) p := &puredkg.PureDKG{} - err := gob.NewDecoder(buf).Decode(p) + err = gob.NewDecoder(buf).Decode(p) if err != nil { return nil, err } @@ -127,25 +133,36 @@ func EncodePureDKGResult(result *puredkg.Result) ([]byte, error) { if err != nil { return nil, err } - return buf.Bytes(), nil + return tee.SealSecret(buf.Bytes()) } func DecodePureDKGResult(b []byte) (*puredkg.Result, error) { + unsealed, err := tee.UnsealSecret(b) + if err != nil { + return nil, err + } + res := puredkg.Result{} - err := gob.NewDecoder(bytes.NewBuffer(b)).Decode(&res) + + err = gob.NewDecoder(bytes.NewBuffer(unsealed)).Decode(&res) if err != nil { return nil, err } return &res, nil } -func EncodeEpochSecretKeyShare(share *shcrypto.EpochSecretKeyShare) []byte { - return share.Marshal() +func EncodeEpochSecretKeyShare(share *shcrypto.EpochSecretKeyShare) ([]byte, error) { + return tee.SealSecret(share.Marshal()) } func DecodeEpochSecretKeyShare(b []byte) (*shcrypto.EpochSecretKeyShare, error) { + unsealed, err := tee.UnsealSecret(b) + if err != nil { + return nil, err + } + share := new(shcrypto.EpochSecretKeyShare) - err := share.Unmarshal(b) + err = share.Unmarshal(unsealed) if err != nil { return nil, err } diff --git a/rolling-shutter/tee/README.md b/rolling-shutter/tee/README.md new file mode 100644 index 000000000..3c0a5a5e0 --- /dev/null +++ b/rolling-shutter/tee/README.md @@ -0,0 +1,71 @@ +# TEE-fortified Keyper + +The Keyper software has been fortified with TEE capabilities (currently only supporting Intel® SGX™), securing the secret key storage using hardware-based encryption. +This raises the confidence level of the non-collusion security assumption of the Keypers, as it makes it harder for attackers to access the key material. +Now, it is less likely that an honest Keyper operator can get compromised by an attacker, ensuring his honest behaviour. + +## How it works + +**Hardware-based process isolation**  +TEEs use a hardware-level process isolation mechanism that prevents even the operating system and kernel from inspecting or manipulating the process' memory. +This means that secrets are completely safe from even administrator-level attackers, as long as they reside in process memory (assuming the hardware is not compromised). + +**Hardware-based encryption**  +Additionally, we employ the _sealing_ mechanism which allows us to encrypt and authenticate data using hardware-derived keys. +This mechanism allows us to use encryption keys that can either only be derived by a specific executable, or executables signed by a specific authority. +Using this, we can securely store secrets on the disk without the risk of them being deciphered by an attacker. + +### Warning + +Create a backup of the old persistence files before trying to run this in production on SGX. +The TEE keyper can load old persistence files, but the new persistence files written by it are sealed and therefore no longer compatible with non-TEE keypers. +It is therefore best to create a backup on cold storage somewhere. + +## Roadmap + +1. Done: Currently, besides ensuring that it can run inside SGX to benefit from strong process isolation, the Keyper code has been adapted to seal its checkpoints before persisting them to the disk. +2. Coming up: In the next step, we eliminate the trust in the blockchain node that is providing the keyper with information about the outside world. We fully verify the blockchain consensus inside the TEE, and we check the merkle proofs for all blocks to ensure correctness and completeness of the blockchain events. This mainly prevents attacks that try to make the keyper reveal its key shares too early by providing fake blockchain data. +3. Future perspective: An outsider's confidence and trust in a Keyper operator can be improved using further measures such as remote attestation (provable computation), which would allow the Keyper to publicly prove that he is running his node inside a TEE. Further fortifications could then fortify the initial cryptographic setup. + +The currently planned fortifications only ensure that the Keyper operator can trust the software running on his machine. +As the Keypers already have access to their keys, if they wanted to, they could use those to misbehave at any time. +Since the keypers cannot provably forget their keys, they cannot prove that their keys are entirely controlled by the TEE. +Compatibility-breaking changes to the Shutter protocol would have to be enacted to further improve trust. + +# How to set up Shutter to use SGX + +1. Clone the Shutter repository and install Go. +2. Install EGo, following the [official instructions](https://docs.edgeless.systems/ego/getting-started/install) for your OS. +3. Build shutter using `GO=ego-go make build`. + + +**Testing**  +To run shutter tests inside SGX, create a compiled test executable: + +```sh +ego-go test -c +``` + +Note that currently, this has to be done for each package separately. + +**Running an executable inside the TEE**  +Then, sign and run the resulting test executable, run + +```sh +ego sign package.test +ego run package.test +``` + +This signs the executable and generates an `enclave.json` file in the current directory, which contains the launch configuration for the TEE enclave. +The resulting executable can also be run normally without SGX by invoking it like a normal executable, and conversely, can also be run on a non-SGX CPU in a TEE-emulation mode by setting `OE_SIMULATION=1` before running it using `ego run`. +This step is the same for test executables, as well as the normal shutter executables. + +For maximum security, we recommend that there is a trusted central authority responsible for the `ego sign` step of the keyper executable. +This creates an additional layer of security, because an attacker in possession of the signing key can create executables that can unseal the persistence files. +Therefore, the signing key should either be on cold storage, and each keyper signs for himself, or it should be one official key handled by a trusted party. + +## TEE-aware programming + +Currently, the TEE-specific functionality is contained within the `keyper/tee` package. +Using `tee.HasTEE()`, code can query whether it is running inside a TEE (but does not differentiate between true hardware support and the emulation mode). +This way, code paths can be introduced that will only activate when the executable has been explicitly invoked as a TEE-capable process using `ego run`, and so the same code can stay compatible with non-TEE hardware. \ No newline at end of file diff --git a/rolling-shutter/tee/cmd/demo.go b/rolling-shutter/tee/cmd/demo.go new file mode 100644 index 000000000..2b1f06c60 --- /dev/null +++ b/rolling-shutter/tee/cmd/demo.go @@ -0,0 +1,45 @@ +package main + +import ( + "fmt" + + "github.com/ethereum/go-ethereum/common" + "github.com/shutter-network/rolling-shutter/rolling-shutter/tee" +) + +func main() { + conn, err := tee.DialVerifiedChainDataChannel("127.0.0.1:7772", tee.Config{ + // // Configuration options for SGX verification: + // SameSigner: false, // Require that the etheruem-enclave is signed with the same key as we are + // SignerID: []byte{}, // Alternative: Specify the SignerID + // MrEnclave: []byte{}, // Hash of the code the ethereum-enclave is running + // ProductID: new(uint16), // Number by the signer to identify the ethereum-enclave to distinguish different products/binaries + // MinISVSVN: 0, // Minimum (security) version number specified when signing the ethereum enclave + + // Currently we do not use events verified by the ethereum-enclave, so we can just tell it to not extract them. + // EventExtractionStartBlocknum: ^uint64(0), + // Contracts: nil, // For now: We are not interested in events + + EventExtractionStartBlocknum: 9031200, + Contracts: []common.Address{ + common.HexToAddress("0x3eDF60dd017aCe33A0220F78741b5581C385A1BA"), // USDZ + }, + }) + if err != nil { + panic(err) + } + + for { + select { + case d := <-conn.Headers(): + fmt.Println("Headers: ", d) + case d := <-conn.Events(): + if len(d.Events) > 0 { + fmt.Println("Finalized Events: ", d) + } + case d := <-conn.Errors(): + fmt.Println("Error: ", d) + return + } + } +} diff --git a/rolling-shutter/tee/conn.go b/rolling-shutter/tee/conn.go new file mode 100644 index 000000000..14b5db86b --- /dev/null +++ b/rolling-shutter/tee/conn.go @@ -0,0 +1,455 @@ +package tee + +import ( + "bytes" + "crypto/ecdh" + "crypto/hmac" + "crypto/rand" + "encoding/base64" + "encoding/binary" + "encoding/hex" + "encoding/json" + "fmt" + "hash" + "io" + "net" + "strings" + + "github.com/edgelesssys/ego/attestation" + "github.com/edgelesssys/ego/enclave" + "github.com/ethereum/go-ethereum/common" + "golang.org/x/crypto/sha3" +) + +// Communication and guarantees +// Currently there are two different messages we can get (if the "shutter" feature is enabled for the ethereum-enclave): +// +// **VerifiedHeadData** contains information about the latest and finalized (processed) header. +// it has the most up-to-date state but is currently still lacking behind the real chain head +// during sync. The reason for this is simplicity. The ethereum-enclave needs this data to +// verify events. If needed we could change it to sync to the head and store a block_hash +// for each Epoch (~25h) that is then used for event verification. These messages should +// be sorted based on Finalized.Number, but there is no mechanism to ensure we receiver every +// message. Nor are there any guarantees about ordering (unless explicitly enforced here) or +// gaps. +// +// **FinalizedEventData** contains verified and filtered contract events. It provides much +// stronger guarantees: Ordering is guaranteed/enforced by checking the block numbers. This +// additionally guarantees that no block was missed. Processing starts at (or up to 25h before) +// `HandshakeMsg1.StartBlocknum` + +type VerifiedHeadData struct { + Finalized ExecutionPayloadHeader `json:"finalized"` + Optimistic ExecutionPayloadHeader `json:"optimistic"` +} +type ExecutionPayloadHeader struct { + // There are more fields that we currently don't decode into this struct. + ParentHash common.Hash `json:"parent_hash"` + Number uint64 `json:"block_number"` + Timestamp uint64 `json:"timestamp"` // Seconds + BlockHash common.Hash `json:"block_hash"` +} + +type FinalizedEventData struct { + Number uint64 `json:"number"` + Timestamp uint64 `json:"timestamp"` // Milliseconds + Events []Event `json:"events"` +} + +// For now this code only supports RawEthereum events, as that's all we need in Shutter. +type Event struct { + ContractId uint `json:"contract_id"` + EventEnum `json:"data"` +} +type EventEnum struct { + Raw RawEth `json:"raweth"` +} +type RawEth struct { + Topics []string `json:"topics"` // hex + Data string `json:"data"` // hex +} + +type Attestation struct { + // The raw local attestation (already verified + checked according to config) + Report *attestation.Report + // Additional Data from the chain enclave (configuration). Should be checked against the application configuration. + // Current format (for Ethereum): JSON + Data []byte +} +type Connection struct { + conn net.Conn + attestation Attestation + + // Channel for communication/use by other systems + headers chan VerifiedHeadData + events chan FinalizedEventData + errors chan error +} +type Config struct { + // SGX related + SameSigner bool + SignerID []byte + MrEnclave []byte + ProductID *uint16 + MinISVSVN uint16 + + // Verifier settings + EventExtractionStartBlocknum uint64 // Set to all ones to disable event extraction `^uint64(0)` + Contracts []common.Address +} + +func DialVerifiedChainDataChannel(address string, config Config) (*Connection, error) { + conn, err := net.Dial("tcp", address) + if err != nil { + return nil, fmt.Errorf("Could not connect: %w", err) + } + + sk, err := ecdh.X25519().GenerateKey(rand.Reader) + if err != nil { + return nil, fmt.Errorf("Could not generate ECDH secret: %w", err) + } + + selfreport_bytes, err := enclave.GetLocalReport(nil, nil) + var target string + var selfreport *attestation.Report + if err == nil { + target = "0x" + hex.EncodeToString(extract_target_info(selfreport_bytes)) + // The selfreport_bytes we have above is not signed and I'm not sure if we can fully trust it. + // Ego gets another report for itself and verifies that, so we're doing the same to be on the safe side. + r, err := enclave.GetLocalReport(nil, selfreport_bytes) + if err != nil { + return nil, fmt.Errorf("Could not get second selfreport: %w", err) + } + r2, err := enclave.VerifyLocalReport(r) + if err != nil { + return nil, fmt.Errorf("Could not verify selfreport: %w", err) + } + selfreport = &r2 + } else if err.Error() == "OE_UNSUPPORTED" { + target = "" + } else { + return nil, fmt.Errorf("Could not get SelfReport: %w", err) + } + + // Convert easy-to-configure format to the communication format. + contracts := make([]contract, len(config.Contracts)) + for i, c := range config.Contracts { + contracts[i] = contract{ + Addr: c.Hex(), + Typ: DECODE_TYPE_RAW, + } + } + + h1 := handshakeMsg1{ + Version: 1, + AttestationTarget: target, + ECDH_Pubkey: "0x" + hex.EncodeToString(sk.PublicKey().Bytes()), + // Current ethereum-runner expects us to always provide some data here. + Data: erdstallParams{ + StartBlocknum: config.EventExtractionStartBlocknum, + Contracts: contracts, + }, + } + err = sendJSON(conn, h1) + if err != nil { + return nil, fmt.Errorf("Could not send HandshakeMsg1: %w", err) + } + var h2 handshakeMsg2 + err = recvJSON(conn, &h2) + if err != nil { + return nil, fmt.Errorf("Could not receive HandshakeMsg2: %w", err) + } + + other_pk_bytes, err := hex.DecodeString(strings.TrimPrefix(h2.ECDH_Pubkey, "0x")) + if err != nil { + return nil, fmt.Errorf("Could not hex decode ECDH pubkey: %w", err) + } + other_pk, err := ecdh.X25519().NewPublicKey(other_pk_bytes) + if err != nil { + return nil, fmt.Errorf("Could not convert to ECDH pubkey: %w", err) + } + sharedSecret, err := sk.ECDH(other_pk) + if err != nil { + return nil, fmt.Errorf("Could not finish ECDH: %w", err) + } + mac := hmac.New(sha3.New256, sharedSecret) + + attestation_data, err := base64.StdEncoding.DecodeString(h2.AttestationData) + if err != nil { + return nil, fmt.Errorf("could not base64 decode attestation_data") + } + + // Checking the report only makes sense if we've requested one (which we only do if we're running in a TEE, too) + var report *attestation.Report + if selfreport != nil { + report_bytes, err := hex.DecodeString(strings.TrimPrefix(h2.Report, "0x")) + if err != nil { + return nil, fmt.Errorf("Could not hex decode report: %w", err) + } + r, err := enclave.VerifyLocalReport(decorate_report(report_bytes)) + if err != nil { + return nil, fmt.Errorf("Invalid chain-enclave attestation report: %w", err) + } + + err = verify_report(r, *selfreport, config, other_pk_bytes, attestation_data) + if err != nil { + return nil, err + } + report = &r + } + + channel := make(chan VerifiedHeadData) + errchannel := make(chan error) + events := make(chan FinalizedEventData) + attestation := Attestation{Report: report, Data: attestation_data} + + c := Connection{conn, attestation, channel, events, errchannel} + go func() { + errchannel <- c.run(mac) + }() + + return &c, nil +} + +func allZero(b []byte) bool { + for _, v := range b { + if v != 0 { + return false + } + } + return true +} + +// Caller should verify the signature itself (to get an attestation.Report). This function just checks the report data. +func verify_report(report attestation.Report, selfreport attestation.Report, config Config, other_pk_bytes []byte, attestation_data []byte) error { + // Debug attribute + if !selfreport.Debug && report.Debug { + return fmt.Errorf("chain-enclave is in Debug mode while we are not") + } + + // Product ID + prodid := binary.LittleEndian.Uint16(report.ProductID[:2]) + if config.ProductID != nil && prodid != *config.ProductID { + return fmt.Errorf("unexpected ProductID: %v (%v)", prodid, report.ProductID) + } + if config.ProductID != nil && !allZero(report.ProductID[2:]) { + return fmt.Errorf("unexpected ProductID padding: %v", report.ProductID) + } + + // SVN + if config.MinISVSVN > 0 && report.SecurityVersion < uint(config.MinISVSVN) { + return fmt.Errorf("SVN too low: %d < %d", report.SecurityVersion, config.MinISVSVN) + } + + // Signer + if config.SameSigner && !bytes.Equal(report.SignerID, selfreport.SignerID) { + return fmt.Errorf("chain-enclave signer differs from our own") + } + if config.SignerID != nil && !bytes.Equal(report.SignerID, config.SignerID) { + return fmt.Errorf("unexpected mrsigner") + } + + // MRENCLAVE + if config.MrEnclave != nil && !bytes.Equal(report.UniqueID, config.MrEnclave) { + return fmt.Errorf("unexpected mrenclave") + } + + // report.TCBStatus is always tcbstatus.Unknown for local attestations => No need to check + + // AttestationData (ecdh_pk) + if !bytes.Equal(other_pk_bytes, report.Data[:32]) { + return fmt.Errorf("ECDH PublicKey doesn't match report_data") + } + + // AttestationData (extra) + // See get_attestation_ecdh in lib/enclave/src/lib.rs + // Contrary to Erdstall we have (and output) the complete attestation_data and should thus check its integrity. + // The Erdstall enclave doesn't have this and only processes/returns its hash. + hasher := sha3.New256() + _, err := hasher.Write(attestation_data) + if err != nil { + return fmt.Errorf("Could not hash attestation_data: %w", err) + } + attestation_data_hash := hasher.Sum(nil) + if !bytes.Equal(attestation_data_hash, report.Data[32:]) { + return fmt.Errorf("Attestation data doesn't match report_data") + } + + return nil +} + +func (c *Connection) run(mac hash.Hash) error { + for { + var msg message + err := recvJSON(c.conn, &msg) + if err != nil { + return fmt.Errorf("Could not receive message: %w", err) + } + + data, err := base64.StdEncoding.DecodeString(msg.Data) + if err != nil { + return fmt.Errorf("Could not base64 decode message.Data: %w", err) + } + + received_mac, err := base64.StdEncoding.DecodeString(msg.Mac) + if err != nil { + return fmt.Errorf("Could not hex decode message.Mac: %w", err) + } + + mac.Reset() + _, err = mac.Write(data) + if err != nil { + return fmt.Errorf("Could not hash message.Data: %w", err) + } + computed_mac := mac.Sum(nil) + if !hmac.Equal(computed_mac, received_mac) { + return fmt.Errorf("Invalid message MAC") + } + + // First, try to decode as FinalizedEventData + var d FinalizedEventData + err1 := json.Unmarshal(data, &d) + if err1 == nil && d.Number > 0 { + c.events <- d + continue + } + + // If that fails: Try VerifiedHeadData + var v VerifiedHeadData + err2 := json.Unmarshal(data, &v) + if err2 == nil { + c.headers <- v + continue + } + + // Could not decode into either type, log as much as we can and fail loudly + fmt.Println(string(data)) + fmt.Printf("Failed decoding: %v, %v", err1, err2) + panic(err2) + } +} + +func (c *Connection) Close() { + c.conn.Close() +} +func (c *Connection) Headers() <-chan VerifiedHeadData { + return c.headers +} +func (c *Connection) Events() <-chan FinalizedEventData { + return c.events +} +func (c *Connection) Errors() <-chan error { + return c.errors +} +func (c *Connection) Attestation() Attestation { + return c.attestation +} + +func sendJSON(conn net.Conn, v any) error { + data, err := json.Marshal(v) + if err != nil { + return err + } + // 4-byte big-endian length prefixed json encoding + _, err = conn.Write(binary.BigEndian.AppendUint32(nil, uint32(len(data)))) + if err != nil { + return err + } + _, err = conn.Write(data) + return err +} +func recvJSON(conn net.Conn, v any) error { + var len_bytes [4]byte + _, err := io.ReadFull(conn, len_bytes[:]) + if err != nil { + return err + } + l := binary.BigEndian.Uint32(len_bytes[:]) + data := make([]byte, l) + _, err = io.ReadFull(conn, data[:]) + if err != nil { + return err + } + return json.Unmarshal(data, v) +} + +type handshakeMsg1 struct { + Version uint16 `json:"version"` + AttestationTarget string `json:"attestation_target,omitempty"` // hex + ECDH_Pubkey string `json:"ecdh_pk"` // hex + Data erdstallParams `json:"data"` +} +type handshakeMsg2 struct { + // Local attestation report (not a remote attestation quote) + Report string `json:"report"` // hex | null + ECDH_Pubkey string `json:"ecdh_pk"` // hex + AttestationData string `json:"attestation_data"` // base64 encoded json +} +type message struct { + Data string `json:"data"` // base64 + Mac string `json:"mac"` // base64 of [u8; 32] +} +type erdstallParams struct { + StartBlocknum uint64 `json:"extraction_start_blocknum"` + Contracts []contract `json:"contracts"` +} +type contract struct { + Addr string `json:"addr"` + Typ string `json:"typ"` // Decode Type +} + +const TARGET_INFO_SIZE = 512 +const REPORT_SIZE = 432 +const OE_REPORT_HEADER_SIZE = 16 + +const DECODE_TYPE_RAW = "raw" + +func extract_target_info(oe_report []byte) []byte { + // EGo unfortunately has no way to directly get a TargetInfo object, but the + // report contains everything we need, so we can build one ourselves. + // Alternatively we could have sent the report to the rust side, but I think + // it is better to use the types closer to the cpu instructions for + // communicating between Rust and Go. + + // Details of report: + // - https://github.com/openenclave/openenclave/blob/416ce1964ec1041e7f8fcd9ecedf44f951c92489/include/openenclave/bits/sgx/sgxtypes.h#L599-L660 + // - https://github.com/fortanix/rust-sgx/blob/c16c8aa29f1daf85159ff25f1290553bd334f879/intel-sgx/sgx-isa/src/lib.rs#L640-L660 + + // Details of target info: + // - https://github.com/openenclave/openenclave/blob/416ce1964ec1041e7f8fcd9ecedf44f951c92489/include/openenclave/bits/sgx/sgxtypes.h#L541-L557 + // - https://github.com/fortanix/rust-sgx/blob/c16c8aa29f1daf85159ff25f1290553bd334f879/intel-sgx/sgx-isa/src/lib.rs#L732-L756 + + // Details of the report header in OpenEnclave: + // - https://github.com/openenclave/openenclave/blob/cdeb95c1ec163117de409295333b6b2702013e08/enclave/core/sgx/report.c#L349-L361 + // - https://github.com/openenclave/openenclave/blob/cdeb95c1ec163117de409295333b6b2702013e08/include/openenclave/internal/report.h#L71-L77 + + if len(oe_report) != OE_REPORT_HEADER_SIZE+REPORT_SIZE { + panic(fmt.Sprintf("Unexpected OE report size, got %v (%d bytes)", oe_report, len(oe_report))) + } + report := oe_report[16:] + + target_info := make([]byte, TARGET_INFO_SIZE) + copy(target_info[0:32], report[64:96]) // mrenclave + copy(target_info[32:48], report[48:64]) // attributes + copy(target_info[52:56], report[16:20]) // miscselect + + return target_info +} + +const OE_REPORT_TYPE_SGX_LOCAL = 1 +const OE_REPORT_TYPE_SGX_REMOTE = 2 + +// prepends relevant header data to a raw report +func decorate_report(report []byte) []byte { + // For checking the report we need to add the header. + if len(report) != REPORT_SIZE { + panic(fmt.Sprintf("Unexpected report size, got %d (%d bytes)", report, len(report))) + } + oe_report := make([]byte, OE_REPORT_HEADER_SIZE+REPORT_SIZE) + oe_report[0] = 0x01 // version (uint32) + oe_report[4] = OE_REPORT_TYPE_SGX_LOCAL // report_type (uint32) + binary.LittleEndian.PutUint64(oe_report[8:16], REPORT_SIZE) + copy(oe_report[16:], report) + + return oe_report +} diff --git a/rolling-shutter/tee/sealedsecret.go b/rolling-shutter/tee/sealedsecret.go new file mode 100644 index 000000000..a73779839 --- /dev/null +++ b/rolling-shutter/tee/sealedsecret.go @@ -0,0 +1,168 @@ +/* +TEE-based hardware encryption functionality for secret data. + +This package contains functionality for handling secrets so that they can be +securely stored on — and loaded from — disk. This is a simple serialisation +step, and depending on whether TEE hardware is present (and whether the code is +running in an enclave), these functions either perform hardware sealing and +unsealing, or they simply pass the original data along. This ensures that old +persistence/config files can be loaded as usual, but new files written from +within an enclave will have their secrets sealed. + +This package is designed to be minimally invasive for the rest of the codebase. +It does not protect against replay attacks where old persistences are loaded. +The sealed data can only be unsealed by the same CPU that sealed it and only from within a binary that is signed with the same key as the binary that sealed it. + +The threat model of this package is to protect a node operator from hackers with admin access that seek to leak his secrets. It does not provide trust in the +node operator's honesty to the outside world. It merely makes the operator trust his own machine to not leak secrets sealed using this package. +*/ +package tee + +import ( + "encoding/hex" + "errors" + "fmt" + "strings" + "testing" + + egoattestation "github.com/edgelesssys/ego/attestation" + "github.com/edgelesssys/ego/ecrypto" + egoenclave "github.com/edgelesssys/ego/enclave" +) + +// If we are running in SGX, this contains the enclave identity. +var tpmIdentity *egoattestation.Report = nil + +func init() { + ident, hasTPM, err := QueryTPMIdentity() + if err != nil { + panic(fmt.Sprintf("Could not query TPM identity: %v", err)) + } + if hasTPM { + tpmIdentity = &ident + } +} + +func HasTEE() bool { + return tpmIdentity != nil +} + +func QueryTPMIdentity() (identity egoattestation.Report, exists bool, err error) { + // Get some information about where we're running (if in a TEE and some + // details about the TEE we're running in) + identity, err = egoenclave.GetSelfReport() + exists = true + if err != nil { + if err.Error() == "OE_UNSUPPORTED" { + exists = false + err = nil + } else { + err = fmt.Errorf("Could not get self-report: %w", err) + } + } + return +} + +// All sealed blobs are prefixed with this string, to distinguish them from +// normal blobs. Normal data should never start with this string. +const sealedPrefix = "sealed://" + +func blobIsSealed(blob []byte) (sealed bool, payload []byte) { + if len(blob) >= len(sealedPrefix) && string(blob[:len(sealedPrefix)]) == sealedPrefix { + return true, blob[len(sealedPrefix):] + } + return false, blob +} + +// If we are in a TEE, always seal the secret. Otherwise, simply convert to hex. +func SealSecretAsHex(secret []byte) (string, error) { + return SealSecretAsCustomText(secret, hex.EncodeToString) +} + +func SealSecretAsCustomText(secret []byte, encoder func([]byte) string) (string, error) { + if sealed, _ := blobIsSealed(secret); sealed { + return "", errors.New("Blob looks like it is already sealed") + } + + if !HasTEE() { + return encoder(secret), nil + } + + sealed, err := ecrypto.SealWithProductKey(secret, nil) + if err != nil { + return "", fmt.Errorf("failed to seal secret: %w", err) + } + + return sealedPrefix + encoder(sealed), nil +} + +// If we are in a TEE, always seal the secret. Otherwise, returns the raw bytes. +func SealSecret(secret []byte) ([]byte, error) { + if sealed, _ := blobIsSealed(secret); sealed { + return nil, errors.New("Blob looks like it is already sealed") + } + + if !HasTEE() { + return secret, nil + } + + sealed, err := ecrypto.SealWithProductKey(secret, nil) + if err != nil { + return nil, fmt.Errorf("failed to seal secret: %w", err) + } + + ret := []byte(sealedPrefix) + return append(ret, sealed...), nil +} + +// If we are in a TEE, unseal the secret, if it is sealed. Otherwise, simply parse the hex string. +func UnsealSecretFromHex(str string) ([]byte, error) { + return UnsealSecretFromCustomText(str, hex.DecodeString) +} + +func UnsealSecretFromCustomText(str string, decoder func(string) ([]byte, error)) ([]byte, error) { + if cut, ok := strings.CutPrefix(str, sealedPrefix); ok { + bytes, err := decoder(cut) + if err != nil { + return nil, err + } + + if !HasTEE() { + return nil, errors.New("Cannot unseal secret: not in a TEE") + } + + return ecrypto.Unseal(bytes, nil) + } + + return decoder(str) +} + +func UnsealSecret(secret []byte) ([]byte, error) { + if sealed, payload := blobIsSealed(secret); sealed { + if !HasTEE() { + return nil, errors.New("Cannot unseal secret: not in a TEE") + } + + return ecrypto.Unseal(payload, nil) + } + + return secret, nil +} + +// This function is only for testing purposes, when the non-TEE behaviour is +// needed somewhere. +// +// Executes the passed function with temporarily disabled TEE functionality +// (during the execution, HasTEE() returns false). If this function is called +// outside of tests, it panics. +func DoWithoutTEE(the_thing func()) { + if !testing.Testing() { + panic("This function is for tests only") + } + + restore := tpmIdentity + defer func() { tpmIdentity = restore }() + tpmIdentity = nil + + the_thing() +} diff --git a/rolling-shutter/tee/sealedsecret_test.go b/rolling-shutter/tee/sealedsecret_test.go new file mode 100644 index 000000000..ec91172fd --- /dev/null +++ b/rolling-shutter/tee/sealedsecret_test.go @@ -0,0 +1,100 @@ +package tee + +import ( + "strings" + "testing" + + "gotest.tools/v3/assert" +) + +// Start of utlities + +func RequiresTEE(t *testing.T) { + if !HasTEE() { + t.Skip("Test requires TEE, but we are not running on a TEE") + } +} + +func RunWithoutTEE(t *testing.T, name string, test func(t *testing.T)) { + t.Helper() + t.Run(name, func(t *testing.T) { + DoWithoutTEE(func() { test(t) }) + }) +} + +func RunWithTEE(t *testing.T, name string, test func(t *testing.T)) { + t.Helper() + t.Run(name, func(t *testing.T) { + RequiresTEE(t) + test(t) + }) +} + +// End of utilities + +// Start of test cases + +func TestBlobIsSealed(t *testing.T) { + t.Run("Sealed input", func(t *testing.T) { + input := append([]byte(sealedPrefix), 1, 2, 3, 4) + isSealed, payload := blobIsSealed(input) + assert.Equal(t, isSealed, true, "isSealed") + assert.DeepEqual(t, payload, []byte{1, 2, 3, 4}) + }) + + t.Run("Unsealed input", func(t *testing.T) { + input := []byte{5, 6, 7, 8} + isSealed, payload := blobIsSealed(input) + assert.Equal(t, isSealed, false, "isSealed") + assert.DeepEqual(t, payload, []byte{5, 6, 7, 8}) + }) +} + +func TestSealSecretAsHex(t *testing.T) { + RunWithoutTEE(t, "Without TEE", func(t *testing.T) { + input := []byte{0x12, 0x34, 0x56, 0x78} + output, err := SealSecretAsHex(input) + assert.NilError(t, err, "seal") + assert.Equal(t, output, "12345678", "seal") + + reverse, err := UnsealSecretFromHex(output) + assert.NilError(t, err, "unseal") + assert.DeepEqual(t, reverse, input) + }) + + RunWithTEE(t, "HW sealing", func(t *testing.T) { + input := []byte{0x9a, 0xbc, 0xde, 0xf0} + output, err := SealSecretAsHex(input) + assert.NilError(t, err, "seal") + assert.Assert(t, strings.HasPrefix(output, sealedPrefix), "seal") + + reverse, err := UnsealSecretFromHex(output) + assert.NilError(t, err, "unseal") + assert.DeepEqual(t, reverse, input) + }) +} + +func TestSealSecret(t *testing.T) { + RunWithoutTEE(t, "Without TEE", func(t *testing.T) { + input := []byte{0x12, 0x34, 0x56, 0x78} + output, err := SealSecret(input) + assert.NilError(t, err, "seal") + assert.DeepEqual(t, output, input) + + reverse, err := UnsealSecret(output) + assert.NilError(t, err, "unseal") + assert.DeepEqual(t, reverse, input) + }) + + RunWithTEE(t, "HW sealing", func(t *testing.T) { + input := []byte{0x9a, 0xbc, 0xde, 0xf0} + output, err := SealSecret(input) + assert.NilError(t, err, "seal") + isSealed, _ := blobIsSealed(output) + assert.Assert(t, isSealed, "seal") + + reverse, err := UnsealSecret(output) + assert.NilError(t, err, "unseal") + assert.DeepEqual(t, reverse, input) + }) +}