Browse Source

Added a wrapper around exported root keys which
provides integrity and confidentiality protection using
a key derived from the root password.

Matthew Carr 2 years ago
parent
commit
f58a85f5a7

+ 182 - 0
crates/btnode/Cargo.lock

@@ -11,6 +11,15 @@ dependencies = [
  "memchr",
 ]
 
+[[package]]
+name = "ansi_term"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+dependencies = [
+ "winapi",
+]
+
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -43,6 +52,29 @@ dependencies = [
  "base64",
 ]
 
+[[package]]
+name = "bindgen"
+version = "0.59.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "clap",
+ "env_logger",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "peeking_take_while",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "which",
+]
+
 [[package]]
 name = "bitfield"
 version = "0.13.2"
@@ -70,11 +102,13 @@ dependencies = [
  "serde",
  "serde-big-array",
  "serde-block-tree",
+ "static_assertions",
  "strum",
  "strum_macros",
  "tempdir",
  "tss-esapi",
  "tss-esapi-sys",
+ "zeroize",
 ]
 
 [[package]]
@@ -83,12 +117,47 @@ version = "1.0.73"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
 
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
 [[package]]
 name = "cfg-if"
 version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
+[[package]]
+name = "clang-sys"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a050e2153c5be08febd6734e29298e844fdb0fa21aeddd63b4eb7baa106c69b"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "clap"
+version = "2.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
 [[package]]
 name = "ctor"
 version = "0.1.23"
@@ -99,6 +168,12 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "either"
+version = "1.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797"
+
 [[package]]
 name = "enumflags2"
 version = "0.7.5"
@@ -153,6 +228,12 @@ version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba"
 
+[[package]]
+name = "glob"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
+
 [[package]]
 name = "harness"
 version = "0.0.1"
@@ -189,12 +270,34 @@ version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
 
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
 [[package]]
 name = "libc"
 version = "0.2.132"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5"
 
+[[package]]
+name = "libloading"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd"
+dependencies = [
+ "cfg-if",
+ "winapi",
+]
+
 [[package]]
 name = "log"
 version = "0.4.17"
@@ -230,6 +333,12 @@ dependencies = [
  "autocfg",
 ]
 
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
 [[package]]
 name = "nix"
 version = "0.25.0"
@@ -244,6 +353,16 @@ dependencies = [
  "pin-utils",
 ]
 
+[[package]]
+name = "nom"
+version = "7.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
 [[package]]
 name = "num-derive"
 version = "0.3.3"
@@ -328,6 +447,12 @@ dependencies = [
  "vcpkg",
 ]
 
+[[package]]
+name = "peeking_take_while"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099"
+
 [[package]]
 name = "pest"
 version = "2.1.3"
@@ -465,6 +590,12 @@ dependencies = [
  "winapi",
 ]
 
+[[package]]
+name = "rustc-hash"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
+
 [[package]]
 name = "rustc_version"
 version = "0.3.3"
@@ -544,12 +675,30 @@ dependencies = [
  "syn",
 ]
 
