diff --git a/image-rs/protos/getresource.proto b/image-rs/protos/getresource.proto index 93cd2e4de..97faf9344 100644 --- a/image-rs/protos/getresource.proto +++ b/image-rs/protos/getresource.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -package getresource; +package api; message GetResourceRequest { string ResourcePath = 1; diff --git a/image-rs/scripts/build_attestation_agent.sh b/image-rs/scripts/build_confidential_data_hub.sh similarity index 69% rename from image-rs/scripts/build_attestation_agent.sh rename to image-rs/scripts/build_confidential_data_hub.sh index 4d073604b..d499baf72 100755 --- a/image-rs/scripts/build_attestation_agent.sh +++ b/image-rs/scripts/build_confidential_data_hub.sh @@ -9,12 +9,9 @@ set -o errexit set -o nounset set -o pipefail -parameters=("KBC=offline_fs_kbc") - [ -n "${BASH_VERSION:-}" ] && set -o errtrace [ -n "${DEBUG:-}" ] && set -o xtrace if [[ -n "${TTRPC:-}" ]]; then - parameters+=("ttrpc=true") dest_dir_suffix="ttrpc" else dest_dir_suffix="grpc" @@ -23,12 +20,12 @@ fi source $HOME/.cargo/env SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) -AA_DIR=$SCRIPT_DIR/../../attestation-agent +CDH_DIR=$SCRIPT_DIR/../../confidential-data-hub -pushd $AA_DIR +pushd $CDH_DIR -make "${parameters[@]}" +make make DESTDIR="${SCRIPT_DIR}/${dest_dir_suffix}" install -file "${SCRIPT_DIR}/${dest_dir_suffix}/attestation-agent" +file "${SCRIPT_DIR}/${dest_dir_suffix}/confidential-data-hub" popd diff --git a/image-rs/scripts/install_offline_fs_kbc_files.sh b/image-rs/scripts/install_offline_fs_kbc_files.sh index cdf5bfc24..62597bd6f 100755 --- a/image-rs/scripts/install_offline_fs_kbc_files.sh +++ b/image-rs/scripts/install_offline_fs_kbc_files.sh @@ -25,11 +25,19 @@ if [ "${1:-}" = "install" ]; then sudo install --owner=root --group=root --mode=0640 "${test_resource_record}" "${target_resource_record_path}" sudo install --owner=root --group=root --mode=0640 "${test_keys}" "${target_keys_path}" + # This is a workaround for CDH to read the aa_kbc_params from a + # file rather than kernel commandline. This makes a fake environment + # that CDH will recognize as it is in "peer pod", so aa_kbc param will + # be read from a file. + # TODO: when CDH has its own configuration launch file, we can + # promote this way. + mkdir -p /run/peerpod + touch /run/peerpod/daemon.json + echo "aa_kbc_params = \"offline_fs_kbc::null\"" > /etc/agent-config.toml elif [ "${1:-}" = "clean" ]; then sudo rm "${target_resource_record_path}" sudo rm "${target_keys_path}" else echo >&2 "ERROR: Wrong or missing argument: '${1:-}'" - fi diff --git a/image-rs/src/resource/kbs/grpc.rs b/image-rs/src/resource/kbs/grpc.rs index e23ddab1c..d44b28c4a 100644 --- a/image-rs/src/resource/kbs/grpc.rs +++ b/image-rs/src/resource/kbs/grpc.rs @@ -19,7 +19,7 @@ mod get_resource { #![allow(unknown_lints)] #![allow(clippy::derive_partial_eq_without_eq)] #![allow(clippy::redundant_async_block)] - tonic::include_proto!("getresource"); + tonic::include_proto!("api"); } /// Attestation Agent's GetResource gRPC address. diff --git a/image-rs/src/resource/kbs/mod.rs b/image-rs/src/resource/kbs/mod.rs index eb565d023..7d7990696 100644 --- a/image-rs/src/resource/kbs/mod.rs +++ b/image-rs/src/resource/kbs/mod.rs @@ -119,27 +119,10 @@ impl Protocol for SecureChannel { return Ok(res); } - // Related issue: https://github.com/confidential-containers/attestation-agent/issues/130 - // - // Now we use `aa_kbc_params` to specify the KBC and KBS URI - // used in CoCo System. Different KBCs are initialized in AA lazily due - // to the kbs uri information included in a `download_confidential_resource` or - // `decrypt_image_layer_annotation`. The kbs uri input to the two APIs - // are from `aa_kbc_params` but not the kbs uri in a resource uri. - // Thus as a temporary solution, we need to overwrite the - // kbs uri field using the one included in `aa_kbc_params`, s.t. - // `kbs_uri` of [`SecureChannel`]. - let resource_path = get_resource_path(resource_uri)?; - - let res = self.client.get_resource(&resource_path).await?; + let res = self.client.get_resource(resource_uri).await?; let path = self.get_filepath(resource_uri); fs::write(path, &res).await?; Ok(res) } } - -fn get_resource_path(uri: &str) -> Result { - let path = url::Url::parse(uri)?; - Ok(path.path().to_string()) -} diff --git a/image-rs/src/resource/kbs/ttrpc_proto/getresource.rs b/image-rs/src/resource/kbs/ttrpc_proto/getresource.rs index 0bdf02599..7cfe2fefa 100644 --- a/image-rs/src/resource/kbs/ttrpc_proto/getresource.rs +++ b/image-rs/src/resource/kbs/ttrpc_proto/getresource.rs @@ -25,14 +25,14 @@ /// of protobuf runtime. const _PROTOBUF_VERSION_CHECK: () = ::protobuf::VERSION_3_3_0; -// @@protoc_insertion_point(message:getresource.GetResourceRequest) +// @@protoc_insertion_point(message:api.GetResourceRequest) #[derive(PartialEq,Clone,Default,Debug)] pub struct GetResourceRequest { // message fields - // @@protoc_insertion_point(field:getresource.GetResourceRequest.ResourcePath) + // @@protoc_insertion_point(field:api.GetResourceRequest.ResourcePath) pub ResourcePath: ::std::string::String, // special fields - // @@protoc_insertion_point(special_field:getresource.GetResourceRequest.special_fields) + // @@protoc_insertion_point(special_field:api.GetResourceRequest.special_fields) pub special_fields: ::protobuf::SpecialFields, } @@ -147,14 +147,14 @@ impl ::protobuf::reflect::ProtobufValue for GetResourceRequest { type RuntimeType = ::protobuf::reflect::rt::RuntimeTypeMessage; } -// @@protoc_insertion_point(message:getresource.GetResourceResponse) +// @@protoc_insertion_point(message:api.GetResourceResponse) #[derive(PartialEq,Clone,Default,Debug)] pub struct GetResourceResponse { // message fields - // @@protoc_insertion_point(field:getresource.GetResourceResponse.Resource) + // @@protoc_insertion_point(field:api.GetResourceResponse.Resource) pub Resource: ::std::vec::Vec, // special fields - // @@protoc_insertion_point(special_field:getresource.GetResourceResponse.special_fields) + // @@protoc_insertion_point(special_field:api.GetResourceResponse.special_fields) pub special_fields: ::protobuf::SpecialFields, } @@ -270,11 +270,11 @@ impl ::protobuf::reflect::ProtobufValue for GetResourceResponse { } static file_descriptor_proto_data: &'static [u8] = b"\ - \n\x11getresource.proto\x12\x0bgetresource\"8\n\x12GetResourceRequest\ - \x12\"\n\x0cResourcePath\x18\x01\x20\x01(\tR\x0cResourcePath\"1\n\x13Get\ - ResourceResponse\x12\x1a\n\x08Resource\x18\x01\x20\x01(\x0cR\x08Resource\ - 2f\n\x12GetResourceService\x12P\n\x0bGetResource\x12\x1f.getresource.Get\ - ResourceRequest\x1a\x20.getresource.GetResourceResponseb\x06proto3\ + \n\x11getresource.proto\x12\x03api\"8\n\x12GetResourceRequest\x12\"\n\ + \x0cResourcePath\x18\x01\x20\x01(\tR\x0cResourcePath\"1\n\x13GetResource\ + Response\x12\x1a\n\x08Resource\x18\x01\x20\x01(\x0cR\x08Resource2V\n\x12\ + GetResourceService\x12@\n\x0bGetResource\x12\x17.api.GetResourceRequest\ + \x1a\x18.api.GetResourceResponseb\x06proto3\ "; /// `FileDescriptorProto` object which was a source for this generated file diff --git a/image-rs/src/resource/kbs/ttrpc_proto/getresource_ttrpc.rs b/image-rs/src/resource/kbs/ttrpc_proto/getresource_ttrpc.rs index 40c5e226a..f412ded9b 100644 --- a/image-rs/src/resource/kbs/ttrpc_proto/getresource_ttrpc.rs +++ b/image-rs/src/resource/kbs/ttrpc_proto/getresource_ttrpc.rs @@ -34,7 +34,7 @@ impl GetResourceServiceClient { pub async fn get_resource(&self, ctx: ttrpc::context::Context, req: &super::getresource::GetResourceRequest) -> ::ttrpc::Result { let mut cres = super::getresource::GetResourceResponse::new(); - ::ttrpc::async_client_request!(self, ctx, req, "getresource.GetResourceService", "GetResource", cres); + ::ttrpc::async_client_request!(self, ctx, req, "api.GetResourceService", "GetResource", cres); } } @@ -52,7 +52,7 @@ impl ::ttrpc::r#async::MethodHandler for GetResourceMethod { #[async_trait] pub trait GetResourceService: Sync { async fn get_resource(&self, _ctx: &::ttrpc::r#async::TtrpcContext, _: super::getresource::GetResourceRequest) -> ::ttrpc::Result { - Err(::ttrpc::Error::RpcStatus(::ttrpc::get_status(::ttrpc::Code::NOT_FOUND, "/getresource.GetResourceService/GetResource is not supported".to_string()))) + Err(::ttrpc::Error::RpcStatus(::ttrpc::get_status(::ttrpc::Code::NOT_FOUND, "/api.GetResourceService/GetResource is not supported".to_string()))) } } @@ -64,6 +64,6 @@ pub fn create_get_resource_service(service: Arc); - ret.insert("getresource.GetResourceService".to_string(), ::ttrpc::r#async::Service{ methods, streams }); + ret.insert("api.GetResourceService".to_string(), ::ttrpc::r#async::Service{ methods, streams }); ret } diff --git a/image-rs/test_data/ocicrypt_keyprovider_ttrpc.conf b/image-rs/test_data/ocicrypt_keyprovider_ttrpc.conf index 016c4385c..75df218b6 100644 --- a/image-rs/test_data/ocicrypt_keyprovider_ttrpc.conf +++ b/image-rs/test_data/ocicrypt_keyprovider_ttrpc.conf @@ -1,7 +1,7 @@ { "key-providers": { "attestation-agent": { - "ttrpc": "unix:///run/confidential-containers/attestation-agent/keyprovider.sock" + "ttrpc": "unix:///run/confidential-containers/cdh.sock" } } } diff --git a/image-rs/tests/common/mod.rs b/image-rs/tests/common/mod.rs index ef75572e3..61d02089a 100644 --- a/image-rs/tests/common/mod.rs +++ b/image-rs/tests/common/mod.rs @@ -19,7 +19,7 @@ const OFFLINE_FS_KBC_RESOURCE_SCRIPT: &str = "scripts/install_offline_fs_kbc_fil pub const AA_PARAMETER: &str = "provider:attestation-agent:offline_fs_kbc::null"; /// Attestation Agent Offline Filesystem KBC resources file for general tests that use images stored in the quay.io registry -pub const AA_OFFLINE_FS_KBC_RESOURCES_FILE: &str = "aa-offline_fs_kbc-resources.json"; +pub const OFFLINE_FS_KBC_RESOURCES_FILE: &str = "aa-offline_fs_kbc-resources.json"; /// Attestation Agent Offline Filesystem KBC resources file for XRSS tests #[cfg(feature = "signature-simple-xrss")] @@ -62,70 +62,61 @@ pub async fn clean() { .expect("Clean GPG signature file failed."); } -pub async fn start_attestation_agent() -> Result { +pub async fn start_confidential_data_hub() -> Result { let script_dir = format!("{}/{}", std::env!("CARGO_MANIFEST_DIR"), "scripts"); cfg_if::cfg_if! { if #[cfg(feature = "keywrap-ttrpc")] { - let aa_path = format!("{}/ttrpc/{}", script_dir, "attestation-agent"); + let cdh_path = format!("{}/ttrpc/{}", script_dir, "confidential-data-hub"); } else { - let aa_path = format!("{}/grpc/{}", script_dir, "attestation-agent"); + let cdh_path = format!("{}/grpc/{}", script_dir, "confidential-data-hub"); } }; - println!("aa_path: {}", aa_path); + println!("cdh_path: {}", cdh_path); println!("script_dir: {}", script_dir); - if !Path::new(&aa_path).exists() { - let script_path = format!("{}/{}", script_dir, "build_attestation_agent.sh"); + if !Path::new(&cdh_path).exists() { + let script_path = format!("{}/{}", script_dir, "build_confidential_data_hub.sh"); cfg_if::cfg_if! { if #[cfg(feature = "keywrap-ttrpc")] { let output = Command::new(script_path) .env("TTRPC", "1") .output() .await - .expect("Failed to build attestation-agent"); - println!("build ttrpc attestation-agent: {:?}", output); + .expect("Failed to build confidential-data-hub"); + println!("build ttrpc confidential-data-hub: {:?}", output); } else { let output = Command::new(script_path) .output() .await - .expect("Failed to build attestation-agent"); - println!("build grpc attestation-agent: {:?}", output); + .expect("Failed to build confidential-data-hub"); + println!("build grpc confidential-data-hub: {:?}", output); } } } cfg_if::cfg_if! { if #[cfg(feature = "keywrap-ttrpc")] { - let mut aa = Command::new(aa_path) - .kill_on_drop(true) - .args([ - "--keyprovider_sock", - "unix:///run/confidential-containers/attestation-agent/keyprovider.sock", - "--getresource_sock", - "unix:///run/confidential-containers/attestation-agent/getresource.sock" - ]) - .spawn() - .expect("Failed to start ttrpc attestation-agent"); + let mut cdh = Command::new(cdh_path) + .kill_on_drop(true) + .args(["-s", "unix:///run/confidential-containers/cdh.sock"]) + .spawn() + .expect("Failed to start confidential-data-hub"); } else { - let mut aa = Command::new(aa_path) - .kill_on_drop(true) - .args([ - "--keyprovider_sock", - "127.0.0.1:50000", - "--getresource_sock", - "127.0.0.1:50001" - ]) - .spawn() - .expect("Failed to start grpc attestation-agent"); + // TODO: implement this after CDH supports gRPC + let mut cdh = Command::new(cdh_path) + .kill_on_drop(true) + .args(["-s", "unix:///run/confidential-containers/cdh.sock"]) + .spawn() + .expect("Failed to start confidential-data-hub"); } }; // Leave some time to let fork-ed AA process to be ready tokio::time::sleep(tokio::time::Duration::from_secs(1)).await; - if (aa.try_wait()?).is_some() { - panic!("Attestation Agent failed to start"); + if (cdh.try_wait()?).is_some() { + panic!("Confidential Data Hub failed to start"); } - Ok(aa) + Ok(cdh) } pub fn umount_bundle(bundle_dir: &tempfile::TempDir) { diff --git a/image-rs/tests/credential.rs b/image-rs/tests/credential.rs index 55360ceb5..2075c5157 100644 --- a/image-rs/tests/credential.rs +++ b/image-rs/tests/credential.rs @@ -3,31 +3,29 @@ // SPDX-License-Identifier: Apache-2.0 // -#[cfg(feature = "getresource")] +#[cfg(all(feature = "getresource", feature = "keywrap-ttrpc"))] use image_rs::image::ImageClient; -#[cfg(feature = "getresource")] +#[cfg(all(feature = "getresource", feature = "keywrap-ttrpc"))] use rstest::rstest; -#[cfg(feature = "getresource")] +#[cfg(all(feature = "getresource", feature = "keywrap-ttrpc"))] use serial_test::serial; pub mod common; -#[cfg(feature = "getresource")] +// TODO: add `keywrap-grpc` integration test after CDH supports grpc mode +#[cfg(all(feature = "getresource", feature = "keywrap-ttrpc"))] #[rstest] #[case("liudalibj/private-busy-box", "kbs:///default/credential/test")] #[case("quay.io/liudalibj/private-busy-box", "kbs:///default/credential/test")] #[tokio::test] #[serial] async fn test_use_credential(#[case] image_ref: &str, #[case] auth_file_uri: &str) { - common::prepare_test(common::AA_OFFLINE_FS_KBC_RESOURCES_FILE).await; + common::prepare_test(common::OFFLINE_FS_KBC_RESOURCES_FILE).await; - // Init AA - let _aa = common::start_attestation_agent() + // // Init CDH + let _cdh = common::start_confidential_data_hub() .await - .expect("Failed to start attestation agent!"); - - // AA parameter - let aa_parameters = common::AA_PARAMETER; + .expect("Failed to start confidential data hub!"); // clean former test files, which is needed to prevent // lint from warning dead code. @@ -50,7 +48,12 @@ async fn test_use_credential(#[case] image_ref: &str, #[case] auth_file_uri: &st let bundle_dir = tempfile::tempdir().unwrap(); let res = image_client - .pull_image(image_ref, bundle_dir.path(), &None, &Some(aa_parameters)) + .pull_image( + image_ref, + bundle_dir.path(), + &None, + &Some(common::AA_PARAMETER), + ) .await; if cfg!(all(feature = "snapshot-overlayfs",)) { assert!(res.is_ok(), "{:?}", res); diff --git a/image-rs/tests/image_decryption.rs b/image-rs/tests/image_decryption.rs index 3c59d1a8d..d9e92a552 100644 --- a/image-rs/tests/image_decryption.rs +++ b/image-rs/tests/image_decryption.rs @@ -6,38 +6,54 @@ //! Test for decryption of image layers. -#[cfg(all(feature = "getresource", feature = "encryption"))] +#[cfg(all( + feature = "getresource", + feature = "encryption", + feature = "keywrap-ttrpc" +))] use image_rs::image::ImageClient; -#[cfg(all(feature = "getresource", feature = "encryption"))] +#[cfg(all( + feature = "getresource", + feature = "encryption", + feature = "keywrap-ttrpc" +))] use serial_test::serial; pub mod common; -/// Ocicrypt-rs config for grpc -#[cfg(all(feature = "getresource", feature = "encryption"))] -#[cfg(not(feature = "keywrap-ttrpc"))] -const OCICRYPT_CONFIG: &str = "test_data/ocicrypt_keyprovider_grpc.conf"; +// TODO: add `keywrap-grpc` integration test after CDH supports grpc mode +// /// Ocicrypt-rs config for grpc +// #[cfg(all(feature = "getresource", feature = "encryption"))] +// #[cfg(not(feature = "keywrap-ttrpc"))] +// const OCICRYPT_CONFIG: &str = "test_data/ocicrypt_keyprovider_grpc.conf"; /// Ocicrypt-rs config for ttrpc -#[cfg(all(feature = "getresource", feature = "encryption"))] -#[cfg(feature = "keywrap-ttrpc")] +#[cfg(all( + feature = "getresource", + feature = "encryption", + feature = "keywrap-ttrpc" +))] const OCICRYPT_CONFIG: &str = "test_data/ocicrypt_keyprovider_ttrpc.conf"; -#[cfg(all(feature = "getresource", feature = "encryption"))] +#[cfg(all( + feature = "getresource", + feature = "encryption", + feature = "keywrap-ttrpc" +))] #[rstest::rstest] #[case("ghcr.io/confidential-containers/test-container:unencrypted")] #[case("ghcr.io/confidential-containers/test-container:encrypted")] #[tokio::test] #[serial] async fn test_decrypt_layers(#[case] image: &str) { - common::prepare_test(common::AA_OFFLINE_FS_KBC_RESOURCES_FILE).await; - // Init AA - let _aa = common::start_attestation_agent() + common::prepare_test(common::OFFLINE_FS_KBC_RESOURCES_FILE).await; + // Init CDH + let _cdh = common::start_confidential_data_hub() .await - .expect("Failed to start attestation agent!"); + .expect("Failed to start confidential data hub!"); // Set env for ocicrypt-rs. The env is needed by ocicrypt-rs - // to communicate with AA + // to communicate with CDH let manifest_dir = std::env!("CARGO_MANIFEST_DIR"); let keyprovider_config = format!("{}/{}", manifest_dir, OCICRYPT_CONFIG); std::env::set_var("OCICRYPT_KEYPROVIDER_CONFIG", keyprovider_config); diff --git a/image-rs/tests/signature_verification.rs b/image-rs/tests/signature_verification.rs index 223c6bc51..9a2149797 100644 --- a/image-rs/tests/signature_verification.rs +++ b/image-rs/tests/signature_verification.rs @@ -6,9 +6,9 @@ //! Test for signature verification. -#[cfg(feature = "getresource")] +#[cfg(all(feature = "getresource", feature = "keywrap-ttrpc"))] use image_rs::image::ImageClient; -#[cfg(feature = "getresource")] +#[cfg(all(feature = "getresource", feature = "keywrap-ttrpc"))] use serial_test::serial; use strum_macros::{Display, EnumString}; @@ -105,23 +105,27 @@ const _TESTS_XRSS: [_TestItem; _TEST_ITEMS_XRSS] = [ }, ]; -#[cfg(feature = "getresource")] +#[cfg(all(feature = "getresource", feature = "keywrap-ttrpc"))] const POLICY_URI: &str = "kbs:///default/security-policy/test"; -#[cfg(feature = "getresource")] +#[cfg(all(feature = "getresource", feature = "keywrap-ttrpc"))] const SIGSTORE_CONFIG_URI: &str = "kbs:///default/sigstore-config/test"; /// image-rs built without support for cosign image signing cannot use a policy that includes a type that /// uses cosign (type: sigstoreSigned), even if the image being pulled is not signed using cosign. /// https://github.com/confidential-containers/guest-components/blob/main/attestation-agent/kbc/src/sample_kbc/policy.json -#[cfg(feature = "getresource")] +#[cfg(all(feature = "getresource", feature = "keywrap-ttrpc"))] #[tokio::test] #[serial] async fn signature_verification() { - do_signature_verification_tests(&_TESTS, common::AA_OFFLINE_FS_KBC_RESOURCES_FILE, &None).await; + do_signature_verification_tests(&_TESTS, common::OFFLINE_FS_KBC_RESOURCES_FILE, &None).await; } -#[cfg(all(feature = "signature-simple-xrss", feature = "getresource"))] +#[cfg(all( + feature = "signature-simple-xrss", + feature = "getresource", + feature = "keywrap-ttrpc" +))] #[tokio::test] #[serial] async fn signature_verification_xrss() { @@ -147,17 +151,17 @@ async fn signature_verification_xrss() { } } -#[cfg(feature = "getresource")] +#[cfg(all(feature = "getresource", feature = "keywrap-ttrpc"))] async fn do_signature_verification_tests( tests: &[_TestItem<'_, '_>], offline_fs_kbc_resources: &str, auth_info: &Option<&str>, ) { common::prepare_test(offline_fs_kbc_resources).await; - // Init AA - let _aa = common::start_attestation_agent() + // Init CDH + let _cdh = common::start_confidential_data_hub() .await - .expect("Failed to start attestation agent!"); + .expect("Failed to start confidential data hub!"); for test in tests { let mut test_auth_info = auth_info; diff --git a/ocicrypt-rs/src/keywrap/keyprovider/mod.rs b/ocicrypt-rs/src/keywrap/keyprovider/mod.rs index 02279a3b6..9f5b31f6a 100644 --- a/ocicrypt-rs/src/keywrap/keyprovider/mod.rs +++ b/ocicrypt-rs/src/keywrap/keyprovider/mod.rs @@ -150,9 +150,9 @@ impl KeyProviderKeyWrapProtocolOutput { OpKey::Unwrap => kc1 .un_wrap_key(ttrpc::context::with_timeout(50 * 1000 * 1000 * 1000), &req) .await - .map_err(|_| { + .map_err(|e| { anyhow!( - "keyprovider: Error from ttrpc server for {:?} operation", + "keyprovider: Error from ttrpc server for {:?} operation: {e:?}", OpKey::Unwrap.to_string() ) })?, @@ -440,8 +440,8 @@ impl KeyProviderKeyWrapper { }); match handler.join() { Ok(Ok(v)) => Ok(v), - Ok(Err(e)) => bail!("failed to unwrap key by gRPC, {e}"), - Err(e) => bail!("failed to unwrap key by gRPC, {e:?}"), + Ok(Err(e)) => bail!("failed to unwrap key by ttrpc, {e}"), + Err(e) => bail!("failed to unwrap key by ttrpc, {e:?}"), } } }