From b95093d6406f95ee4a3300a058a3b909fd459c72 Mon Sep 17 00:00:00 2001 From: David Drysdale Date: Thu, 11 Jan 2024 19:26:57 +0000 Subject: [PATCH] Secretkeeper: add test CLI Allows testing of secret persistence across reboot (and non-persistence across factory reset). Move some test code into a library for re-use. Test: Manual Change-Id: I23772692d2de652f6d4a8e5659186bd9c1c06b72 --- security/secretkeeper/aidl/vts/Android.bp | 45 ++- security/secretkeeper/aidl/vts/lib.rs | 34 ++ .../secretkeeper/aidl/vts/secretkeeper_cli.rs | 347 ++++++++++++++++++ .../aidl/vts/secretkeeper_test_client.rs | 24 +- 4 files changed, 429 insertions(+), 21 deletions(-) create mode 100644 security/secretkeeper/aidl/vts/lib.rs create mode 100644 security/secretkeeper/aidl/vts/secretkeeper_cli.rs diff --git a/security/secretkeeper/aidl/vts/Android.bp b/security/secretkeeper/aidl/vts/Android.bp index 720b8a2479..9d1701a303 100644 --- a/security/secretkeeper/aidl/vts/Android.bp +++ b/security/secretkeeper/aidl/vts/Android.bp @@ -18,6 +18,19 @@ package { default_applicable_licenses: ["Android-Apache-2.0"], } +rust_library { + name: "libsecretkeeper_test", + crate_name: "secretkeeper_test", + srcs: ["lib.rs"], + rustlibs: [ + "libciborium", + "libcoset", + "libdiced_open_dice", + "liblog_rust", + "libsecretkeeper_client", + ], +} + rust_test { name: "VtsSecretkeeperTargetTest", srcs: ["secretkeeper_test_client.rs"], @@ -30,20 +43,40 @@ rust_test { ], test_config: "AndroidTest.xml", rustlibs: [ - "libdiced_open_dice", - "libdice_policy", - "libsecretkeeper_client", - "libsecretkeeper_comm_nostd", - "libsecretkeeper_core_nostd", "android.hardware.security.secretkeeper-V1-rust", "libauthgraph_boringssl", "libauthgraph_core", - "libcoset", "libauthgraph_vts_test", "libbinder_rs", "libciborium", "libcoset", + "libdice_policy", "liblog_rust", + "libsecretkeeper_client", + "libsecretkeeper_comm_nostd", + "libsecretkeeper_core_nostd", + "libsecretkeeper_test", ], require_root: true, } + +rust_binary { + name: "secretkeeper_cli", + srcs: ["secretkeeper_cli.rs"], + lints: "android", + rlibs: [ + "android.hardware.security.secretkeeper-V1-rust", + "libanyhow", + "libauthgraph_boringssl", + "libauthgraph_core", + "libbinder_rs", + "libclap", + "libcoset", + "libdice_policy", + "libhex", + "liblog_rust", + "libsecretkeeper_client", + "libsecretkeeper_comm_nostd", + "libsecretkeeper_test", + ], +} diff --git a/security/secretkeeper/aidl/vts/lib.rs b/security/secretkeeper/aidl/vts/lib.rs new file mode 100644 index 0000000000..9f98165bd3 --- /dev/null +++ b/security/secretkeeper/aidl/vts/lib.rs @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2023 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. + */ + +//! Test helper functions. + +pub mod dice_sample; + +// Constants for DICE map keys. + +/// Map key for authority hash. +pub const AUTHORITY_HASH: i64 = -4670549; +/// Map key for config descriptor. +pub const CONFIG_DESC: i64 = -4670548; +/// Map key for component name. +pub const COMPONENT_NAME: i64 = -70002; +/// Map key for component version. +pub const COMPONENT_VERSION: i64 = -70003; +/// Map key for security version. +pub const SECURITY_VERSION: i64 = -70005; +/// Map key for mode. +pub const MODE: i64 = -4670551; diff --git a/security/secretkeeper/aidl/vts/secretkeeper_cli.rs b/security/secretkeeper/aidl/vts/secretkeeper_cli.rs new file mode 100644 index 0000000000..5f0848252b --- /dev/null +++ b/security/secretkeeper/aidl/vts/secretkeeper_cli.rs @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2023 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. + */ + +//! Command line test tool for interacting with Secretkeeper. + +use android_hardware_security_secretkeeper::aidl::android::hardware::security::secretkeeper::{ + ISecretkeeper::ISecretkeeper, SecretId::SecretId, +}; +use anyhow::{anyhow, bail, Context, Result}; +use authgraph_boringssl::BoringSha256; +use authgraph_core::traits::Sha256; +use clap::{Args, Parser, Subcommand}; +use coset::CborSerializable; +use dice_policy::{ConstraintSpec, ConstraintType, DicePolicy, MissingAction}; +use secretkeeper_client::{dice::OwnedDiceArtifactsWithExplicitKey, SkSession}; +use secretkeeper_comm::data_types::{ + error::SecretkeeperError, + packet::{ResponsePacket, ResponseType}, + request::Request, + request_response_impl::{GetSecretRequest, GetSecretResponse, StoreSecretRequest}, + response::Response, + {Id, Secret}, +}; +use secretkeeper_test::{ + dice_sample::make_explicit_owned_dice, AUTHORITY_HASH, CONFIG_DESC, MODE, SECURITY_VERSION, +}; +use std::io::Write; + +#[derive(Parser, Debug)] +#[command(about = "Interact with Secretkeeper HAL")] +#[command(version = "0.1")] +#[command(propagate_version = true)] +struct Cli { + #[command(subcommand)] + command: Command, + + /// Secretkeeper instance to connect to. + #[arg(long, short)] + instance: Option, + + /// Security version in leaf DICE node. + #[clap(default_value_t = 100)] + #[arg(long, short = 'v')] + dice_version: u64, + + /// Show hex versions of secrets and their IDs. + #[clap(default_value_t = false)] + #[arg(long, short = 'v')] + hex: bool, +} + +#[derive(Subcommand, Debug)] +enum Command { + /// Store a secret value. + Store(StoreArgs), + /// Get a secret value. + Get(GetArgs), + /// Delete a secret value. + Delete(DeleteArgs), + /// Delete all secret values. + DeleteAll(DeleteAllArgs), +} + +#[derive(Args, Debug)] +struct StoreArgs { + /// Identifier for the secret, as either a short (< 32 byte) string, or as 32 bytes of hex. + id: String, + /// Value to use as the secret value. If specified as 32 bytes of hex, the decoded value + /// will be used as-is; otherwise, a string (less than 31 bytes in length) will be encoded + /// as the secret. + value: String, +} + +#[derive(Args, Debug)] +struct GetArgs { + /// Identifier for the secret, as either a short (< 32 byte) string, or as 32 bytes of hex. + id: String, +} + +#[derive(Args, Debug)] +struct DeleteArgs { + /// Identifier for the secret, as either a short (< 32 byte) string, or as 32 bytes of hex. + id: String, +} + +#[derive(Args, Debug)] +struct DeleteAllArgs { + /// Confirm deletion of all secrets. + yes: bool, +} + +const SECRETKEEPER_SERVICE: &str = "android.hardware.security.secretkeeper.ISecretkeeper"; + +/// Secretkeeper client information. +struct SkClient { + sk: binder::Strong, + session: SkSession, + dice_artifacts: OwnedDiceArtifactsWithExplicitKey, +} + +impl SkClient { + fn new(instance: &str, dice_artifacts: OwnedDiceArtifactsWithExplicitKey) -> Self { + let sk: binder::Strong = + binder::get_interface(&format!("{SECRETKEEPER_SERVICE}/{instance}")).unwrap(); + let session = SkSession::new(sk.clone(), &dice_artifacts).unwrap(); + Self { sk, session, dice_artifacts } + } + + fn secret_management_request(&mut self, req_data: &[u8]) -> Result> { + self.session + .secret_management_request(req_data) + .map_err(|e| anyhow!("secret management: {e:?}")) + } + + /// Construct a sealing policy on the DICE chain with constraints: + /// 1. `ExactMatch` on `AUTHORITY_HASH` (non-optional). + /// 2. `ExactMatch` on `MODE` (non-optional). + /// 3. `GreaterOrEqual` on `SECURITY_VERSION` (optional). + fn sealing_policy(&self) -> Result> { + let dice = + self.dice_artifacts.explicit_key_dice_chain().context("extract explicit DICE chain")?; + + let constraint_spec = [ + ConstraintSpec::new( + ConstraintType::ExactMatch, + vec![AUTHORITY_HASH], + MissingAction::Fail, + ), + ConstraintSpec::new(ConstraintType::ExactMatch, vec![MODE], MissingAction::Fail), + ConstraintSpec::new( + ConstraintType::GreaterOrEqual, + vec![CONFIG_DESC, SECURITY_VERSION], + MissingAction::Ignore, + ), + ]; + DicePolicy::from_dice_chain(dice, &constraint_spec) + .unwrap() + .to_vec() + .context("serialize DICE policy") + } + + fn store(&mut self, id: &Id, secret: &Secret) -> Result<()> { + let store_request = StoreSecretRequest { + id: id.clone(), + secret: secret.clone(), + sealing_policy: self.sealing_policy().context("build sealing policy")?, + }; + let store_request = + store_request.serialize_to_packet().to_vec().context("serialize StoreSecretRequest")?; + + let store_response = self.secret_management_request(&store_request)?; + let store_response = + ResponsePacket::from_slice(&store_response).context("deserialize ResponsePacket")?; + let response_type = store_response.response_type().unwrap(); + if response_type == ResponseType::Success { + Ok(()) + } else { + let err = *SecretkeeperError::deserialize_from_packet(store_response).unwrap(); + Err(anyhow!("STORE failed: {err:?}")) + } + } + + fn get(&mut self, id: &Id) -> Result> { + let get_request = GetSecretRequest { id: id.clone(), updated_sealing_policy: None } + .serialize_to_packet() + .to_vec() + .context("serialize GetSecretRequest")?; + + let get_response = self.secret_management_request(&get_request).context("secret mgmt")?; + let get_response = + ResponsePacket::from_slice(&get_response).context("deserialize ResponsePacket")?; + + if get_response.response_type().unwrap() == ResponseType::Success { + let get_response = *GetSecretResponse::deserialize_from_packet(get_response).unwrap(); + Ok(Some(Secret(get_response.secret.0))) + } else { + // Only expect a not-found failure. + let err = *SecretkeeperError::deserialize_from_packet(get_response).unwrap(); + if err == SecretkeeperError::EntryNotFound { + Ok(None) + } else { + Err(anyhow!("GET failed: {err:?}")) + } + } + } + + /// Helper method to delete secrets. + fn delete(&self, ids: &[&Id]) -> Result<()> { + let ids: Vec = ids.iter().map(|id| SecretId { id: id.0 }).collect(); + self.sk.deleteIds(&ids).context("deleteIds") + } + + /// Helper method to delete everything. + fn delete_all(&self) -> Result<()> { + self.sk.deleteAll().context("deleteAll") + } +} + +/// Convert a string input into an `Id`. Input can be 64 bytes of hex, or a string +/// that will be hashed to give the `Id` value. Returns the `Id` and a display string. +fn string_to_id(s: &str, show_hex: bool) -> (Id, String) { + if let Ok(data) = hex::decode(s) { + if data.len() == 64 { + // Assume something that parses as 64 bytes of hex is it. + return (Id(data.try_into().unwrap()), s.to_string().to_lowercase()); + } + } + // Create a secret ID by repeating the SHA-256 hash of the string twice. + let hash = BoringSha256.compute_sha256(s.as_bytes()).unwrap(); + let mut id = Id([0; 64]); + id.0[..32].copy_from_slice(&hash); + id.0[32..].copy_from_slice(&hash); + if show_hex { + let hex_id = hex::encode(&id.0); + (id, format!("'{s}' (as {hex_id})")) + } else { + (id, format!("'{s}'")) + } +} + +/// Convert a string input into a `Secret`. Input can be 32 bytes of hex, or a short string +/// that will be encoded as the `Secret` value. Returns the `Secret` and a display string. +fn value_to_secret(s: &str, show_hex: bool) -> Result<(Secret, String)> { + if let Ok(data) = hex::decode(s) { + if data.len() == 32 { + // Assume something that parses as 32 bytes of hex is it. + return Ok((Secret(data.try_into().unwrap()), s.to_string().to_lowercase())); + } + } + let data = s.as_bytes(); + if data.len() > 31 { + return Err(anyhow!("secret too long")); + } + let mut secret = Secret([0; 32]); + secret.0[0] = data.len() as u8; + secret.0[1..1 + data.len()].copy_from_slice(data); + Ok(if show_hex { + let hex_secret = hex::encode(&secret.0); + (secret, format!("'{s}' (as {hex_secret})")) + } else { + (secret, format!("'{s}'")) + }) +} + +/// Convert a `Secret` into a displayable string. If the secret looks like an encoded +/// string, show that, otherwise show the value in hex. +fn secret_to_value_display(secret: &Secret, show_hex: bool) -> String { + let hex = hex::encode(&secret.0); + secret_to_value(secret) + .map(|s| if show_hex { format!("'{s}' (from {hex})") } else { format!("'{s}'") }) + .unwrap_or_else(|_e| format!("{hex}")) +} + +/// Attempt to convert a `Secret` back to a string. +fn secret_to_value(secret: &Secret) -> Result { + let len = secret.0[0] as usize; + if len > 31 { + return Err(anyhow!("too long")); + } + std::str::from_utf8(&secret.0[1..1 + len]).map(|s| s.to_string()).context("not UTF-8 string") +} + +fn main() -> Result<()> { + let cli = Cli::parse(); + + // Figure out which Secretkeeper instance is desired, and connect to it. + let instance = if let Some(instance) = &cli.instance { + // Explicitly specified. + instance.clone() + } else { + // If there's only one instance, use that. + let instances: Vec = binder::get_declared_instances(SECRETKEEPER_SERVICE) + .unwrap_or_default() + .into_iter() + .collect(); + match instances.len() { + 0 => bail!("No Secretkeeper instances available on device!"), + 1 => instances[0].clone(), + _ => { + bail!( + concat!( + "Multiple Secretkeeper instances available on device: {}\n", + "Use --instance to specify one." + ), + instances.join(", ") + ); + } + } + }; + let dice = make_explicit_owned_dice(cli.dice_version); + let mut sk_client = SkClient::new(&instance, dice); + + match cli.command { + Command::Get(args) => { + let (id, display_id) = string_to_id(&args.id, cli.hex); + print!("GET key {display_id}: "); + match sk_client.get(&id).context("GET") { + Ok(None) => println!("not found"), + Ok(Some(s)) => println!("{}", secret_to_value_display(&s, cli.hex)), + Err(e) => { + println!("failed!"); + return Err(e); + } + } + } + Command::Store(args) => { + let (id, display_id) = string_to_id(&args.id, cli.hex); + let (secret, display_secret) = value_to_secret(&args.value, cli.hex)?; + println!("STORE key {display_id}: {display_secret}"); + sk_client.store(&id, &secret).context("STORE")?; + } + Command::Delete(args) => { + let (id, display_id) = string_to_id(&args.id, cli.hex); + println!("DELETE key {display_id}"); + sk_client.delete(&[&id]).context("DELETE")?; + } + Command::DeleteAll(args) => { + if !args.yes { + // Request confirmation. + println!("Confirm delete all secrets: [y/N]"); + let _ = std::io::stdout().flush(); + let mut input = String::new(); + std::io::stdin().read_line(&mut input)?; + let c = input.chars().next(); + if c != Some('y') && c != Some('Y') { + bail!("DELETE_ALL not confirmed"); + } + } + println!("DELETE_ALL"); + sk_client.delete_all().context("DELETE_ALL")?; + } + } + Ok(()) +} diff --git a/security/secretkeeper/aidl/vts/secretkeeper_test_client.rs b/security/secretkeeper/aidl/vts/secretkeeper_test_client.rs index df916d5fa9..8c33f0412d 100644 --- a/security/secretkeeper/aidl/vts/secretkeeper_test_client.rs +++ b/security/secretkeeper/aidl/vts/secretkeeper_test_client.rs @@ -14,12 +14,6 @@ * limitations under the License. */ -#![cfg(test)] -mod dice_sample; - -use crate::dice_sample::make_explicit_owned_dice; - -use rdroidtest::{ignore_if, rdroidtest}; use android_hardware_security_secretkeeper::aidl::android::hardware::security::secretkeeper::ISecretkeeper::ISecretkeeper; use android_hardware_security_secretkeeper::aidl::android::hardware::security::secretkeeper::SecretId::SecretId; use authgraph_vts_test as ag_vts; @@ -27,6 +21,7 @@ use authgraph_boringssl as boring; use authgraph_core::key; use coset::{CborSerializable, CoseEncrypt0}; use dice_policy::{ConstraintSpec, ConstraintType, DicePolicy, MissingAction}; +use rdroidtest::{ignore_if, rdroidtest}; use secretkeeper_client::dice::OwnedDiceArtifactsWithExplicitKey; use secretkeeper_client::SkSession; use secretkeeper_core::cipher; @@ -38,6 +33,10 @@ use secretkeeper_comm::data_types::request_response_impl::{ use secretkeeper_comm::data_types::{Id, Secret, SeqNum}; use secretkeeper_comm::data_types::response::Response; use secretkeeper_comm::data_types::packet::{ResponsePacket, ResponseType}; +use secretkeeper_test::{ + AUTHORITY_HASH, MODE, CONFIG_DESC, SECURITY_VERSION, + dice_sample::make_explicit_owned_dice +}; const SECRETKEEPER_SERVICE: &str = "android.hardware.security.secretkeeper.ISecretkeeper"; const CURRENT_VERSION: u64 = 1; @@ -246,20 +245,15 @@ fn assert_entry_not_found(res: Result) { /// Construct a sealing policy on the dice chain. This method uses the following set of /// constraints which are compatible with sample DICE chains used in VTS. /// 1. ExactMatch on AUTHORITY_HASH (non-optional). -/// 2. ExactMatch on KEY_MODE (non-optional). +/// 2. ExactMatch on MODE (non-optional). /// 3. GreaterOrEqual on SECURITY_VERSION (optional). fn sealing_policy(dice: &[u8]) -> Vec { - let authority_hash: i64 = -4670549; - let key_mode: i64 = -4670551; - let config_desc: i64 = -4670548; - let security_version: i64 = -70005; - let constraint_spec = [ - ConstraintSpec::new(ConstraintType::ExactMatch, vec![authority_hash], MissingAction::Fail), - ConstraintSpec::new(ConstraintType::ExactMatch, vec![key_mode], MissingAction::Fail), + ConstraintSpec::new(ConstraintType::ExactMatch, vec![AUTHORITY_HASH], MissingAction::Fail), + ConstraintSpec::new(ConstraintType::ExactMatch, vec![MODE], MissingAction::Fail), ConstraintSpec::new( ConstraintType::GreaterOrEqual, - vec![config_desc, security_version], + vec![CONFIG_DESC, SECURITY_VERSION], MissingAction::Ignore, ), ];