blob: aba2c619117ab99d22d47a3dde8b478facc7987c [file] [log] [blame]
// Copyright 2022, The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//! Helper wrapper around RKPD interface.
// TODO(b/264891956): Return RKP specific errors.
use crate::error::{map_binder_status_code, Error};
use crate::globals::get_remotely_provisioned_component_name;
use crate::ks_err;
use crate::utils::watchdog as wd;
use android_hardware_security_keymint::aidl::android::hardware::security::keymint::SecurityLevel::SecurityLevel;
use android_security_rkp_aidl::aidl::android::security::rkp::{
IGetKeyCallback::BnGetKeyCallback, IGetKeyCallback::IGetKeyCallback,
IGetRegistrationCallback::BnGetRegistrationCallback,
IGetRegistrationCallback::IGetRegistrationCallback, IRegistration::IRegistration,
IRemoteProvisioning::IRemoteProvisioning,
IStoreUpgradedKeyCallback::BnStoreUpgradedKeyCallback,
IStoreUpgradedKeyCallback::IStoreUpgradedKeyCallback,
RemotelyProvisionedKey::RemotelyProvisionedKey,
};
use android_security_rkp_aidl::binder::{BinderFeatures, Interface, Strong};
use android_system_keystore2::aidl::android::system::keystore2::ResponseCode::ResponseCode;
use anyhow::{Context, Result};
use std::sync::Mutex;
use std::time::Duration;
use tokio::sync::oneshot;
use tokio::time::timeout;
// Normally, we block indefinitely when making calls outside of keystore and rely on watchdog to
// report deadlocks. However, RKPD is mainline updatable. Also, calls to RKPD may wait on network
// for certificates. So, we err on the side of caution and timeout instead.
static RKPD_TIMEOUT: Duration = Duration::from_secs(10);
fn tokio_rt() -> tokio::runtime::Runtime {
tokio::runtime::Builder::new_current_thread().enable_all().build().unwrap()
}
/// Thread-safe channel for sending a value once and only once. If a value has
/// already been send, subsequent calls to send will noop.
struct SafeSender<T> {
inner: Mutex<Option<oneshot::Sender<T>>>,
}
impl<T> SafeSender<T> {
fn new(sender: oneshot::Sender<T>) -> Self {
Self { inner: Mutex::new(Some(sender)) }
}
fn send(&self, value: T) {
if let Some(inner) = self.inner.lock().unwrap().take() {
// assert instead of unwrap, because on failure send returns Err(value)
assert!(inner.send(value).is_ok(), "thread state is terminally broken");
}
}
}
struct GetRegistrationCallback {
registration_tx: SafeSender<Result<binder::Strong<dyn IRegistration>>>,
}
impl GetRegistrationCallback {
pub fn new_native_binder(
registration_tx: oneshot::Sender<Result<binder::Strong<dyn IRegistration>>>,
) -> Strong<dyn IGetRegistrationCallback> {
let result: Self =
GetRegistrationCallback { registration_tx: SafeSender::new(registration_tx) };
BnGetRegistrationCallback::new_binder(result, BinderFeatures::default())
}
}
impl Interface for GetRegistrationCallback {}
impl IGetRegistrationCallback for GetRegistrationCallback {
fn onSuccess(&self, registration: &Strong<dyn IRegistration>) -> binder::Result<()> {
let _wp = wd::watch_millis("IGetRegistrationCallback::onSuccess", 500);
self.registration_tx.send(Ok(registration.clone()));
Ok(())
}
fn onCancel(&self) -> binder::Result<()> {
let _wp = wd::watch_millis("IGetRegistrationCallback::onCancel", 500);
log::warn!("IGetRegistrationCallback cancelled");
self.registration_tx.send(
Err(Error::Rc(ResponseCode::OUT_OF_KEYS))
.context(ks_err!("GetRegistrationCallback cancelled.")),
);
Ok(())
}
fn onError(&self, error: &str) -> binder::Result<()> {
let _wp = wd::watch_millis("IGetRegistrationCallback::onError", 500);
log::error!("IGetRegistrationCallback failed: '{error}'");
self.registration_tx.send(
Err(Error::Rc(ResponseCode::OUT_OF_KEYS))
.context(ks_err!("GetRegistrationCallback failed: {:?}", error)),
);
Ok(())
}
}
/// Make a new connection to a IRegistration service.
async fn get_rkpd_registration(
security_level: &SecurityLevel,
) -> Result<binder::Strong<dyn IRegistration>> {
let remote_provisioning: Strong<dyn IRemoteProvisioning> =
map_binder_status_code(binder::get_interface("remote_provisioning"))
.context(ks_err!("Trying to connect to IRemoteProvisioning service."))?;
let rpc_name = get_remotely_provisioned_component_name(security_level)
.context(ks_err!("Trying to get IRPC name."))?;
let (tx, rx) = oneshot::channel();
let cb = GetRegistrationCallback::new_native_binder(tx);
remote_provisioning
.getRegistration(&rpc_name, &cb)
.context(ks_err!("Trying to get registration."))?;
match timeout(RKPD_TIMEOUT, rx).await {
Err(e) => {
Err(Error::Rc(ResponseCode::SYSTEM_ERROR)).context(ks_err!("Waiting for RKPD: {:?}", e))
}
Ok(v) => v.unwrap(),
}
}
struct GetKeyCallback {
key_tx: SafeSender<Result<RemotelyProvisionedKey>>,
}
impl GetKeyCallback {
pub fn new_native_binder(
key_tx: oneshot::Sender<Result<RemotelyProvisionedKey>>,
) -> Strong<dyn IGetKeyCallback> {
let result: Self = GetKeyCallback { key_tx: SafeSender::new(key_tx) };
BnGetKeyCallback::new_binder(result, BinderFeatures::default())
}
}
impl Interface for GetKeyCallback {}
impl IGetKeyCallback for GetKeyCallback {
fn onSuccess(&self, key: &RemotelyProvisionedKey) -> binder::Result<()> {
let _wp = wd::watch_millis("IGetKeyCallback::onSuccess", 500);
self.key_tx.send(Ok(RemotelyProvisionedKey {
keyBlob: key.keyBlob.clone(),
encodedCertChain: key.encodedCertChain.clone(),
}));
Ok(())
}
fn onCancel(&self) -> binder::Result<()> {
let _wp = wd::watch_millis("IGetKeyCallback::onCancel", 500);
log::warn!("IGetKeyCallback cancelled");
self.key_tx.send(
Err(Error::Rc(ResponseCode::OUT_OF_KEYS)).context(ks_err!("GetKeyCallback cancelled.")),
);
Ok(())
}
fn onError(&self, error: &str) -> binder::Result<()> {
let _wp = wd::watch_millis("IGetKeyCallback::onError", 500);
log::error!("IGetKeyCallback failed: {error}");
self.key_tx.send(
Err(Error::Rc(ResponseCode::OUT_OF_KEYS))
.context(ks_err!("GetKeyCallback failed: {:?}", error)),
);
Ok(())
}
}
async fn get_rkpd_attestation_key_from_registration_async(
registration: &Strong<dyn IRegistration>,
caller_uid: u32,
) -> Result<RemotelyProvisionedKey> {
let (tx, rx) = oneshot::channel();
let cb = GetKeyCallback::new_native_binder(tx);
registration
.getKey(caller_uid.try_into().unwrap(), &cb)
.context(ks_err!("Trying to get key."))?;
match timeout(RKPD_TIMEOUT, rx).await {
Err(e) => Err(Error::Rc(ResponseCode::OUT_OF_KEYS))
.context(ks_err!("Waiting for RKPD key timed out: {:?}", e)),
Ok(v) => v.unwrap(),
}
}
async fn get_rkpd_attestation_key_async(
security_level: &SecurityLevel,
caller_uid: u32,
) -> Result<RemotelyProvisionedKey> {
let registration = get_rkpd_registration(security_level)
.await
.context(ks_err!("Trying to get to IRegistration service."))?;
get_rkpd_attestation_key_from_registration_async(&registration, caller_uid).await
}
struct StoreUpgradedKeyCallback {
completer: SafeSender<Result<()>>,
}
impl StoreUpgradedKeyCallback {
pub fn new_native_binder(
completer: oneshot::Sender<Result<()>>,
) -> Strong<dyn IStoreUpgradedKeyCallback> {
let result: Self = StoreUpgradedKeyCallback { completer: SafeSender::new(completer) };
BnStoreUpgradedKeyCallback::new_binder(result, BinderFeatures::default())
}
}
impl Interface for StoreUpgradedKeyCallback {}
impl IStoreUpgradedKeyCallback for StoreUpgradedKeyCallback {
fn onSuccess(&self) -> binder::Result<()> {
let _wp = wd::watch_millis("IGetRegistrationCallback::onSuccess", 500);
self.completer.send(Ok(()));
Ok(())
}
fn onError(&self, error: &str) -> binder::Result<()> {
let _wp = wd::watch_millis("IGetRegistrationCallback::onError", 500);
log::error!("IGetRegistrationCallback failed: {error}");
self.completer.send(
Err(Error::Rc(ResponseCode::SYSTEM_ERROR))
.context(ks_err!("Failed to store upgraded key: {:?}", error)),
);
Ok(())
}
}
async fn store_rkpd_attestation_key_with_registration_async(
registration: &Strong<dyn IRegistration>,
key_blob: &[u8],
upgraded_blob: &[u8],
) -> Result<()> {
let (tx, rx) = oneshot::channel();
let cb = StoreUpgradedKeyCallback::new_native_binder(tx);
registration
.storeUpgradedKeyAsync(key_blob, upgraded_blob, &cb)
.context(ks_err!("Failed to store upgraded blob with RKPD."))?;
match timeout(RKPD_TIMEOUT, rx).await {
Err(e) => Err(Error::Rc(ResponseCode::SYSTEM_ERROR))
.context(ks_err!("Waiting for RKPD to complete storing key: {:?}", e)),
Ok(v) => v.unwrap(),
}
}
async fn store_rkpd_attestation_key_async(
security_level: &SecurityLevel,
key_blob: &[u8],
upgraded_blob: &[u8],
) -> Result<()> {
let registration = get_rkpd_registration(security_level)
.await
.context(ks_err!("Trying to get to IRegistration service."))?;
store_rkpd_attestation_key_with_registration_async(&registration, key_blob, upgraded_blob).await
}
/// Get attestation key from RKPD.
pub fn get_rkpd_attestation_key(
security_level: &SecurityLevel,
caller_uid: u32,
) -> Result<RemotelyProvisionedKey> {
let _wp = wd::watch_millis("Calling get_rkpd_attestation_key()", 500);
tokio_rt().block_on(get_rkpd_attestation_key_async(security_level, caller_uid))
}
/// Store attestation key in RKPD.
pub fn store_rkpd_attestation_key(
security_level: &SecurityLevel,
key_blob: &[u8],
upgraded_blob: &[u8],
) -> Result<()> {
let _wp = wd::watch_millis("Calling store_rkpd_attestation_key()", 500);
tokio_rt().block_on(store_rkpd_attestation_key_async(security_level, key_blob, upgraded_blob))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::error::map_km_error;
use crate::globals::get_keymint_device;
use crate::utils::upgrade_keyblob_if_required_with;
use android_hardware_security_keymint::aidl::android::hardware::security::keymint::{
Algorithm::Algorithm, AttestationKey::AttestationKey, KeyParameter::KeyParameter,
KeyParameterValue::KeyParameterValue, Tag::Tag,
};
use android_security_rkp_aidl::aidl::android::security::rkp::IRegistration::BnRegistration;
use keystore2_crypto::parse_subject_from_certificate;
use std::sync::atomic::{AtomicU32, Ordering};
#[derive(Default)]
struct MockRegistration {
key: RemotelyProvisionedKey,
latency: Option<Duration>,
}
impl MockRegistration {
pub fn new_native_binder(
key: &RemotelyProvisionedKey,
latency: Option<Duration>,
) -> Strong<dyn IRegistration> {
let result = Self {
key: RemotelyProvisionedKey {
keyBlob: key.keyBlob.clone(),
encodedCertChain: key.encodedCertChain.clone(),
},
latency,
};
BnRegistration::new_binder(result, BinderFeatures::default())
}
}
impl Interface for MockRegistration {}
impl IRegistration for MockRegistration {
fn getKey(&self, _: i32, cb: &Strong<dyn IGetKeyCallback>) -> binder::Result<()> {
let key = RemotelyProvisionedKey {
keyBlob: self.key.keyBlob.clone(),
encodedCertChain: self.key.encodedCertChain.clone(),
};
let latency = self.latency;
let get_key_cb = cb.clone();
// Need a separate thread to trigger timeout in the caller.
std::thread::spawn(move || {
if let Some(duration) = latency {
std::thread::sleep(duration);
}
get_key_cb.onSuccess(&key).unwrap();
});
Ok(())
}
fn cancelGetKey(&self, _: &Strong<dyn IGetKeyCallback>) -> binder::Result<()> {
todo!()
}
fn storeUpgradedKeyAsync(
&self,
_: &[u8],
_: &[u8],
cb: &Strong<dyn IStoreUpgradedKeyCallback>,
) -> binder::Result<()> {
// We are primarily concerned with timing out correctly. Storing the key in this mock
// registration isn't particularly interesting, so skip that part.
let store_cb = cb.clone();
let latency = self.latency;
std::thread::spawn(move || {
if let Some(duration) = latency {
std::thread::sleep(duration);
}
store_cb.onSuccess().unwrap();
});
Ok(())
}
}
fn get_mock_registration(
key: &RemotelyProvisionedKey,
latency: Option<Duration>,
) -> Result<binder::Strong<dyn IRegistration>> {
let (tx, rx) = oneshot::channel();
let cb = GetRegistrationCallback::new_native_binder(tx);
let mock_registration = MockRegistration::new_native_binder(key, latency);
assert!(cb.onSuccess(&mock_registration).is_ok());
tokio_rt().block_on(rx).unwrap()
}
// Using the same key ID makes test cases race with each other. So, we use separate key IDs for
// different test cases.
fn get_next_key_id() -> u32 {
static ID: AtomicU32 = AtomicU32::new(0);
ID.fetch_add(1, Ordering::Relaxed)
}
#[test]
fn test_get_registration_cb_success() {
let key: RemotelyProvisionedKey = Default::default();
let registration = get_mock_registration(&key, /*latency=*/ None);
assert!(registration.is_ok());
}
#[test]
fn test_get_registration_cb_cancel() {
let (tx, rx) = oneshot::channel();
let cb = GetRegistrationCallback::new_native_binder(tx);
assert!(cb.onCancel().is_ok());
let result = tokio_rt().block_on(rx).unwrap();
assert_eq!(
result.unwrap_err().downcast::<Error>().unwrap(),
Error::Rc(ResponseCode::OUT_OF_KEYS)
);
}
#[test]
fn test_get_registration_cb_error() {
let (tx, rx) = oneshot::channel();
let cb = GetRegistrationCallback::new_native_binder(tx);
assert!(cb.onError("error").is_ok());
let result = tokio_rt().block_on(rx).unwrap();
assert_eq!(
result.unwrap_err().downcast::<Error>().unwrap(),
Error::Rc(ResponseCode::OUT_OF_KEYS)
);
}
#[test]
fn test_get_key_cb_success() {
let mock_key =
RemotelyProvisionedKey { keyBlob: vec![1, 2, 3], encodedCertChain: vec![4, 5, 6] };
let (tx, rx) = oneshot::channel();
let cb = GetKeyCallback::new_native_binder(tx);
assert!(cb.onSuccess(&mock_key).is_ok());
let key = tokio_rt().block_on(rx).unwrap().unwrap();
assert_eq!(key, mock_key);
}
#[test]
fn test_get_key_cb_cancel() {
let (tx, rx) = oneshot::channel();
let cb = GetKeyCallback::new_native_binder(tx);
assert!(cb.onCancel().is_ok());
let result = tokio_rt().block_on(rx).unwrap();
assert_eq!(
result.unwrap_err().downcast::<Error>().unwrap(),
Error::Rc(ResponseCode::OUT_OF_KEYS)
);
}
#[test]
fn test_get_key_cb_error() {
let (tx, rx) = oneshot::channel();
let cb = GetKeyCallback::new_native_binder(tx);
assert!(cb.onError("error").is_ok());
let result = tokio_rt().block_on(rx).unwrap();
assert_eq!(
result.unwrap_err().downcast::<Error>().unwrap(),
Error::Rc(ResponseCode::OUT_OF_KEYS)
);
}
#[test]
fn test_store_upgraded_cb_success() {
let (tx, rx) = oneshot::channel();
let cb = StoreUpgradedKeyCallback::new_native_binder(tx);
assert!(cb.onSuccess().is_ok());
tokio_rt().block_on(rx).unwrap().unwrap();
}
#[test]
fn test_store_upgraded_key_cb_error() {
let (tx, rx) = oneshot::channel();
let cb = StoreUpgradedKeyCallback::new_native_binder(tx);
assert!(cb.onError("oh no! it failed").is_ok());
let result = tokio_rt().block_on(rx).unwrap();
assert_eq!(
result.unwrap_err().downcast::<Error>().unwrap(),
Error::Rc(ResponseCode::SYSTEM_ERROR)
);
}
#[test]
fn test_get_mock_key_success() {
let mock_key =
RemotelyProvisionedKey { keyBlob: vec![1, 2, 3], encodedCertChain: vec![4, 5, 6] };
let registration = get_mock_registration(&mock_key, /*latency=*/ None).unwrap();
let key = tokio_rt()
.block_on(get_rkpd_attestation_key_from_registration_async(&registration, 0))
.unwrap();
assert_eq!(key, mock_key);
}
#[test]
fn test_get_mock_key_timeout() {
let mock_key =
RemotelyProvisionedKey { keyBlob: vec![1, 2, 3], encodedCertChain: vec![4, 5, 6] };
let latency = RKPD_TIMEOUT + Duration::from_secs(10);
let registration = get_mock_registration(&mock_key, Some(latency)).unwrap();
let result =
tokio_rt().block_on(get_rkpd_attestation_key_from_registration_async(&registration, 0));
assert_eq!(
result.unwrap_err().downcast::<Error>().unwrap(),
Error::Rc(ResponseCode::OUT_OF_KEYS)
);
}
#[test]
fn test_store_mock_key_success() {
let mock_key =
RemotelyProvisionedKey { keyBlob: vec![1, 2, 3], encodedCertChain: vec![4, 5, 6] };
let registration = get_mock_registration(&mock_key, /*latency=*/ None).unwrap();
tokio_rt()
.block_on(store_rkpd_attestation_key_with_registration_async(&registration, &[], &[]))
.unwrap();
}
#[test]
fn test_store_mock_key_timeout() {
let mock_key =
RemotelyProvisionedKey { keyBlob: vec![1, 2, 3], encodedCertChain: vec![4, 5, 6] };
let latency = RKPD_TIMEOUT + Duration::from_secs(10);
let registration = get_mock_registration(&mock_key, Some(latency)).unwrap();
let result = tokio_rt().block_on(store_rkpd_attestation_key_with_registration_async(
&registration,
&[],
&[],
));
assert_eq!(
result.unwrap_err().downcast::<Error>().unwrap(),
Error::Rc(ResponseCode::SYSTEM_ERROR)
);
}
#[test]
fn test_get_rkpd_attestation_key() {
binder::ProcessState::start_thread_pool();
let key_id = get_next_key_id();
let key = get_rkpd_attestation_key(&SecurityLevel::TRUSTED_ENVIRONMENT, key_id).unwrap();
assert!(!key.keyBlob.is_empty());
assert!(!key.encodedCertChain.is_empty());
}
#[test]
fn test_get_rkpd_attestation_key_same_caller() {
binder::ProcessState::start_thread_pool();
let sec_level = SecurityLevel::TRUSTED_ENVIRONMENT;
let key_id = get_next_key_id();
// Multiple calls should return the same key.
let first_key = get_rkpd_attestation_key(&sec_level, key_id).unwrap();
let second_key = get_rkpd_attestation_key(&sec_level, key_id).unwrap();
assert_eq!(first_key.keyBlob, second_key.keyBlob);
assert_eq!(first_key.encodedCertChain, second_key.encodedCertChain);
}
#[test]
fn test_get_rkpd_attestation_key_different_caller() {
binder::ProcessState::start_thread_pool();
let sec_level = SecurityLevel::TRUSTED_ENVIRONMENT;
let first_key_id = get_next_key_id();
let second_key_id = get_next_key_id();
// Different callers should be getting different keys.
let first_key = get_rkpd_attestation_key(&sec_level, first_key_id).unwrap();
let second_key = get_rkpd_attestation_key(&sec_level, second_key_id).unwrap();
assert_ne!(first_key.keyBlob, second_key.keyBlob);
assert_ne!(first_key.encodedCertChain, second_key.encodedCertChain);
}
#[test]
// Couple of things to note:
// 1. This test must never run with UID of keystore. Otherwise, it can mess up keys stored by
// keystore.
// 2. Storing and reading the stored key is prone to race condition. So, we only do this in one
// test case.
fn test_store_rkpd_attestation_key() {
binder::ProcessState::start_thread_pool();
let sec_level = SecurityLevel::TRUSTED_ENVIRONMENT;
let key_id = get_next_key_id();
let key = get_rkpd_attestation_key(&SecurityLevel::TRUSTED_ENVIRONMENT, key_id).unwrap();
let new_blob: [u8; 8] = rand::random();
assert!(store_rkpd_attestation_key(&sec_level, &key.keyBlob, &new_blob).is_ok());
let new_key =
get_rkpd_attestation_key(&SecurityLevel::TRUSTED_ENVIRONMENT, key_id).unwrap();
// Restore original key so that we don't leave RKPD with invalid blobs.
assert!(store_rkpd_attestation_key(&sec_level, &new_blob, &key.keyBlob).is_ok());
assert_eq!(new_key.keyBlob, new_blob);
}
#[test]
// This is a helper for a manual test. We want to check that after a system upgrade RKPD
// attestation keys can also be upgraded and stored again with RKPD. The steps are:
// 1. Run this test and check in stdout that no key upgrade happened.
// 2. Perform a system upgrade.
// 3. Run this test and check in stdout that key upgrade did happen.
//
// Note that this test must be run with that same UID every time. Running as root, i.e. UID 0,
// should do the trick. Also, use "--nocapture" flag to get stdout.
fn test_rkpd_attestation_key_upgrade() {
binder::ProcessState::start_thread_pool();
let security_level = SecurityLevel::TRUSTED_ENVIRONMENT;
let (keymint, _, _) = get_keymint_device(&security_level).unwrap();
let key_id = get_next_key_id();
let mut key_upgraded = false;
let key = get_rkpd_attestation_key(&security_level, key_id).unwrap();
assert!(!key.keyBlob.is_empty());
assert!(!key.encodedCertChain.is_empty());
upgrade_keyblob_if_required_with(
&*keymint,
&key.keyBlob,
/*upgrade_params=*/ &[],
/*km_op=*/
|blob| {
let params = vec![
KeyParameter {
tag: Tag::ALGORITHM,
value: KeyParameterValue::Algorithm(Algorithm::AES),
},
KeyParameter { tag: Tag::KEY_SIZE, value: KeyParameterValue::Integer(128) },
];
let attestation_key = AttestationKey {
keyBlob: blob.to_vec(),
attestKeyParams: vec![],
issuerSubjectName: parse_subject_from_certificate(&key.encodedCertChain)
.unwrap(),
};
map_km_error(keymint.generateKey(&params, Some(&attestation_key)))
},
/*new_blob_handler=*/
|new_blob| {
// This handler is only executed if a key upgrade was performed.
key_upgraded = true;
store_rkpd_attestation_key(&security_level, &key.keyBlob, new_blob).unwrap();
Ok(())
},
)
.unwrap();
if key_upgraded {
println!("RKPD key was upgraded and stored with RKPD.");
} else {
println!("RKPD key was NOT upgraded.");
}
}
#[test]
#[ignore] // b/266607003
fn test_stress_get_rkpd_attestation_key() {
binder::ProcessState::start_thread_pool();
let key_id = get_next_key_id();
let mut threads = vec![];
const NTHREADS: u32 = 10;
const NCALLS: u32 = 1000;
for _ in 0..NTHREADS {
threads.push(std::thread::spawn(move || {
for _ in 0..NCALLS {
let key = get_rkpd_attestation_key(&SecurityLevel::TRUSTED_ENVIRONMENT, key_id)
.unwrap();
assert!(!key.keyBlob.is_empty());
assert!(!key.encodedCertChain.is_empty());
}
}));
}
for t in threads {
assert!(t.join().is_ok());
}
}
}