+[[package]]
+name = "shlex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3"
+
 [[package]]
 name = "stable_deref_trait"
 version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
 
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
+
+[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
 [[package]]
 name = "strum"
 version = "0.24.1"
@@ -620,6 +769,15 @@ dependencies = [
  "winapi-util",
 ]
 
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
 [[package]]
 name = "tss-esapi"
 version = "7.1.0"
@@ -648,6 +806,7 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0e2f37914ec4d494d145cfa18bb8429498b238d63c47a08b89d09c1ec2545ff0"
 dependencies = [
+ "bindgen",
  "pkg-config",
  "target-lexicon",
 ]
@@ -664,6 +823,12 @@ version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
 
+[[package]]
+name = "unicode-width"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
+
 [[package]]
 name = "unicode-xid"
 version = "0.2.3"
@@ -676,6 +841,23 @@ version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
 
+[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
+name = "which"
+version = "4.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae"
+dependencies = [
+ "either",
+ "lazy_static",
+ "libc",
+]
+
 [[package]]
 name = "winapi"
 version = "0.3.9"

+ 3 - 1
crates/btnode/Cargo.toml

@@ -17,9 +17,11 @@ strum = { version = "^0.24.0", features = ["derive"] }
 strum_macros = { version = "^0.24.0" }
 env_logger = "0.9.0"
 log = "0.4.17"
-tss-esapi = "7.1.0"
+tss-esapi = { version = "7.1.0", features = ["generate-bindings"] }
 tss-esapi-sys = "0.3.0"
 foreign-types = "0.3.1"
+zeroize = { version = "1.5.7", features = ["zeroize_derive"] }
+static_assertions = "1.1.0"
 
 [dev-dependencies]
 tempdir = "0.3.7"

+ 148 - 1
crates/btnode/src/crypto/mod.rs

@@ -8,7 +8,7 @@ use openssl::{
     error::ErrorStack,
     encrypt::{Encrypter as OsslEncrypter, Decrypter as OsslDecrypter},
     pkey::{PKey, HasPublic, HasPrivate},
-    symm::{Cipher, encrypt as openssl_encrypt, decrypt as openssl_decrypt},
+    symm::{Cipher, encrypt as openssl_encrypt, decrypt as openssl_decrypt, Crypter, Mode},
     rand::rand_bytes,
     rsa::{Rsa as OsslRsa, Padding as OpensslPadding},
     hash::{hash, MessageDigest},
@@ -19,6 +19,7 @@ use serde::{
     de::{self, DeserializeOwned, Deserializer, SeqAccess, Visitor},
     ser::{Serializer, SerializeStruct},
 };
+use zeroize::ZeroizeOnDrop;
 use std::{
     str::FromStr, num::TryFromIntError, marker::PhantomData,
 };
@@ -44,6 +45,7 @@ pub enum Error {
     BlockNotEncrypted,
     InvalidHashFormat,
     InvalidSignature,
+    IncorrectSize(usize),
     WritecapAuthzErr(WritecapAuthzErr),
     Serde(serde_block_tree::Error),
     Io(std::io::Error),
@@ -66,6 +68,7 @@ impl Display for Error {
             Error::BlockNotEncrypted => write!(f, "block was not encrypted"),
             Error::InvalidHashFormat => write!(f, "invalid format"),
             Error::InvalidSignature => write!(f, "invalid signature"),
+            Error::IncorrectSize(size) => write!(f, "incorrect size: {}", size),
             Error::WritecapAuthzErr(err) => err.fmt(f),
             Error::Serde(err) => err.fmt(f),
             Error::Io(err) => err.fmt(f),
@@ -136,6 +139,15 @@ impl Default for HashKind {
     }
 }
 
+impl HashKind {
+    const fn len(self) -> usize {
+        match self {
+            HashKind::Sha2_256 => 32,
+            HashKind::Sha2_512 => 64,
+        }
+    }
+}
+
 impl From<HashKind> for MessageDigest {
     fn from(kind: HashKind) -> Self {
         match kind {
@@ -247,6 +259,114 @@ impl AsMut<[u8]> for Signature {
     }
 }
 
+#[derive(Serialize, Deserialize)]
+struct TaggedCiphertext<T: Serialize, U: Serialize> {
+    aad: U,
+    ciphertext: Cryptotext<T>,
+    tag: Vec<u8>,
+}
+
+#[derive(EnumDiscriminants, ZeroizeOnDrop)]
+#[strum_discriminants(name(AeadKeyKind))]
+#[strum_discriminants(derive(Serialize, Deserialize))]
+pub enum AeadKey {
+    AesGcm256 {
+        key: [u8; AeadKeyKind::AesGcm256.key_len()],
+        iv: [u8; AeadKeyKind::AesGcm256.iv_len()],
+    }
+}
+
+impl AeadKeyKind {
+    const fn key_len(self) -> usize {
+        match self {
+            AeadKeyKind::AesGcm256 => 32,
+        }
+    }
+
+    const fn iv_len(self) -> usize {
+        match self {
+            AeadKeyKind::AesGcm256 => 16,
+        }
+    }
+}
+
+fn array_from<const N: usize>(slice: &[u8]) -> Result<[u8; N]> {
+    let slice_len = slice.len();
+    if N != slice_len {
+        return Err(Error::IncorrectSize(slice_len))
+    }
+    let mut array = [0u8; N];
+    array.copy_from_slice(slice);
+    Ok(array)
+}
+
+impl AeadKey {
+    fn new(kind: AeadKeyKind) -> Result<AeadKey> {
+        match kind {
+            AeadKeyKind::AesGcm256
+                => Ok(AeadKey::AesGcm256 { key: rand_array()?, iv: rand_array()? }),
+        }
+    }
+
+    fn copy_components(kind: AeadKeyKind, key_buf: &[u8], iv_buf: &[u8]) -> Result<AeadKey> {
+        match kind {
+            AeadKeyKind::AesGcm256 => {
+                Ok(AeadKey::AesGcm256 { key: array_from(key_buf)?, iv: array_from(iv_buf)? })
+            },
+        }
+    }
+
+    fn encrypt<T: Serialize + DeserializeOwned, U: Serialize + DeserializeOwned>(
+        &self, aad: U, plaintext: &T
+    ) -> Result<TaggedCiphertext<T, U>> {
+        let (cipher, key, iv, mut tag) = match self {
+            AeadKey::AesGcm256 { key, iv }
+                => (Cipher::aes_256_gcm(), key.as_slice(), iv.as_slice(), vec![0u8; 16]),
+        };
+
+        let aad_data = to_vec(&aad)?;
+        let plaintext_buf = to_vec(&plaintext)?;
+        let mut ciphertext = vec![0u8; plaintext_buf.len() + cipher.block_size()];
+        let mut crypter = Crypter::new(cipher, Mode::Encrypt, key, Some(iv))?;
+        crypter.aad_update(&aad_data)?;
+        let mut count = crypter.update(&plaintext_buf, &mut ciphertext)?;
+        count += crypter.finalize(&mut ciphertext[count..])?;
+        ciphertext.truncate(count);
+        crypter.get_tag(&mut tag)?;
+        
+        Ok(TaggedCiphertext {
+            aad,
+            ciphertext: Cryptotext::Cipher(ciphertext),
+            tag
+        })
+    }
+
+    fn decrypt<T: Serialize + DeserializeOwned, U: Serialize + DeserializeOwned>(
+        &self, tagged: &TaggedCiphertext<T, U>
+    ) -> Result<T> {
+        let ciphertext = match &tagged.ciphertext {
+            Cryptotext::Plain(_) => return Err(Error::custom("already decrypted")),
+            Cryptotext::Cipher(ciphertext) => ciphertext,
+        };
+
+        let (cipher, key, iv) = match self {
+            AeadKey::AesGcm256 { key, iv }
+                => (Cipher::aes_256_gcm(), key.as_slice(), iv.as_slice()),
+        };
+
+        let mut plaintext = vec![0u8; ciphertext.len() + cipher.block_size()];
+        let mut crypter = Crypter::new(cipher, Mode::Decrypt, key, Some(iv))?;
+        crypter.set_tag(&tagged.tag)?;
+        let aad_buf = to_vec(&tagged.aad)?;
+        crypter.aad_update(&aad_buf)?;
+        let mut count = crypter.update(ciphertext, &mut plaintext)?;
+        count += crypter.finalize(&mut plaintext[count..])?;
+        plaintext.truncate(count);
+
+        Ok(from_vec(&plaintext)?)
+    }
+}
+
 #[derive(Debug, PartialEq, Serialize, Deserialize, Clone, EnumDiscriminants)]
 #[strum_discriminants(name(SymKeyKind))]
 pub(crate) enum SymKey {
@@ -1252,6 +1372,33 @@ mod tests {
         assert_authz_err(WritecapAuthzErr::RootDoesNotOwnPath, result)
     }
 
+    #[test]
+    fn encrypt_decrypt_aes256gcm() {
+        let key = AeadKey::new(AeadKeyKind::AesGcm256).expect("failed to create key");
+        let aad = [0u8; 16];
+        let expected = [0u8; 32];
+        let tagged = key.encrypt(aad, &expected).expect("encrypt failed");
+        let actual = key.decrypt(&tagged).expect("decrypt failed");
+        assert_eq!(expected, actual.as_slice());
+    }
+
+    #[test]
+    fn decrypt_fails_when_ct_modified() {
+        let key = AeadKey::new(AeadKeyKind::AesGcm256).expect("failed to create key");
+        let aad = [0u8; 16];
+        let expected = [0u8; 32];
+        let mut tagged = key.encrypt(aad, &expected).expect("encrypt failed");
+        {
+            let ct = match &mut tagged.ciphertext {
+                Cryptotext::Cipher(ct) => Some(ct),
+                Cryptotext::Plain(_) => None,
+            }.expect("failed to extract cipher text");
+            ct[0] += 1;
+        }
+        let result = key.decrypt(&tagged);
+        assert!(result.is_err())
+    }
+
     /// Tests that validate the dependencies of this module.
     mod dependency_tests {
         use super::*;

+ 333 - 140
crates/btnode/src/crypto/tpm.rs

@@ -10,12 +10,12 @@ use std::{
     path::{Path, PathBuf},
     fs::{OpenOptions},
     sync::{Arc, RwLock, RwLockWriteGuard},
-    mem::size_of,
+    mem::size_of, ops::Deref,
 };
 use openssl::{
     bn::{BigNum, BigNumRef},
     hash::Hasher,
-    nid::Nid,
+    nid::Nid, pkcs5::pbkdf2_hmac,
 };
 use tss_esapi_sys::{
     TSS2_RC,
@@ -35,8 +35,8 @@ use tss_esapi::{
     tcti_ldr::{TctiNameConf, TabrmdConfig},
     interface_types::{
         resource_handles::{Hierarchy, Provision},
-        algorithm::HashingAlgorithm,
-        key_bits::RsaKeyBits,
+        algorithm::{HashingAlgorithm},
+        key_bits::{RsaKeyBits},
         dynamic_handles::Persistent,
         session_handles::{AuthSession, PolicySession},
     },
@@ -55,7 +55,7 @@ use tss_esapi::{
         SignatureScheme,
         RsaDecryptionScheme,
         Data,
-        CapabilityData, EncryptedSecret,
+        CapabilityData, EncryptedSecret, Private,
     },
     attributes::{
         object::ObjectAttributes, SessionAttributesBuilder,
@@ -69,6 +69,7 @@ use tss_esapi::{
     traits::{Marshall, UnMarshall},
 };
 use serde::ser;
+use zeroize::Zeroizing;
 
 impl From<tss_esapi::Error> for Error {
     fn from(err: tss_esapi::Error) -> Self {
@@ -108,6 +109,25 @@ trait ContextExt {
     fn start_default_auth_session(&mut self) -> Result<AuthSession>;
 
     fn start_policy_session(&mut self, is_trial: IsTrial) -> Result<PolicySession>;
+
+    fn dup_with_password_policy(&mut self) -> Result<Digest>;
+
+    fn export_key<S: Scheme>(
+        &mut self,
+        key_pair: &KeyPair<S>,
+        new_parent: KeyHandle,
+        key_auth: Auth,
+        policy_session: PolicySession,
+        encryption_key: &AeadKey,
+    ) -> Result<ExportedKeyPair<S>>;
+
+    fn import_key<S: Scheme>(
+        &mut self,
+        exported: ExportedKeyPair<S>,
+        new_parent: KeyHandle,
+        auth: Auth,
+        encryption_key: &AeadKey,
+    ) -> Result<KeyPair<S>>;
 }
 
 impl ContextExt for Context {
@@ -263,6 +283,79 @@ impl ContextExt for Context {
         })?;
         Ok(obj_handle.into())
     }
+
+    /// Returns the digest for the policy which allows a key to be duplicated provided that the
+    /// caller supplies the correct password (authValue) for the key being duplicated.
+    fn dup_with_password_policy(&mut self) -> Result<Digest> {
+        let policy_session = self.start_policy_session(IsTrial::True)?;
+        self.execute_with_session(None, |ctx| {
+            ctx.policy_password(policy_session)?;
+            ctx.policy_command_code(policy_session, CommandCode::Duplicate)?;
+            Ok(ctx.policy_get_digest(policy_session)?)
+        })
+    }
+
+    fn export_key<S: Scheme>(
+        &mut self,
+        key_pair: &KeyPair<S>,
+        new_parent: KeyHandle,
+        key_auth: Auth,
+        policy_session: PolicySession,
+        encryption_key: &AeadKey,
+    ) -> Result<ExportedKeyPair<S>> {
+        let (public, ..) = self.read_public(key_pair.private)?;
+        let result: Result<()> = self.execute_with_session(None, |ctx| {
+            ctx.policy_password(policy_session)?;
+            ctx.policy_command_code(policy_session, CommandCode::Duplicate)?;
+            Ok(())
+        });
+        result?;
+        let result: Result<(tss_esapi::structures::Private, EncryptedSecret)> = self
+        .execute_with_session(Some(policy_session.into()), |ctx| {
+            let obj_handle = ObjectHandle::from(key_pair.private);
+            ctx.tr_set_auth(obj_handle, key_auth)?;
+            let (_, private, secret) = ctx.duplicate(
+                obj_handle,
+                new_parent.into(),
+                None,
+                SymmetricDefinitionObject::Null,
+            )?;
+            Ok((private, secret))
+        });
+        let (private, secret) = result?;
+        let tagged_ct = encryption_key.encrypt(
+            PublicWrapper(public),
+            &TpmBlobs { private, secret }
+        )?;
+        Ok(ExportedKeyPair { scheme: key_pair.public.scheme, tagged_ct })
+    }
+
+    fn import_key<S: Scheme>(
+        &mut self,
+        exported: ExportedKeyPair<S>,
+        new_parent: KeyHandle,
+        auth: Auth,
+        encryption_key: &AeadKey,
+    ) -> Result<KeyPair<S>> {
+        let blobs = encryption_key.decrypt(&exported.tagged_ct)?;
+        let public = exported.tagged_ct.aad;
+        let private = self.import(
+            new_parent.into(),
+            None,
+            public.clone(),
+            blobs.private,
+            blobs.secret,
+            SymmetricDefinitionObject::Null,
+        )?;
+        let key_handle = self.load(
+            new_parent,
+            private,
+            public.clone(),
+        )?;
+        self.tr_set_auth(key_handle.into(), auth)?;
+        let public = AsymKeyPub::try_from(public.0, exported.scheme)?;
+        Ok(KeyPair { public, private: key_handle })
+    }
 }
 
 enum IsTrial {
@@ -323,30 +416,74 @@ impl Cookie {
     }
 }
 
+#[derive(Serialize, Deserialize)]
+struct PublicWrapper(
+    #[serde(serialize_with = "serialize_marshall")]
+    #[serde(deserialize_with = "deserialize_unmarshall")]
+    Public
+);
+
+impl Deref for PublicWrapper {
+    type Target = Public;
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+fn serialize_marshall<S: Serializer, T: Marshall>(
+    value: &T, ser: S
+) -> std::result::Result<S::Ok, S::Error> {
+    let vec = value.marshall().map_err(ser::Error::custom)?;
+    vec.as_slice().serialize(ser)
+}
+
+fn deserialize_unmarshall<'de, D: Deserializer<'de>, T: UnMarshall>(
+    de: D
+) -> std::result::Result<T, D::Error> {
+    let vec: Vec<u8> = Deserialize::deserialize(de)?;
+    T::unmarshall(&vec).map_err(de::Error::custom)
+}
+
+#[derive(Serialize, Deserialize)]
+struct TpmBlobs {
+    #[serde(serialize_with = "serialize_as_vec")]
+    #[serde(deserialize_with = "deserialize_as_vec")]
+    private: Private,
+    #[serde(serialize_with = "serialize_as_vec")]
+    #[serde(deserialize_with = "deserialize_as_vec")]
+    secret: EncryptedSecret,
+}
+
+fn serialize_as_vec<S: Serializer, T: Deref<Target=Vec<u8>>>(
+    value: &T, ser: S
+) -> std::result::Result<S::Ok, S::Error> {
+    value.deref().serialize(ser)
+}
+
+fn deserialize_as_vec<'de, D: Deserializer<'de>, T: TryFrom<Vec<u8>>>(
+    de: D
+) -> std::result::Result<T, D::Error> {
+    T::try_from(Vec::deserialize(de)?)
+        .map_err(|_| de::Error::custom("Unable to convert from vector"))
+}
+
 pub struct ExportedKeyPair<S: Scheme> {
     scheme: S,
-    public: Public,
-    private: tss_esapi::structures::Private,
-    secret: EncryptedSecret, 
+    tagged_ct: TaggedCiphertext<TpmBlobs, PublicWrapper>,
 }
 
 impl<S: Scheme> ExportedKeyPair<S> {
     const FIELDS: &'static [&'static str] = &[
         "scheme",
-        "public",
-        "private",
-        "secret",
+        "tagged_ct",
     ];
 }
 
 impl<S: Scheme> Serialize for ExportedKeyPair<S> {
     fn serialize<T: Serializer>(&self, ser: T) -> std::result::Result<T::Ok, T::Error> {
-        let vec = self.public.marshall().map_err(ser::Error::custom)?;
         let mut struct_ser = ser.serialize_struct(stringify!(ExportedKey), Self::FIELDS.len())?;
         struct_ser.serialize_field(Self::FIELDS[0], &self.scheme)?;
-        struct_ser.serialize_field(Self::FIELDS[1], &vec)?;
-        struct_ser.serialize_field(Self::FIELDS[2], self.private.value())?;
-        struct_ser.serialize_field(Self::FIELDS[3], self.secret.value())?;
+        struct_ser.serialize_field(Self::FIELDS[1], &self.tagged_ct)?;
         struct_ser.end()
     }
 }
@@ -373,19 +510,9 @@ impl<'de, S: Scheme> Deserialize<'de> for ExportedKeyPair<S> {
 
                 let scheme: S = seq.next_element()?
                     .ok_or_else(|| de::Error::missing_field(ExportedKeyPair::<S>::FIELDS[0]))?;
-                let public: Box<[u8]> = seq.next_element()?
+                let tagged_ct: TaggedCiphertext<TpmBlobs, PublicWrapper> = seq.next_element()?
                     .ok_or_else(|| de::Error::missing_field(ExportedKeyPair::<S>::FIELDS[1]))?;
-                let public = conv_err(Public::unmarshall(public.as_ref()))?;
-                let private: Box<[u8]> = seq.next_element()?
-                    .ok_or_else(|| de::Error::missing_field(ExportedKeyPair::<S>::FIELDS[2]))?;
-                let secret: Box<[u8]> = seq.next_element()?
-                    .ok_or_else(|| de::Error::missing_field(ExportedKeyPair::<S>::FIELDS[3]))?;
-                Ok(ExportedKeyPair {
-                    scheme,
-                    public,
-                    private: conv_err(private.as_ref().try_into())?,
-                    secret: conv_err(secret.as_ref().try_into())?,
-                })
+                Ok(ExportedKeyPair { scheme, tagged_ct })
             }
         }
 
@@ -397,6 +524,7 @@ impl<'de, S: Scheme> Deserialize<'de> for ExportedKeyPair<S> {
 pub struct ExportedCreds {
     sign: ExportedKeyPair<Sign>,
     enc: ExportedKeyPair<Encrypt>,
+    params: DerivationParams,
 }
 
 /// A public/private key pair in a form which can be serialized and deserialized.
@@ -581,13 +709,6 @@ impl<'a, S: Scheme> KeyBuilder<'a, S> {
         }
     }
 
-    fn for_storage_key(scheme: S, unique: &'a [u8]) -> KeyBuilder<'a, S> {
-        KeyBuilder::new(scheme, unique)
-            .with_allow_dup(false)
-            .with_restricted(true)
-            .with_symmetric(SymmetricDefinitionObject::AES_256_CFB)
-    }
-
     fn with_allow_dup(mut self, allow_dup: bool) -> Self {
         self.allow_dup = allow_dup;
         self
@@ -707,6 +828,15 @@ impl<'a, S: Scheme> KeyBuilder<'a, S> {
     }
 }
 
+impl<'a> KeyBuilder<'a, Encrypt> {
+    fn for_storage_key(scheme: Encrypt, unique: &'a [u8]) -> KeyBuilder<'a, Encrypt> {
+        KeyBuilder::new(scheme, unique)
+            .with_allow_dup(false)
+            .with_restricted(true)
+            .with_symmetric(SymmetricDefinitionObject::AES_128_CFB)
+    }
+}
+
 /// A public/private key pair.
 #[derive(Clone)]
 struct KeyPair<S: Scheme> {
@@ -892,18 +1022,6 @@ impl TpmCredStore {
         Ok(creds)
     }
 
-    /// Returns the digest for the policy which allows a key to be duplicated provided that the
-    /// caller supplies the correct password (authValue) for the key being duplicated.
-    fn dup_with_password_policy(&self) -> Result<Digest> {
-        let mut guard = self.state.write()?;
-        let policy_session = guard.context.start_policy_session(IsTrial::True)?;
-        guard.context.execute_with_session(None, |ctx| {
-            ctx.policy_password(policy_session)?;
-            ctx.policy_command_code(policy_session, CommandCode::Duplicate)?;
-            Ok(ctx.policy_get_digest(policy_session)?)
-        })
-    }
-
     fn gen_root_sign_key(&self, password: &str, policy: Digest) -> Result<KeyPair<Sign>> {
         let scheme = Self::SIGN_SCHEME;
         let unique = rand_vec(scheme.key_len() as usize)?;
@@ -929,6 +1047,46 @@ impl TpmCredStore {
     }
 }
 
+#[derive(Serialize, Deserialize)]
+struct DerivationParams {
+    iter: usize,
+    hash: HashKind,
+    kind: AeadKeyKind,
+    salt: Vec<u8>,
+    iv: Vec<u8>,
+}
+
+impl DerivationParams {
+    const PBKDF2_ITER: usize = 1000000;
+    const PBKDF2_HASH: HashKind = HashKind::Sha2_256;
+    const EXPORT_KEY_KIND: AeadKeyKind = AeadKeyKind::AesGcm256;
+
+    fn new() -> Result<DerivationParams> {
+        const_assert!(
+            DerivationParams::PBKDF2_HASH.len() == DerivationParams::EXPORT_KEY_KIND.key_len()
+        );
+        Ok(DerivationParams {
+            iter: Self::PBKDF2_ITER,
+            hash: Self::PBKDF2_HASH,
+            kind: Self::EXPORT_KEY_KIND,
+            salt: rand_vec(Self::PBKDF2_HASH.len())?,
+            iv: rand_vec(Self::EXPORT_KEY_KIND.iv_len())?,
+        })
+    }
+
+    fn derive_key(&self, password: &str) -> Result<AeadKey> {
+        let mut key = Zeroizing::new([0u8; Self::EXPORT_KEY_KIND.key_len()]);
+        pbkdf2_hmac(
+            password.as_bytes(),
+            self.salt.as_slice(),
+            self.iter,
+            self.hash.into(),
+            key.as_mut_slice()
+        )?;
+        AeadKey::copy_components(self.kind, key.as_slice(), &self.iv)
+    }
+}
+
 impl CredStore for TpmCredStore {
     type CredHandle = TpmCreds;
     type ExportedCreds = ExportedCreds;
@@ -965,7 +1123,10 @@ impl CredStore for TpmCredStore {
                 return Err(Error::custom("root creds have already been generated"))
             }
         }
-        let policy = self.dup_with_password_policy()?;
+        let policy = {
+            let mut guard = self.state.write()?;
+            guard.context.dup_with_password_policy()?
+        };
         let sign = self.gen_root_sign_key(password, policy.clone())?;
         let enc = self.gen_root_enc_key(password, policy)?;
         let cred_data = CredData { sign, enc };
@@ -982,38 +1143,8 @@ impl CredStore for TpmCredStore {
     fn export_root_creds(
         &self, root_creds: &TpmCreds, password: &str, new_parent: &AsymKeyPub<Encrypt>
     ) -> Result<ExportedCreds> {
-        fn export_key<S: Scheme>(
-            context: &mut Context,
-            key_pair: &KeyPair<S>,
-            new_parent: KeyHandle,
-            key_auth: Auth,
-            policy_session: PolicySession,
-        ) -> Result<ExportedKeyPair<S>> {
-            let result: Result<()> = context.execute_with_session(None, |ctx| {
-                ctx.policy_password(policy_session)?;
-                ctx.policy_command_code(policy_session, CommandCode::Duplicate)?;
-                Ok(())
-            });
-            result?;
-            let (public, ..) = context.read_public(key_pair.private)?;
-            context.execute_with_session(Some(policy_session.into()), |ctx| {
-                let obj_handle = ObjectHandle::from(key_pair.private);
-                ctx.tr_set_auth(obj_handle, key_auth)?;
-                let (_, private, secret) = ctx.duplicate(
-                    obj_handle,
-                    new_parent.into(),
-                    None,
-                    SymmetricDefinitionObject::Null,
-                )?;
-                Ok(ExportedKeyPair {
-                    scheme: key_pair.public.scheme,
-                    public,
-                    private,
-                    secret,
-                })
-            })
-        }
-
+        let params = DerivationParams::new()?;
+        let aead_key = params.derive_key(password)?;
         let new_parent = new_parent.storage_key_public()?;
         let mut guard = self.state.write()?;
         if let Some(storage_key) = guard.storage_key.take() {
@@ -1023,63 +1154,40 @@ impl CredStore for TpmCredStore {
         let new_parent_handle = guard.context.load_external_public(new_parent, Hierarchy::Null)?;
         let policy_session = guard.context.start_policy_session(IsTrial::False)?;
         let auth = Auth::try_from(password.as_bytes())?;
-        let sign = export_key(
-            &mut guard.context,
+        let sign = guard.context.export_key(
             &root_creds.sign,
             new_parent_handle,
             auth.clone(),
             policy_session,
+            &aead_key,
         )?;
-        let enc = export_key(
-            &mut guard.context,
+        let enc = guard.context.export_key(
             &root_creds.enc,
             new_parent_handle,
             auth,
             policy_session,
+            &aead_key,
         )?;
-        Ok(ExportedCreds { sign, enc })
+        Ok(ExportedCreds { sign, enc, params })
     }
 
     fn import_root_creds(&self, password: &str, exported: ExportedCreds) -> Result<TpmCreds> {
-        fn import_key<S: Scheme>(
-            context: &mut Context,
-            exported: ExportedKeyPair<S>,
-            new_parent: KeyHandle,
-            auth: Auth,
-        ) -> Result<KeyPair<S>> {
-            let private = context.import(
-                new_parent.into(),
-                None,
-                exported.public.clone(),
-                exported.private,
-                exported.secret,
-                SymmetricDefinitionObject::Null,
-            )?;
-            let key_handle = context.load(
-                new_parent,
-                private,
-                exported.public.clone(),
-            )?;
-            context.tr_set_auth(key_handle.into(), auth)?;
-            let public = AsymKeyPub::try_from(exported.public, exported.scheme)?;
-            Ok(KeyPair { public, private: key_handle })
-        }
-
+        let aead_key = exported.params.derive_key(password)?;
+        let auth = Auth::try_from(password.as_bytes())?;
         let storage_key = self.get_or_init_storage_key()?;
         let creds = {
             let mut guard = self.state.write()?;
-            let auth = Auth::try_from(password.as_bytes())?;
-            let sign = import_key(
-                &mut guard.context,
+            let sign = guard.context.import_key(
                 exported.sign,
                 storage_key.private,
-                auth.clone()
+                auth.clone(),
+                &aead_key,
             )?;
-            let enc = import_key(
-                &mut guard.context,
+            let enc = guard.context.import_key(
                 exported.enc,
                 storage_key.private,
                 auth,
+                &aead_key,
             )?;
             let cred_data = CredData { sign, enc, };
             TpmCreds::new(cred_data, &self.state)
@@ -1103,10 +1211,12 @@ impl<S: Scheme> AsymKeyPub<S> {
             _ => Err(Error::custom("Unsupported key type returned by TPM")),
         }
     }
+}
 
+impl AsymKeyPub<Encrypt> {
     fn storage_key_public(&self) -> Result<Public> {
-        fn from_rsa<S: Scheme>(
-            scheme: S, rsa: openssl::rsa::Rsa<crypto::Public>
+        fn from_rsa(
+            scheme: Encrypt, rsa: openssl::rsa::Rsa<crypto::Public>
         ) -> Result<Public> {
             let exponent = rsa.e().try_into_u32()?;
             let modulus = rsa.n().to_vec();
@@ -1114,14 +1224,9 @@ impl<S: Scheme> AsymKeyPub<S> {
                 .with_rsa_exponent(exponent);
             builder.rsa_template()
         }
-        match self.scheme.as_enum() {
-            SchemeKind::Encrypt(inner) => match inner {
-                Encrypt::RsaEsOaep(scheme) => from_rsa(scheme, self.pkey.rsa()?),
-            }
-            SchemeKind::Sign(inner) => match inner {
-                Sign::RsaSsaPss(scheme) => from_rsa(scheme, self.pkey.rsa()?),
-            }
-        } 
+        match self.scheme {
+            Encrypt::RsaEsOaep(_) => from_rsa(self.scheme, self.pkey.rsa()?),
+        }
     }
 }
 
@@ -1482,9 +1587,9 @@ active_pcr_banks = sha256
     }
 
     /// Displays the message associated with a TSS2 return code.
-    //#[test]
+    #[test]
     fn print_error_message() {
-        const RC: TSS2_RC = 0x00000982;
+        const RC: TSS2_RC = 2461;
         let msg = tss2_rc_decode(RC);
         println!("{}", msg);
     }
@@ -1738,27 +1843,115 @@ active_pcr_banks = sha256
     }
 
     #[test]
-    fn root_key_export_import() -> Result<()> {
+    fn root_key_export_import() {
         const PASSWORD: &str = "Frobinate";
-        let (_src_harness, src_store) = test_store()?;
-        let src_root_creds = src_store.gen_root_creds(PASSWORD)?;
+        let (_src_harness, src_store) = test_store().unwrap();
+        let src_root_creds = src_store.gen_root_creds(PASSWORD).unwrap();
 
-        let (_dest_harness, dest_store) = test_store()?;
-        let dest_storage_key = dest_store.storage_key()?;
+        let (_dest_harness, dest_store) = test_store().unwrap();
+        let dest_storage_key = dest_store.storage_key().unwrap();
 
-        let exported = src_store.export_root_creds(&src_root_creds, PASSWORD, &dest_storage_key)?;
-        let vec = to_vec(&exported)?;
+        let exported = src_store.export_root_creds(&src_root_creds, PASSWORD, &dest_storage_key)
+            .unwrap();
+        let vec = to_vec(&exported).unwrap();
         let mut slice = vec.as_slice();
-        let exported = read_from(&mut slice)?;
-        let dest_root_creds = dest_store.import_root_creds(PASSWORD, exported)?;
+        let exported = read_from(&mut slice).unwrap();
+        let dest_root_creds = dest_store.import_root_creds(PASSWORD, exported).unwrap();
 
-        let message = rand_vec(TpmCredStore::ENCRYPT_SCHEME.key_len() as usize / 2)?;
-        let sig = dest_root_creds.sign([message.as_slice()].into_iter())?;
-        src_root_creds.verify([message.as_slice()].into_iter(), sig.as_slice())?;
+        let message = rand_vec(TpmCredStore::ENCRYPT_SCHEME.key_len() as usize / 2).unwrap();
+        let sig = dest_root_creds.sign([message.as_slice()].into_iter()).unwrap();
+        src_root_creds.verify([message.as_slice()].into_iter(), sig.as_slice()).unwrap();
 
-        let ct = src_root_creds.encrypt(message.as_slice())?;
-        let pt = dest_root_creds.decrypt(ct.as_slice())?;
+        let ct = src_root_creds.encrypt(message.as_slice()).unwrap();
+        let pt = dest_root_creds.decrypt(ct.as_slice()).unwrap();
         assert_eq!(message.as_slice(), pt.as_slice());
+    }
+
+    /// This test is broken for unknown reasons. It passes if no inner encryption key is supplied.
+    /// To work-around this issue I've chosen to wrap the data returned by `duplicate` with a
+    /// symmetric key in software.
+    //#[test]
+    fn key_export_import() -> Result<()> {
+        let auth = Auth::try_from(vec![0u8; 32])?;
+
+        let src_harness = SwtpmHarness::new()?;
+        let mut src_ctx = src_harness.context()?;
+        {
+            let session = src_ctx.start_default_auth_session()?;
+            src_ctx.set_sessions((Some(session), None, None));
+        }
+
+        let src_storage = {
+            let unique = [0u8; 0];
+            KeyBuilder::for_storage_key(Encrypt::RSA_OAEP_2048_SHA_256, unique.as_slice())
+                .with_parent(Parent::Seed(Hierarchy::Owner))
+                .with_auth(auth.clone())
+                .build(&mut src_ctx)?
+                .private
+        };
+
+        let dest_harness = SwtpmHarness::new()?;
+        let mut dest_ctx = dest_harness.context()?;
+        {
+            let session = dest_ctx.start_default_auth_session()?;
+            dest_ctx.set_sessions((Some(session), None, None));
+        }
+
+        let dest_storage = {
+            let unique = [0u8; 0];
+            KeyBuilder::for_storage_key(Encrypt::RSA_OAEP_2048_SHA_256, unique.as_slice())
+                .with_parent(Parent::Seed(Hierarchy::Owner))
+                .with_auth(auth.clone())
+                .build(&mut dest_ctx)?
+                .private
+        };
+
+        let key_handle = {
+            let policy = src_ctx.dup_with_password_policy()?;
+            let unique = rand_array::<256>()?;
+            KeyBuilder::new(Sign::RSA_PSS_2048_SHA_256, unique.as_slice())
+                .with_parent(Parent::Key(src_storage))
+                .with_allow_dup(true)
+                .with_policy_digest(policy)
+                .build(&mut src_ctx)?
+                .private
+        };
+
+        let new_parent = {
+            let (public, ..) = dest_ctx.read_public(dest_storage)?;
+            src_ctx.load_external_public(public, Hierarchy::Owner)?
+        };
+
+        let (public, ..) = src_ctx.read_public(key_handle)?;
+
+        let encryption_key = Data::try_from(vec![7u8; 16])?;
+
+        let (_, private, secret) = {
+            let session = src_ctx.start_policy_session(IsTrial::False)?;
+            let result: Result<()> = src_ctx.execute_with_session(None, |ctx| {
+                ctx.policy_password(session)?;
+                ctx.policy_command_code(session, CommandCode::Duplicate)?;
+                Ok(())
+            });
+            result?;
+            src_ctx.execute_with_session(Some(session.into()), |ctx| {
+                ctx.duplicate(
+                    key_handle.into(),
+                    new_parent.into(),
+                    Some(encryption_key.clone()),
+                    SymmetricDefinitionObject::AES_128_CFB,
+                )
+            }).unwrap()
+        };
+
+        dest_ctx.import(
+            dest_storage.into(),
+            Some(encryption_key),
+            public,
+            private,
+            secret,
+            SymmetricDefinitionObject::AES_128_CFB,
+        ).unwrap();
         Ok(())
     }
 }

+ 3 - 0
crates/btnode/src/main.rs

@@ -8,6 +8,9 @@ mod test_helpers;
 #[cfg(test)]
 mod serde_tests;
 
+#[macro_use]
+extern crate static_assertions;
+
 use serde_block_tree::{self, read_from, write_to};
 use harness::Message;
 mod crypto;