From af18566e308e79475916fb1a6c502177f8fc8a12 Mon Sep 17 00:00:00 2001 From: Peter Nose Date: Mon, 15 Aug 2022 09:02:00 +0200 Subject: [PATCH] keymanager: Add e2e tests for ephemeral keys --- .buildkite/code.pipeline.yml | 2 +- Cargo.lock | 3 + .../e2e/runtime/keymanager_key_generation.go | 236 ++++++++++++++++++ .../scenario/e2e/runtime/runtime.go | 2 + tests/runtimes/simple-keyvalue/Cargo.toml | 3 + tests/runtimes/simple-keyvalue/src/main.rs | 2 + tests/runtimes/simple-keyvalue/src/methods.rs | 96 ++++++- tests/runtimes/simple-keyvalue/src/types.rs | 14 ++ 8 files changed, 355 insertions(+), 3 deletions(-) create mode 100644 go/oasis-test-runner/scenario/e2e/runtime/keymanager_key_generation.go diff --git a/.buildkite/code.pipeline.yml b/.buildkite/code.pipeline.yml index 199a51d74f0..1c8c56b6d3a 100644 --- a/.buildkite/code.pipeline.yml +++ b/.buildkite/code.pipeline.yml @@ -263,7 +263,7 @@ steps: - export CFLAGS_x86_64_fortanix_unknown_sgx="-isystem/usr/include/x86_64-linux-gnu -mlvi-hardening -mllvm -x86-experimental-lvi-inline-asm-hardening" - export CC_x86_64_fortanix_unknown_sgx=clang-11 # Only run runtime scenarios as others do not use SGX. - - .buildkite/scripts/test_e2e.sh --scenario e2e/runtime/runtime-encryption --scenario e2e/runtime/trust-root + - .buildkite/scripts/test_e2e.sh --scenario e2e/runtime/runtime-encryption --scenario e2e/runtime/trust-root --scenario e2e/runtime/keymanager-key-generation artifact_paths: - coverage-merged-e2e-*.txt - /tmp/e2e/**/*.log diff --git a/Cargo.lock b/Cargo.lock index 37cbb464118..3b7413c6cd1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2747,6 +2747,7 @@ name = "simple-keyvalue" version = "0.0.0" dependencies = [ "anyhow", + "base64", "byteorder", "io-context", "oasis-cbor", @@ -2755,9 +2756,11 @@ dependencies = [ "oasis-core-keymanager-client", "oasis-core-runtime", "oasis-core-tools", + "rand 0.7.3", "simple-keymanager", "thiserror", "tokio 1.19.2", + "x25519-dalek", ] [[package]] diff --git a/go/oasis-test-runner/scenario/e2e/runtime/keymanager_key_generation.go b/go/oasis-test-runner/scenario/e2e/runtime/keymanager_key_generation.go new file mode 100644 index 00000000000..476395df0e4 --- /dev/null +++ b/go/oasis-test-runner/scenario/e2e/runtime/keymanager_key_generation.go @@ -0,0 +1,236 @@ +package runtime + +import ( + "context" + "fmt" + + beacon "github.com/oasisprotocol/oasis-core/go/beacon/api" + "github.com/oasisprotocol/oasis-core/go/common" + "github.com/oasisprotocol/oasis-core/go/common/cbor" + consensus "github.com/oasisprotocol/oasis-core/go/consensus/api" + "github.com/oasisprotocol/oasis-core/go/oasis-test-runner/env" + "github.com/oasisprotocol/oasis-core/go/oasis-test-runner/oasis" + "github.com/oasisprotocol/oasis-core/go/oasis-test-runner/scenario" +) + +// KeymanagerKeyGeneration is the keymanager key generation scenario. +// +// It uses encryption and decryption transactions provided by the +// simple key/value runtime to test whether the key manager client +// can retrieve private and public ephemeral keys from the key manager +// and if the latter generates those according to the specifications. +var KeymanagerKeyGeneration scenario.Scenario = newKmKeyGenerationImpl() + +type kmKeyGenerationImpl struct { + runtimeImpl +} + +func newKmKeyGenerationImpl() scenario.Scenario { + return &kmKeyGenerationImpl{ + runtimeImpl: *newRuntimeImpl("keymanager-key-generation", BasicKVEncTestClient), + } +} + +func (sc *kmKeyGenerationImpl) Fixture() (*oasis.NetworkFixture, error) { + return sc.runtimeImpl.Fixture() +} + +func (sc *kmKeyGenerationImpl) Clone() scenario.Scenario { + return &kmKeyGenerationImpl{ + runtimeImpl: *sc.runtimeImpl.Clone().(*runtimeImpl), + } +} + +func (sc *kmKeyGenerationImpl) Run(childEnv *env.Env) error { + ctx := context.Background() + if err := sc.runtimeImpl.startNetworkAndTestClient(ctx, childEnv); err != nil { + return err + } + + // Wait for the client to exit. + if err := sc.waitTestClientOnly(); err != nil { + return err + } + + // Initialize the nonce DRBG. + rng, err := drbgFromSeed( + []byte("oasis-core/oasis-test-runner/e2e/runtime/keymanager-key-generation"), + []byte("keymanager-key-generation"), + ) + if err != nil { + return err + } + + // Data needed for encryption and decryption. + keyPairID := "key pair id" + plaintext := "The quick brown fox jumps over the lazy dog" + + epoch, err := sc.Net.Controller().Beacon.GetEpoch(ctx, consensus.HeightLatest) + if err != nil { + return fmt.Errorf("failed to get epoch at the latest height: %w", err) + } + + // Encrypt plaintext using ephemeral public key for the current epoch. + // Successful encryption indicates that the key manager generated + // an ephemeral public key. + sc.Logger.Info("encrypting plaintext") + ciphertext, err := sc.submitKeyValueRuntimeEncryptTx( + ctx, + runtimeID, + rng.Uint64(), + epoch, + keyPairID, + plaintext, + ) + if err != nil { + return fmt.Errorf("failed to encrypt plaintext: %w", err) + } + + // Decrypt ciphertext using ephemeral private key for the current epoch. + // Successful decryption indicates that the key manager generates + // matching public and private ephemeral keys. + sc.Logger.Info("decrypting ciphertext") + decrypted, err := sc.submitKeyValueRuntimeDecryptTx( + ctx, + runtimeID, + rng.Uint64(), + epoch, + keyPairID, + ciphertext, + ) + if err != nil { + return fmt.Errorf("failed to decrypt ciphertext: %w", err) + } + if decrypted != plaintext { + return fmt.Errorf("decrypted ciphertext does match the plaintext (got: '%s', expected: '%s')", decrypted, plaintext) + } + + // Decrypt ciphertext using ephemeral private key for the previous epoch. + // As ephemeral keys are derived from the epoch, the decryption should + // fail. + sc.Logger.Info("decrypting ciphertext with wrong epoch") + decrypted, err = sc.submitKeyValueRuntimeDecryptTx( + ctx, + runtimeID, + rng.Uint64(), + epoch-1, + keyPairID, + ciphertext, + ) + if err == nil && decrypted == plaintext { + return fmt.Errorf("decryption with wrong epoch should fail or produce garbage") + } + + // Decrypt ciphertext using ephemeral private key derived from wrong + // key pair id. As ephemeral keys are derived from the id, the decryption + // should fail. + sc.Logger.Info("decrypting ciphertext with wrong key pair id") + decrypted, err = sc.submitKeyValueRuntimeDecryptTx( + ctx, + runtimeID, + rng.Uint64(), + epoch, + "wrong key pair id", + ciphertext, + ) + if err == nil && decrypted == plaintext { + return fmt.Errorf("decryption with wrong key pair id should fail or produce garbage") + } + + // Change epoch and test what happens if epoch is invalid, + // i.e. too old or somewhere in the future. + epoch = epoch + 10 + + // Encrypt plaintext using epoch that is in the future. + // As public ephemeral keys are not allowed to be derived + // for future epoch neither for epoch that are too far + // in the past, the encryption should fail. + sc.Logger.Info("encrypting plaintext with invalid epoch") + _, err = sc.submitKeyValueRuntimeEncryptTx( + ctx, + runtimeID, + rng.Uint64(), + epoch, + keyPairID, + plaintext, + ) + if err == nil { + return fmt.Errorf("encryption with invalid epoch should fail") + } + + // Decrypt ciphertext using epoch that is in the future. + // The same rule holds for private ephemeral keys, + // so the encryption should fail. + sc.Logger.Info("decrypting ciphertext with invalid epoch") + _, err = sc.submitKeyValueRuntimeDecryptTx( + ctx, + runtimeID, + rng.Uint64(), + epoch, + keyPairID, + ciphertext, + ) + if err == nil { + return fmt.Errorf("decryption with invalid epoch should fail") + } + + return nil +} + +func (sc *kmKeyGenerationImpl) submitKeyValueRuntimeEncryptTx( + ctx context.Context, + id common.Namespace, + nonce uint64, + epoch beacon.EpochTime, + keyPairID string, + plaintext string, +) (string, error) { + rawRsp, err := sc.submitRuntimeTx(ctx, runtimeID, nonce, "encrypt", struct { + Epoch uint64 `json:"epoch"` + KeyPairID string `json:"key_pair_id"` + Plaintext string `json:"plaintext"` + }{ + Epoch: uint64(epoch), + KeyPairID: keyPairID, + Plaintext: plaintext, + }) + if err != nil { + return "", fmt.Errorf("failed to submit encrypt tx to runtime: %w", err) + } + + var rsp string + if err = cbor.Unmarshal(rawRsp, &rsp); err != nil { + return "", fmt.Errorf("failed to unmarshal response from runtime: %w", err) + } + + return rsp, nil +} + +func (sc *kmKeyGenerationImpl) submitKeyValueRuntimeDecryptTx( + ctx context.Context, + id common.Namespace, + nonce uint64, + epoch beacon.EpochTime, + keyPairID string, + ciphertext string, +) (string, error) { + rawRsp, err := sc.submitRuntimeTx(ctx, runtimeID, nonce, "decrypt", struct { + Epoch uint64 `json:"epoch"` + KeyPairID string `json:"key_pair_id"` + Ciphertext string `json:"ciphertext"` + }{ + Epoch: uint64(epoch), + KeyPairID: keyPairID, + Ciphertext: ciphertext, + }) + if err != nil { + return "", fmt.Errorf("failed to submit decrypt tx to runtime: %w", err) + } + + var rsp string + if err = cbor.Unmarshal(rawRsp, &rsp); err != nil { + return "", fmt.Errorf("failed to unmarshal response from runtime: %w", err) + } + + return rsp, nil +} diff --git a/go/oasis-test-runner/scenario/e2e/runtime/runtime.go b/go/oasis-test-runner/scenario/e2e/runtime/runtime.go index 2f09edf690b..463f3146898 100644 --- a/go/oasis-test-runner/scenario/e2e/runtime/runtime.go +++ b/go/oasis-test-runner/scenario/e2e/runtime/runtime.go @@ -759,6 +759,8 @@ func RegisterScenarios() error { StorageEarlyStateSync, // Sentry test. Sentry, + // Keymanager key generation test. + KeymanagerKeyGeneration, // Keymanager restart test. KeymanagerRestart, // Keymanager replicate test. diff --git a/tests/runtimes/simple-keyvalue/Cargo.toml b/tests/runtimes/simple-keyvalue/Cargo.toml index 2d93ac55e86..9a71d112cb1 100644 --- a/tests/runtimes/simple-keyvalue/Cargo.toml +++ b/tests/runtimes/simple-keyvalue/Cargo.toml @@ -32,6 +32,9 @@ anyhow = "1.0" thiserror = "1.0" io-context = "0.2.0" byteorder = "1.4.3" +rand = "0.7.3" +base64 = "0.13.0" +x25519-dalek = "1.1.0" tokio = { version = "1", features = ["rt"] } [build-dependencies] diff --git a/tests/runtimes/simple-keyvalue/src/main.rs b/tests/runtimes/simple-keyvalue/src/main.rs index 33160c001b5..23460fcd236 100644 --- a/tests/runtimes/simple-keyvalue/src/main.rs +++ b/tests/runtimes/simple-keyvalue/src/main.rs @@ -143,6 +143,8 @@ impl Dispatcher { "enc_insert" => Self::dispatch_call(ctx, tx.args, Methods::enc_insert), "enc_get" => Self::dispatch_call(ctx, tx.args, Methods::enc_get), "enc_remove" => Self::dispatch_call(ctx, tx.args, Methods::enc_remove), + "encrypt" => Self::dispatch_call(ctx, tx.args, Methods::encrypt), + "decrypt" => Self::dispatch_call(ctx, tx.args, Methods::decrypt), _ => Err("method not found".to_string()), } } diff --git a/tests/runtimes/simple-keyvalue/src/methods.rs b/tests/runtimes/simple-keyvalue/src/methods.rs index 55fea35245b..2303ba9ffc6 100644 --- a/tests/runtimes/simple-keyvalue/src/methods.rs +++ b/tests/runtimes/simple-keyvalue/src/methods.rs @@ -1,14 +1,20 @@ //! Test method implementations. -use std::collections::BTreeMap; +use std::{collections::BTreeMap, convert::TryInto}; +use base64; use io_context::Context as IoContext; +use rand::rngs::OsRng; +use x25519_dalek; use super::{crypto::EncryptionContext, types::*, Context, TxContext}; use oasis_core_keymanager_client::KeyPairId; use oasis_core_runtime::{ common::{ - crypto::{hash::Hash, mrae::deoxysii::NONCE_SIZE}, + crypto::{ + hash::Hash, + mrae::deoxysii::{self, NONCE_SIZE}, + }, key_format::KeyFormat, versioned::Versioned, }, @@ -350,6 +356,92 @@ impl Methods { .transpose() .map_err(|err| err.to_string()) } + + /// ElGamal encryption. + pub fn encrypt(ctx: &mut TxContext, args: Encrypt) -> Result, String> { + if ctx.is_check_only() { + return Ok(None); + } + + // Derive key pair ID based on the given ID. + let key_pair_id = KeyPairId::from(Hash::digest_bytes(args.key_pair_id.as_bytes()).as_ref()); + + // Fetch public key. + let io_ctx = IoContext::create_child(&ctx.parent.core.io_ctx); + let result = + ctx.parent + .key_manager + .get_public_ephemeral_key(io_ctx, key_pair_id, args.epoch); + let long_term_pk = tokio::runtime::Handle::current() + .block_on(result) + .map_err(|err| err.to_string())? + .ok_or("public ephemeral key not available")?; + + // Generate ephemeral key. + let mut rng = OsRng {}; + let ephemeral_sk = x25519_dalek::StaticSecret::new(&mut rng); + let ephemeral_pk = x25519_dalek::PublicKey::from(&ephemeral_sk); + + // ElGamal encryption. + let ciphertext = deoxysii::box_seal( + &[0u8; NONCE_SIZE], + args.plaintext.into_bytes(), + vec![], + &long_term_pk.key.0, + &ephemeral_sk.to_bytes(), + ) + .map_err(|err| format!("failed to encrypt plaintext: {}", err))?; + + // Encode ephemeral_pk || ciphertext to Base64. + let mut v = ephemeral_pk.as_bytes().to_vec(); + v.extend(ciphertext); + let ciphertext = base64::encode(v); + + Ok(Some(ciphertext)) + } + + /// ElGamal decryption. + pub fn decrypt(ctx: &mut TxContext, args: Decrypt) -> Result, String> { + if ctx.is_check_only() { + return Ok(None); + } + + // Derive key pair ID based on the given ID. + let key_pair_id = KeyPairId::from(Hash::digest_bytes(args.key_pair_id.as_bytes()).as_ref()); + + // Fetch private key. + let io_ctx = IoContext::create_child(&ctx.parent.core.io_ctx); + let result = + ctx.parent + .key_manager + .get_or_create_ephemeral_keys(io_ctx, key_pair_id, args.epoch); + let long_term_sk = tokio::runtime::Handle::current() + .block_on(result) + .map_err(|err| format!("private ephemeral key not available: {}", err))?; + + // Decode ephemeral_pk || ciphertext from Base64. + let ciphertext = base64::decode(args.ciphertext).map_err(|err| err.to_string())?; + let ephemeral_pk = ciphertext + .get(0..32) + .ok_or("invalid ciphertext")? + .try_into() + .unwrap(); + let ciphertext = ciphertext.get(32..).ok_or("invalid ciphertext")?.to_vec(); + + // ElGamal decryption. + let plaintext = deoxysii::box_open( + &[0u8; NONCE_SIZE], + ciphertext, + vec![], + ephemeral_pk, + &long_term_sk.input_keypair.sk.0, + ) + .map(String::from_utf8) + .map_err(|err| format!("failed to decrypt ciphertext: {}", err))? + .map_err(|err| format!("failed to decrypt ciphertext: {}", err))?; + + Ok(Some(plaintext)) + } } /// Implementation of a test block handler. diff --git a/tests/runtimes/simple-keyvalue/src/types.rs b/tests/runtimes/simple-keyvalue/src/types.rs index d6bf6dd2704..cc54f3ffb85 100644 --- a/tests/runtimes/simple-keyvalue/src/types.rs +++ b/tests/runtimes/simple-keyvalue/src/types.rs @@ -40,6 +40,20 @@ pub struct KeyValue { pub value: String, } +#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)] +pub struct Encrypt { + pub epoch: u64, + pub key_pair_id: String, + pub plaintext: String, +} + +#[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)] +pub struct Decrypt { + pub epoch: u64, + pub key_pair_id: String, + pub ciphertext: String, +} + #[derive(Clone, Debug, Default, cbor::Encode, cbor::Decode)] pub struct Withdraw { pub withdraw: staking::Withdraw,