Browse Source

Added code to TpmCredStore for persisting credentials.

Matthew Carr 2 years ago
parent
commit
d50e2f4fa2

+ 2 - 2
crates/btnode/Cargo.lock

@@ -678,9 +678,9 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
 [[package]]
 name = "zeroize"
-version = "1.5.6"
+version = "1.5.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "20b578acffd8516a6c3f2a1bdefc1ec37e547bb4e0fb8b6b01a4cafc886b4442"
+checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f"
 dependencies = [
  "zeroize_derive",
 ]

+ 1 - 0
crates/btnode/scripts/swtpm.sh

@@ -54,6 +54,7 @@ stop() {
 
 restart() {
     stop
+    sleep 0.2
     start
 }
 

+ 4 - 4
crates/btnode/src/crypto/mod.rs

@@ -582,13 +582,13 @@ pub(crate) trait CredStore {
 
     /// Returns the node credentials. If credentials haven't been generated, they are generated
     /// stored and returned.
-    fn node_creds(&self) -> Result<&Self::CredHandle>;
+    fn node_creds(&self) -> Result<Self::CredHandle>;
     /// Returns the root credentials. If no root credentials have been generated, or the provided
     /// password is incorrect, then an error is returned.
-    fn root_creds<'a>(&'a self, password: &str) -> Result<&'a Self::CredHandle>;
-    /// Generates the root crednetials and protects them using the given password. If the root
+    fn root_creds(&self, password: &str) -> Result<Self::CredHandle>;
+    /// Generates the root credentials and protects them using the given password. If the root
     /// credentials have already been generated then an error is returned.
-    fn gen_root_creds<'a>(&'a self, password: &str) -> Result<&'a Self::CredHandle>;
+    fn gen_root_creds(&self, password: &str) -> Result<Self::CredHandle>;
 }
 
 pub(crate) fn encrypt_block<C: Creds>(

+ 641 - 136
crates/btnode/src/crypto/tpm.rs

@@ -1,16 +1,16 @@
 use super::*;
 
 use std::{
-    io::{Read, Write},
+    io::{Read, Write, BufReader},
     os::{
         raw::c_char,
         unix::fs::PermissionsExt,
     },
     ffi::CStr,
-    path::Path,
-    fs::{File, OpenOptions},
-    sync::{Arc, Mutex},
-    mem::size_of,
+    path::{Path, PathBuf},
+    fs::{OpenOptions},
+    sync::{Arc, RwLock, RwLockWriteGuard},
+    mem::size_of
 };
 use openssl::{
     bn::BigNum,
@@ -34,12 +34,14 @@ use tss_esapi::{
     },
     tcti_ldr::{TctiNameConf, TabrmdConfig},
     interface_types::{
-        resource_handles::Hierarchy,
+        resource_handles::{Hierarchy, Provision},
         algorithm::HashingAlgorithm,
         key_bits::RsaKeyBits,
         dynamic_handles::Persistent,
+        session_handles::AuthSession,
     },
     structures::{
+        Auth,
         Digest,
         HashScheme,
         Public,
@@ -57,9 +59,9 @@ use tss_esapi::{
         CapabilityData,
     },
     attributes::{
-        object::ObjectAttributes,
+        object::ObjectAttributes, SessionAttributesBuilder,
     },
-    handles::{KeyHandle, PersistentTpmHandle},
+    handles::{KeyHandle, PersistentTpmHandle, TpmHandle},
 };
 
 impl From<tss_esapi::Error> for Error {
@@ -95,11 +97,19 @@ trait ContextExt {
             .conv_err()?;
         Ok(Persistent::Persistent(handle))
     }
+
+    fn persist_key(&mut self, key_handle: KeyHandle) -> Result<TPM2_HANDLE>;
+
+    fn evict_key(&mut self, tpm_handle: TPM2_HANDLE, key_handle: Option<KeyHandle>) -> Result<()>;
+
+    fn key_handle(&mut self, tpm_handle: TpmHandle) -> Result<KeyHandle>;
+
+    fn start_default_auth_session(&mut self) -> Result<AuthSession>;
 }
 
 impl ContextExt for Context {
     fn persistent_handles(&mut self) -> Result<Vec<PersistentTpmHandle>> {
-        let capability= CapabilityType::Handles;
+        let capability = CapabilityType::Handles;
         let property = TPM2_PERSISTENT_FIRST;
         let max = usize::try_from(TPM2_MAX_CAP_BUFFER).unwrap();
         let count = (max - size_of::<TPM2_CAP>() - size_of::<u32>()) / size_of::<TPM2_HANDLE>();
@@ -107,11 +117,13 @@ impl ContextExt for Context {
         let mut all_handles = Vec::new();
         loop {
             let (handles, more) = self.get_capability(capability, property, property_count)
-                .unwrap();
+                .conv_err()?;
             let list = match handles {
                 CapabilityData::Handles(list) => list.into_inner(),
-                _ => panic!("Unexpected capability type returned by TPM: {:?}", handles),
+                _ => return Err(Error::custom(
+                    format!("Unexpected capability type returned by TPM: {:?}", handles))),
             };
+            all_handles.reserve(list.len());
             for handle in list {
                 all_handles.push(PersistentTpmHandle::try_from(handle).conv_err()?);
             }
@@ -119,13 +131,79 @@ impl ContextExt for Context {
                 break;
             }
         }
+        all_handles.sort_unstable_by_key(|handle| TPM2_HANDLE::from(*handle));
         Ok(all_handles)
     }
+
+    fn persist_key(&mut self, key_handle: KeyHandle) -> Result<TPM2_HANDLE> {
+        let persistent = self.execute_with_session(None, |ctx| ctx.first_free_persistent())?;
+        self.evict_control(Provision::Owner, key_handle.into(), persistent).conv_err()?;
+        let Persistent::Persistent(inner) = persistent;
+        Ok(TPM2_HANDLE::from(inner))
+    }
+    
+    fn evict_key(&mut self, persistent: TPM2_HANDLE, key_handle: Option<KeyHandle>) -> Result<()> {
+        let tpm_handle = TpmHandle::try_from(persistent).conv_err()?;
+        let key_handle = match key_handle {
+            Some(key_handle) => key_handle,
+            None => self.tr_from_tpm_public(tpm_handle)?.into()
+        };
+        let persistent = Persistent::Persistent(
+            PersistentTpmHandle::try_from(tpm_handle).conv_err()?);
+        self.evict_control(Provision::Owner, key_handle.into(), persistent)?;
+        Ok(())
+    }
+
+    fn start_default_auth_session(&mut self) -> Result<AuthSession> {
+        let session = self.start_auth_session(
+            None,
+            None,
+            None,
+            SessionType::Hmac,
+            SymmetricDefinition::AES_256_CFB,
+            HashingAlgorithm::Sha256,
+        )
+        .conv_err()?
+        .ok_or_else(|| Error::custom("empty session handle received from TPM"))?;
+
+        let (attributes, mask) = SessionAttributesBuilder::new()
+            .with_decrypt(true)
+            .with_encrypt(true)
+            .build();
+        self.tr_sess_set_attributes(session, attributes, mask).conv_err()?;
+
+        Ok(session)
+    }
+
+    /// Loads the public information from the persistent object stored in the TPM referred to by
+    /// `tpm_handle` and returns the `KeyHandle` for this object. If the TPM handle refers to an
+    /// object which is not actually a key, then an error is returned.
+    fn key_handle(&mut self, tpm_handle: TpmHandle) -> Result<KeyHandle> {
+        let obj_handle = self.execute_with_session(None, |ctx| {
+            ctx.tr_from_tpm_public(tpm_handle).conv_err()
+        })?;
+        Ok(obj_handle.into())
+    }
+}
+
+trait DigestExt {
+    fn empty() -> Digest;
+}
+
+impl DigestExt for Digest {
+    fn empty() -> Digest {
+        let empty = [0u8; 0];
+        Digest::try_from(empty.as_slice()).unwrap()
+    }
 }
 
 const COOKIE_LEN: usize = RSA_KEY_BYTES;
 
-struct Cookie([u8; COOKIE_LEN]);
+#[derive(Serialize, Deserialize, Clone)]
+struct Cookie(
+    #[serde(with = "BigArray")]
+    [u8; COOKIE_LEN]
+);
 
 impl Cookie {
     fn random() -> Result<Cookie> {
@@ -136,25 +214,6 @@ impl Cookie {
         Cookie([0; COOKIE_LEN])
     }
 
-    fn load_or_init<P: AsRef<Path>>(cookie_path: P) -> Result<Cookie> {
-        let cookie = match OpenOptions::new().read(true).open(&cookie_path) {
-            Ok(mut file) => {
-                Cookie::load(&mut file)?
-            },
-            Err(error) => {
-                if std::io::ErrorKind::NotFound != error.kind() {
-                    return Err(Error::from(error));
-                }
-                let cookie = Cookie::random()?;
-                let mut file = OpenOptions::new().write(true).create_new(true).open(&cookie_path)
-                    .conv_err()?;
-                cookie.save(&mut file)?; 
-                cookie
-            }
-        };
-        Ok(cookie)
-    }
-
     fn as_slice(&self) -> &[u8] {
         self.0.as_slice()
     }
@@ -163,90 +222,303 @@ impl Cookie {
         self.0.as_mut_slice()
     }
 
-    fn save(&self, to: &mut File) -> Result<()> {
-        to.write_all(self.as_slice()).conv_err()?;
-        // Only allow read access from the current user.
-        let metadata = to.metadata().conv_err()?;
+    /// Returns the `Auth` value associated with this cookie.
+    fn auth(&self) -> Auth {
+        // This shouldn't fail because the given slice is only 64 bytes long.
+        Auth::try_from(&self.as_slice()[..64]).unwrap()
+    }
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+struct StoredKeyPair {
+    public: AsymKeyPub,
+    private: TPM2_HANDLE,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+struct TpmHandles {
+    sign: StoredKeyPair,
+    enc: StoredKeyPair,
+}
+
+impl TpmHandles {
+    fn new(sign: StoredKeyPair, enc: StoredKeyPair) -> TpmHandles {
+        TpmHandles { sign, enc }
+    }
+
+    fn to_key_handles(&self, context: &mut Context) -> Result<KeyHandles> {
+        let sign = SignKey(KeyPair::from_stored(context, &self.sign)?);
+        let enc = EncKey(KeyPair::from_stored(context, &self.enc)?);
+        Ok(KeyHandles {sign, enc})
+    }
+}
+
+
+#[derive(Serialize, Deserialize)]
+struct Storage {
+    cookie: Cookie,
+    node: Option<TpmHandles>,
+    root: Option<TpmHandles>,
+}
+
+impl Storage {
+    fn new(cookie: Cookie) -> Storage {
+        Storage {
+            cookie,
+            node: None,
+            root: None,
+        }
+    }
+
+    fn save<W: Write>(&self, to: &mut W) -> Result<()> {
+        write_to(self, to).conv_err()
+    }
+
+    fn load<R: Read>(from: &mut R) -> Result<Storage> {
+        read_from(from).conv_err()
+    }
+
+    fn init<P: AsRef<Path>>(&self, path: P) -> Result<()> {
+        let file = OpenOptions::new().write(true).create_new(true).open(&path)
+            .conv_err()?;
+        // Only allow access from the current user.
+        let metadata = file.metadata().conv_err()?;
         let mut permissions = metadata.permissions();
-        permissions.set_mode(0o400);
-        to.set_permissions(permissions).conv_err()?;
+        permissions.set_mode(0o600);
+        file.set_permissions(permissions).conv_err()?;
+        let mut reader = BufWriter::new(file);
+        self.save(&mut reader)?; 
+        reader.flush().conv_err()?;
         Ok(())
     }
 
-    fn load(from: &mut File) -> Result<Cookie> {
-        let mut cookie = Cookie::empty();
-        from.read_exact(cookie.as_mut_slice()).conv_err()?;
-        Ok(cookie)
+    fn load_or_init<P: AsRef<Path>>(path: P) -> Result<Storage> {
+        match OpenOptions::new().read(true).open(&path) {
+            Ok(file) => {
+                let mut reader = BufReader::new(file);
+                Self::load(&mut reader)
+            },
+            Err(error) => {
+                if std::io::ErrorKind::NotFound != error.kind() {
+                    return Err(error.into());
+                }
+                let state = Self::new(Cookie::random()?);
+                state.init(path)?;
+                Ok(state)
+            }
+        }
     }
 }
 
-pub(crate) struct TpmCredStore {
-    context: Arc<Mutex<Context>>,
-    cookie: Cookie,
+#[derive(Clone, Copy)]
+enum KeyKind {
+    Sign,
+    Decrypt,
 }
 
-impl TpmCredStore {
+impl KeyKind {
+    fn sign(self) -> bool {
+        match self {
+            KeyKind::Sign => true,
+            KeyKind::Decrypt => false,
+        }
+    }
+
+    fn decrypt(self) -> bool {
+        match self {
+            KeyKind::Sign => false,
+            KeyKind::Decrypt => true,
+        }
+    }
+}
+
+struct KeyParams<'a> {
+    kind: KeyKind,
+    allow_dup: bool,
+    scheme: RsaScheme,
+    unique: &'a [u8],
+    auth: Option<Auth>,
+}
+
+impl<'a> KeyParams<'a> {
     /// The public exponent to use for generated RSA keys.
     const RSA_EXPONENT: u32 = 65537; // 2**16 + 1
 
     const RSA_KEY_BITS: RsaKeyBits = RsaKeyBits::Rsa3072;
 
-    pub(crate) fn new<P: AsRef<Path>>(
-        mut context: Context, cookie_path: P
-    ) -> Result<TpmCredStore> {
-        let cookie = Cookie::load_or_init(cookie_path)?;
-        let session = context.start_auth_session(
-            None,
-            None,
-            None,
-            SessionType::Hmac,
-            SymmetricDefinition::AES_256_CFB,
-            HashingAlgorithm::Sha256,
-        )
-        .conv_err()?
-        .ok_or_else(|| Error::custom("Received invalid session handle"))?;
-        context.set_sessions((Some(session), None, None));
-        let context = Arc::new(Mutex::new(context));
-        Ok(TpmCredStore { context, cookie })
+    fn with_unique(unique: &'a [u8]) -> KeyParams<'a> {
+        KeyParams {
+            kind: KeyKind::Sign,
+            allow_dup: false,
+            scheme: RsaScheme::Null,
+            unique,
+            auth: None,
+        }
     }
 
-    fn gen_node_creds(&self) -> Result<TpmCreds> {
-        let template = {
-            let object_attributes = ObjectAttributes::builder()
-                .with_fixed_tpm(true)
-                .with_fixed_parent(true)
-                .with_sensitive_data_origin(true)
-                .with_user_with_auth(true)
-                .with_decrypt(true)
-                .with_sign_encrypt(true)
-                .with_restricted(false)
-                .build()
-                .conv_err()?;
-            let name_hashing_algorithm = HashingAlgorithm::Sha256;
-            let empty = [0u8; 0];
-            let auth_policy = Digest::try_from(empty.as_slice()).conv_err()?;
-            let parameters = PublicRsaParameters::new(
-                SymmetricDefinitionObject::Null,
-                RsaScheme::Null,
-                TpmCredStore::RSA_KEY_BITS,
-                RsaExponent::try_from(TpmCredStore::RSA_EXPONENT).conv_err()?, 
-            );
-            let unique = PublicKeyRsa::try_from(self.cookie.as_slice())
-                .conv_err()?;
-            Public::Rsa {
-                object_attributes,
-                name_hashing_algorithm,
-                auth_policy,
-                parameters,
-                unique,
-            }
+    fn with_kind(mut self, kind: KeyKind) -> Self {
+        self.kind = kind;
+        self
+    }
+
+    fn with_allow_dup(mut self, allow_dup: bool) -> Self {
+        self.allow_dup = allow_dup;
+        self
+    }
+
+    fn with_scheme(mut self, scheme: RsaScheme) -> Self {
+        self.scheme = scheme;
+        self
+    }
+
+    fn with_auth(mut self, auth: Auth) -> Self {
+        self.auth = Some(auth);
+        self
+    }
+
+    fn template(&self) -> Result<Public> {
+        let object_attributes = ObjectAttributes::builder()
+            .with_fixed_tpm(!self.allow_dup)
+            .with_fixed_parent(!self.allow_dup)
+            .with_sensitive_data_origin(true)
+            .with_user_with_auth(true)
+            .with_decrypt(self.kind.decrypt())
+            .with_sign_encrypt(self.kind.sign())
+            .with_restricted(false)
+            .build()
+            .conv_err()?;
+        let name_hashing_algorithm = HashingAlgorithm::Sha256;
+        let auth_policy = Digest::empty();
+        let parameters = PublicRsaParameters::new(
+            SymmetricDefinitionObject::Null,
+            self.scheme,
+            Self::RSA_KEY_BITS,
+            RsaExponent::try_from(Self::RSA_EXPONENT).conv_err()?, 
+        );
+        let unique = PublicKeyRsa::try_from(self.unique)
+            .conv_err()?;
+        let public = Public::Rsa {
+            object_attributes,
+            name_hashing_algorithm,
+            auth_policy,
+            parameters,
+            unique,
+        };
+        Ok(public)
+    }
+}
+
+#[derive(Clone)]
+struct KeyPair {
+    public: AsymKeyPub,
+    /// A rust struct which wraps an `ESYS_TR` from the ESAPI.
+    private: KeyHandle
+}
+
+impl  KeyPair {
+    fn from_stored(context: &mut Context, stored: &StoredKeyPair) -> Result<KeyPair> {
+        let tpm_handle = TpmHandle::try_from(stored.private).conv_err()?;
+        let key_handle = context.key_handle(tpm_handle)?;
+        Ok(KeyPair { public: stored.public.clone(), private: key_handle })
+    }
+
+    fn to_stored(&self, private: TPM2_HANDLE) -> StoredKeyPair {
+        let public = self.public.clone();
+        StoredKeyPair { public, private }
+    }
+}
+
+#[derive(Clone)]
+struct SignKey(KeyPair);
+
+impl SignKey {
+    fn public(&self) -> &AsymKeyPub {
+        &self.0.public
+    }
+
+    fn private(&self) -> KeyHandle {
+        self.0.private
+    }
+}
+
+#[derive(Clone)]
+struct EncKey(KeyPair);
+
+impl EncKey {
+    fn public(&self) -> &AsymKeyPub {
+        &self.0.public
+    }
+
+    fn private(&self) -> KeyHandle {
+        self.0.private
+    }
+}
+
+struct KeyHandles {
+    sign: SignKey,
+    enc: EncKey,
+}
+
+struct State {
+    context: Context,
+    storage: Storage,
+    node_creds: Option<TpmCreds>,
+}
+
+impl State {
+    fn new(context: Context, storage: Storage) -> State {
+        State { context, storage, node_creds: None }
+    }
+
+    fn init_node_creds(&mut self, state: &Arc<RwLock<State>>) -> Result<()> {
+        let tpm_handles = match &self.storage.node {
+            Some(handles) => handles,
+            None => return Ok(()),
         };
+        let key_handles = tpm_handles.to_key_handles(&mut self.context)?;
+        let auth = self.storage.cookie.auth();
+        self.context.tr_set_auth(key_handles.enc.private().into(), auth.clone())?;
+        self.context.tr_set_auth(key_handles.sign.private().into(), auth)?;
+        self.node_creds = Some(TpmCreds::new(key_handles, state));
+        Ok(())
+    }
+}
+
+pub(crate) struct TpmCredStore {
+    state: Arc<RwLock<State>>,
+    storage_path: PathBuf,
+    cookie: Cookie,
+}
+
+impl TpmCredStore {
+    pub(crate) fn new<P: AsRef<Path>>(mut context: Context, state_path: P) -> Result<TpmCredStore> {
+        let storage = Storage::load_or_init(state_path.as_ref())?;
+        let session = context.start_default_auth_session()?;
+        context.set_sessions((Some(session), None, None));
+        let cookie = storage.cookie.clone();
+        let state = Arc::new(RwLock::new(State::new(context, storage)));
+        {
+            let mut guard = state.write().conv_err()?;
+            guard.init_node_creds(&state)?;
+        }
+        Ok(TpmCredStore { state, storage_path: state_path.as_ref().to_owned(), cookie })
+    }
+
+    fn save_storage(&self, guard: &mut RwLockWriteGuard<State>) -> Result<()> {
+        let file = OpenOptions::new().write(true).open(&self.storage_path).conv_err()?;
+        let mut writer = BufWriter::new(file);
+        guard.storage.save(&mut writer)?;
+        writer.flush().conv_err()?;
+        Ok(())
+    }
+
+    fn gen_key(&self, params: KeyParams) -> Result<KeyPair> {
         let result = {
-            let mut context = self.context.lock().conv_err()?;
-            context.create_primary(
+            let mut guard = self.state.write().conv_err()?;
+            guard.context.create_primary(
                 Hierarchy::Endorsement,
-                template,
-                None,
+                params.template()?,
+                params.auth,
                 None,
                 None,
                 None,
@@ -254,8 +526,128 @@ impl TpmCredStore {
             .conv_err()?
         };
         let public = AsymKeyPub::try_from(result.out_public)?;
+        Ok(KeyPair { public, private: result.key_handle })
+    }
+
+    fn gen_node_sign_key(&self) -> Result<SignKey> {
+        let params = KeyParams::with_unique(self.cookie.as_slice())
+            .with_allow_dup(false)
+            .with_kind(KeyKind::Sign)
+            .with_scheme(RsaScheme::Null)
+            .with_auth(self.cookie.auth());
+        Ok(SignKey(self.gen_key(params)?))
+    }
 
-        Ok(TpmCreds { public, handle: result.key_handle , context: self.context.clone() })
+    fn gen_node_enc_key(&self) -> Result<EncKey> {
+        let params = KeyParams::with_unique(self.cookie.as_slice())
+            .with_allow_dup(false)
+            .with_kind(KeyKind::Decrypt)
+            .with_scheme(RsaScheme::RsaEs)
+            .with_auth(self.cookie.auth());
+        Ok(EncKey(self.gen_key(params)?))
+    }
+
+    fn persist<F: FnOnce(&mut Storage, TpmHandles)>(
+        &self, creds: &TpmCreds, update_storage: F
+    ) -> Result<()> {
+        let mut guard = self.state.write().conv_err()?;
+        let sign_handle = guard.context.persist_key(creds.sign.private())?;
+        let enc_handle = match guard.context.persist_key(creds.enc.private()) {
+            Ok(handle) => handle,
+            Err(error) => {
+                guard.context.evict_key(sign_handle, Some(creds.sign.private()))?;
+                return Err(error)
+            }
+        };
+        let handles = TpmHandles::new(
+            creds.sign.0.to_stored(sign_handle),
+            creds.enc.0.to_stored(enc_handle));
+        update_storage(&mut guard.storage, handles);
+        match self.save_storage(&mut guard) {
+            Ok(_) => Ok(()),
+            Err(error) => {
+                let result = guard.context.evict_key(sign_handle, Some(creds.sign.private()));
+                if let Err(error) = result {
+                    error!("failed to evict signing key due to error: {:?}", error)
+                }
+                let result = guard.context.evict_key(enc_handle, Some(creds.enc.private()));
+                if let Err(error) = result {
+                    error!("failed to evict encryption key due to error: {:?}", error)
+                }
+                Err(error)
+            }
+        }
+    }
+
+    fn gen_node_creds(&self) -> Result<TpmCreds> {
+        let sign = self.gen_node_sign_key()?;
+        let enc = self.gen_node_enc_key()?;
+        let creds = TpmCreds { sign, enc, state: self.state.clone() };
+        self.persist(&creds, |storage, handles| storage.node = Some(handles))?;
+        Ok(creds)
+    }
+
+    fn gen_root_sign_key(&self, password: &str) -> Result<SignKey> {
+        let unique: [u8; COOKIE_LEN] = rand_array()?;
+        let params = KeyParams::with_unique(unique.as_slice())
+            .with_allow_dup(true)
+            .with_kind(KeyKind::Sign)
+            .with_scheme(RsaScheme::Null)
+            .with_auth(Auth::try_from(password.as_bytes()).conv_err()?);
+        Ok(SignKey(self.gen_key(params)?))
+    }
+
+    fn gen_root_enc_key(&self, password: &str) -> Result<EncKey> {
+        let unique: [u8; COOKIE_LEN] = rand_array()?;
+        let params = KeyParams::with_unique(unique.as_slice())
+            .with_allow_dup(true)
+            .with_kind(KeyKind::Decrypt)
+            .with_scheme(RsaScheme::RsaEs)
+            .with_auth(Auth::try_from(password.as_bytes()).conv_err()?);
+        Ok(EncKey(self.gen_key(params)?))
+    }
+}
+
+impl CredStore for TpmCredStore {
+    type CredHandle = TpmCreds;
+
+    fn node_creds(&self) -> Result<TpmCreds> {
+        {
+            let guard = self.state.read().conv_err()?;
+            if let Some(creds) = &guard.node_creds {
+                return Ok(creds.clone())
+            }
+        }
+        self.gen_node_creds()
+    }
+
+    fn root_creds(&self, password: &str) -> Result<Self::CredHandle> {
+        let root_handles = {
+            let guard = self.state.read().conv_err()?;
+            guard.storage.root.as_ref()
+                .ok_or_else(|| Error::custom("root creds have not yet been generated"))?
+                .clone()
+        };
+        let mut guard = self.state.write().conv_err()?;
+        let key_handles = root_handles.to_key_handles(&mut guard.context)?;
+        let auth = Auth::try_from(password.as_bytes()).conv_err()?;
+        guard.context.tr_set_auth(key_handles.sign.private().into(), auth.clone())?;
+        guard.context.tr_set_auth(key_handles.enc.private().into(), auth)?;
+        Ok(TpmCreds::new(key_handles, &self.state))
+    }
+
+    fn gen_root_creds(&self, password: &str) -> Result<Self::CredHandle> {
+        {
+            let guard = self.state.read().conv_err()?;
+            if guard.storage.root.is_some() {
+                return Err(Error::custom("root creds have already been generated"))
+            }
+        }
+        let sign = self.gen_root_sign_key(password)?;
+        let enc = self.gen_root_enc_key(password)?;
+        let creds = TpmCreds { sign, enc, state: self.state.clone() };
+        self.persist(&creds, |storage, handles| storage.root = Some(handles))?;
+        Ok(creds)
     }
 }
 
@@ -350,27 +742,56 @@ impl HashcheckTicketExt for HashcheckTicket {
     }
 }
 
+#[derive(Clone)]
 pub(crate) struct TpmCreds {
-    public: AsymKeyPub,
-    context: Arc<Mutex<Context>>,
-    handle: KeyHandle,
+    state: Arc<RwLock<State>>,
+    sign: SignKey,
+    enc: EncKey,
+}
+
+impl TpmCreds {
+    fn new(key_handles: KeyHandles, state: &Arc<RwLock<State>>) -> TpmCreds {
+        TpmCreds { sign: key_handles.sign, enc: key_handles.enc, state: state.clone() }
+    }
 }
 
 impl Owned for TpmCreds {
     fn owner_of_kind(&self, kind: HashKind) -> Principal {
-        self.public.owner_of_kind(kind)
+        fn hash(creds: &TpmCreds, msg_digest: MessageDigest, mut buf: &mut [u8]) {
+            let digest = {
+                let sign_der = creds.sign.public().to_der().unwrap();
+                let enc_der = creds.enc.public().to_der().unwrap();
+                let mut hasher = Hasher::new(msg_digest).unwrap();
+                hasher.update(sign_der.as_slice()).unwrap();
+                hasher.update(enc_der.as_slice()).unwrap();
+                hasher.finish().unwrap()
+            };
+            buf.write_all(&digest).unwrap();
+        }
+        match kind {
+            HashKind::Sha2_256 => {
+                let mut buf = [0; 32];
+                hash(self, MessageDigest::sha256(), &mut buf);
+                Principal(Hash::Sha2_256(buf))
+            },
+            HashKind::Sha2_512 => {
+                let mut buf = [0; 64];
+                hash(self, MessageDigest::sha512(), &mut buf);
+                Principal(Hash::Sha2_512(buf))
+            }
+        }
     } 
 }
 
 impl Verifier for TpmCreds {
     fn verify<'a, I: Iterator<Item=&'a [u8]>>(&self, parts: I, signature: &[u8]) -> Result<bool> {
-        self.public.verify(parts, signature)
+        self.sign.public().verify(parts, signature)
     }
 }
 
 impl Encrypter for TpmCreds {
     fn encrypt(&self, slice: &[u8]) -> Result<Vec<u8>> {
-        self.public.encrypt(slice)
+        self.enc.public().encrypt(slice)
     }
 }
 
@@ -378,7 +799,7 @@ impl CredsPub for TpmCreds {}
 
 impl Signer for TpmCreds {
     fn sign<'a, I: Iterator<Item=&'a [u8]>>(&self, parts: I) -> Result<Signature> {
-        let msg_digest = self.public.digest();
+        let msg_digest = self.sign.public().digest();
         let digest = {
             let mut hasher = Hasher::new(msg_digest).conv_err()?;
             for part in parts {
@@ -391,8 +812,8 @@ impl Signer for TpmCreds {
         let validation = HashcheckTicket::null();
         let scheme = SignatureScheme::RsaSsa { hash_scheme: msg_digest.hash_scheme()? };
         let sig = {
-            let mut context = self.context.lock().conv_err()?;
-            context.sign(self.handle, digest, scheme, validation)
+            let mut guard = self.state.write().conv_err()?;
+            guard.context.sign(self.sign.private(), digest, scheme, validation)
                 .conv_err()?
         };
         let buf = match sig {
@@ -412,12 +833,12 @@ impl Signer for TpmCreds {
 impl Decrypter for TpmCreds {
     fn decrypt(&self, slice: &[u8]) -> Result<Vec<u8>> {
         let cipher_text = PublicKeyRsa::try_from(slice).conv_err()?;
-        let in_scheme = RsaDecryptionScheme::RsaEs;
+        let in_scheme = RsaDecryptionScheme::Null;
         let empty = [0u8; 0];
         let label = Data::try_from(empty.as_slice()).conv_err()?;
         let plain_text = {
-            let mut lock = self.context.lock().conv_err()?;
-            lock.rsa_decrypt(self.handle, cipher_text, in_scheme, label)?
+            let mut guard = self.state.write().conv_err()?;
+            guard.context.rsa_decrypt(self.enc.private(), cipher_text, in_scheme, label)?
         };
         Ok(Vec::from(plain_text.value()))
     } 
@@ -427,7 +848,7 @@ impl CredsPriv for TpmCreds {}
 
 impl Creds for TpmCreds {
     fn public(&self) -> &AsymKeyPub {
-        &self.public
+        unimplemented!()
     }
 }
 
@@ -603,7 +1024,7 @@ mod test {
         let metadata = cookie.metadata().conv_err()?;
         let actual = metadata.permissions().mode();
         // Assert that the cookie can only be read by its owner.
-        assert_eq!(0o400, 0o777 & actual);
+        assert_eq!(0o600, 0o777 & actual);
         drop(store);
         dir.close()?;
         Ok(())
@@ -650,34 +1071,53 @@ mod test {
         assert_eq(MessageDigest::sha3_512(), Nid::sha3_512());
     }
 
-    fn test_store() -> Result<TpmCredStore> {
+    fn state_path<P: AsRef<Path>>(dir: P) -> PathBuf {
+        dir.as_ref().join("state.bt")
+    }
+
+    fn test_store() -> Result<(TpmCredStore, TempDir)> {
         let dir = TempDir::new("btnode").conv_err()?;
-        let cookie_path = dir.path().join("cookie.bin");
-        let store = TpmCredStore::new(Context::for_test()?, &cookie_path)?;       
-        dir.close()?;
-        Ok(store)
+        let state_path = state_path(dir.path());
+        let store = TpmCredStore::new(Context::for_test()?, &state_path)?;       
+        Ok((store, dir))
     }
 
-    #[test]
-    fn tpm_sign_verify() -> Result<()> {
-        let store = test_store()?;
-        let handle = store.gen_node_creds()?;
+    fn sign_verify_test(creds: &TpmCreds) -> Result<()> {
         let data: [u8; 1024] = rand_array()?;
         let parts = [data.as_slice()];
-        let sig = handle.sign(parts.into_iter())?;
-        assert!(handle.verify(parts.into_iter(), sig.as_slice())?);
-        Ok(())
+        let sig = creds.sign(parts.into_iter())?;
+        if creds.verify(parts.into_iter(), sig.as_slice())? {
+            Ok(())
+        }
+        else {
+            Err(Error::custom("signature verification failed"))
+        }
     }
 
     #[test]
-    fn tpm_encrypt_decrypt() -> Result<()> {
-        let store = test_store()?;
-        let handle = store.gen_node_creds()?;
+    fn tpm_sign_verify() -> Result<()> {
+        let (store, _dir) = test_store()?;
+        let creds = store.gen_node_creds()?;
+        sign_verify_test(&creds)
+    }
+
+    fn encrypt_decrypt_test(creds: &TpmCreds) -> Result<()> {
         let expected: [u8; RSA_KEY_BYTES / 2] = rand_array()?;
-        let ct = handle.encrypt(expected.as_slice())?;
-        let actual = handle.decrypt(&ct)?;
-        assert_eq!(expected.as_slice(), actual);
-        Ok(())
+        let ct = creds.encrypt(expected.as_slice())?;
+        let actual = creds.decrypt(&ct)?;
+        if expected.as_slice() == actual {
+            Ok(())
+        }
+        else {
+            Err(Error::custom("decrypted data did not match input"))
+        }
+    }
+
+    #[test]
+    fn tpm_encrypt_decrypt() -> Result<()> {
+        let (store, _dir) = test_store()?;
+        let creds = store.gen_node_creds()?;
+        encrypt_decrypt_test(&creds)
     }
 
     /// Tests that `HashcheckTicket::null` doesn't panic.
@@ -689,7 +1129,7 @@ mod test {
     /// Checks that the value of `TpmCredStore::RSA_KEY_BITS` matches the value of `RSA_KEY_BYTES`.
     #[test]
     fn rsa_key_bits_and_key_bytes_compatible() {
-        let bytes = match TpmCredStore::RSA_KEY_BITS {
+        let bytes = match KeyParams::RSA_KEY_BITS {
             RsaKeyBits::Rsa1024 => 128,
             RsaKeyBits::Rsa2048 => 256,
             RsaKeyBits::Rsa3072 => 384,
@@ -698,11 +1138,76 @@ mod test {
         assert_eq!(RSA_KEY_BYTES, bytes)
     }
 
-    fn list_persistent_handles() {
+    #[test]
+    fn persistent_handles() {
         let mut context = Context::for_test().unwrap();
-        let all_handles = context.persistent_handles().unwrap();
-        for handle in all_handles.iter() {
-            println!("{:?}", handle);
-        }
+        let _all_handles = context.persistent_handles().unwrap();
+    }
+
+    #[test]
+    fn first_free_persistent() -> Result<()> {
+        let mut context = Context::for_test()?;
+        let _first = context.first_free_persistent()?;
+        Ok(())
+    }
+
+    #[test]
+    fn persist_key() -> Result<()> {
+        let (store, _dir) = test_store()?;
+        let cookie = Cookie::random()?;
+        let params = KeyParams::with_unique(cookie.as_slice());
+        let pair = store.gen_key(params)?;
+        let mut guard = store.state.write().conv_err()?;
+        guard.context.persist_key(pair.private)?;
+        Ok(())
+    }
+
+    /// Tests that the same node key is returned by after a cred store is dropped and recreated.
+    #[test]
+    fn node_key_persisted() -> Result<()> {
+        let (store, dir) = test_store()?;
+        let expected = {
+            let creds = store.node_creds()?;
+            // This contains a hash of the public keys in `creds`, so it strongly identifies them.
+            creds.owner()
+        };
+        drop(store);
+        let store = TpmCredStore::new(Context::for_test()?, state_path(dir.path()))?;
+        let creds = store.node_creds()?;
+        let actual = creds.owner();
+        assert_eq!(expected, actual);
+        sign_verify_test(&creds)?;
+        encrypt_decrypt_test(&creds)?;
+        Ok(())
+    }
+
+    #[test]
+    fn root_key_persisted() -> Result<()> {
+        const PASSWORD: &str = "Scaramouch";
+        let (store, dir) = test_store()?;
+        let expected = {
+            let creds = store.gen_root_creds(PASSWORD)?;
+            creds.owner()
+        };
+        drop(store);
+        let store = TpmCredStore::new(Context::for_test()?, state_path(dir.path()))?;
+        let creds = store.root_creds(PASSWORD)?;
+        let actual = creds.owner();
+        assert_eq!(expected, actual);
+        sign_verify_test(&creds)?;
+        encrypt_decrypt_test(&creds)?;
+        Ok(())
+    }
+
+    #[test]
+    fn root_key_not_returned_when_password_wrong() -> Result<()> {
+        let (store, dir) = test_store()?;
+        store.gen_root_creds("Galileo")?;
+        drop(store);
+        let store = TpmCredStore::new(Context::for_test()?, state_path(dir.path()))?;
+        let creds = store.root_creds("Figaro")?;
+        assert!(sign_verify_test(&creds).is_err());
+        assert!(encrypt_decrypt_test(&creds).is_err());
+        Ok(())
     }
 }