From b36aa43b3a27a97cedf44b69086d3bf5cdfa980b Mon Sep 17 00:00:00 2001 From: Vivian Roest Date: Tue, 19 Dec 2023 09:00:07 +0100 Subject: [PATCH] tpm2 working --- Cargo.lock | 140 ++++++++++------ Cargo.toml | 15 +- flake.nix | 67 +++++--- result | 1 + src/main.rs | 36 ++-- src/tpm/mod.rs | 216 ++++++++++++++++++++++++ src/tpm/tpm_objects.rs | 366 +++++++++++++++++++++++++++++++++++++++++ src/tpm/utils.rs | 110 +++++++++++++ 8 files changed, 862 insertions(+), 89 deletions(-) create mode 120000 result create mode 100644 src/tpm/mod.rs create mode 100644 src/tpm/tpm_objects.rs create mode 100644 src/tpm/utils.rs diff --git a/Cargo.lock b/Cargo.lock index dd9bd14..62d85a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,17 +80,6 @@ version = "1.0.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -118,12 +107,6 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" -[[package]] -name = "base64" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" - [[package]] name = "base64" version = "0.21.5" @@ -136,7 +119,7 @@ version = "0.66.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2b84e06fc203107bfbad243f4aba2af864eb7db3b1cf46ea0a023b0b433d2a7" dependencies = [ - "bitflags", + "bitflags 2.4.1", "cexpr", "clang-sys", "lazy_static", @@ -159,6 +142,12 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2d7e60934ceec538daadb9d8432424ed043a904d8e0243f3c6446bce549a46ac" +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.4.1" @@ -240,20 +229,6 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" -[[package]] -name = "clevis-pin-tpm2" -version = "0.5.3" -dependencies = [ - "anyhow", - "atty", - "base64 0.12.3", - "josekit", - "serde", - "serde_json", - "tpm2-policy", - "tss-esapi", -] - [[package]] name = "color-eyre" version = "0.6.2" @@ -305,6 +280,27 @@ dependencies = [ "powerfmt", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + [[package]] name = "either" version = "1.9.0" @@ -382,6 +378,17 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + [[package]] name = "gimli" version = "0.28.1" @@ -398,12 +405,16 @@ checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" name = "gnome-autounlock-keyring" version = "0.1.0" dependencies = [ + "base64 0.21.5", "clap", - "clevis-pin-tpm2", "color-eyre", + "dirs", + "josekit", "rpassword", "serde", "serde_json", + "tpm2-policy", + "tss-esapi", ] [[package]] @@ -418,15 +429,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - [[package]] name = "home" version = "0.5.9" @@ -466,16 +468,17 @@ checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" [[package]] name = "josekit" -version = "0.7.4" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16e84ea7acc05b40e2fe6fa02a54b3731323c77e6015c36749f0b10c4dbbc32f" +checksum = "5754487a088f527b1407df470db8e654e4064dccbbe1fe850e0773721e9962b7" dependencies = [ "anyhow", - "base64 0.13.1", + "base64 0.21.5", "flate2", "once_cell", "openssl", "regex", + "serde", "serde_json", "thiserror", "time", @@ -509,6 +512,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "libredox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +dependencies = [ + "bitflags 2.4.1", + "libc", + "redox_syscall", +] + [[package]] name = "linux-raw-sys" version = "0.4.12" @@ -613,7 +627,7 @@ version = "0.10.61" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b8419dc8cc6d866deb801274bba2e6f8f6108c1bb7fcc10ee5ab864931dbb45" dependencies = [ - "bitflags", + "bitflags 2.4.1", "cfg-if", "foreign-types", "libc", @@ -645,6 +659,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + [[package]] name = "owo-colors" version = "3.5.0" @@ -749,6 +769,26 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + [[package]] name = "regex" version = "1.10.2" @@ -826,7 +866,7 @@ version = "0.38.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" dependencies = [ - "bitflags", + "bitflags 2.4.1", "errno", "libc", "linux-raw-sys", @@ -1105,6 +1145,12 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + [[package]] name = "which" version = "4.4.2" diff --git a/Cargo.toml b/Cargo.toml index 9020bf0..97be2f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,8 +7,15 @@ edition = "2021" [dependencies] color-eyre = "0.6.2" -serde_json = "1.0.108" -serde = { version = "1.0.193", features = ["derive"] } -clevis-pin-tpm2 = { path = "../clevis-pin-tpm2" } +serde_json = "1" +serde = { version = "1", features = ["derive"] } clap = { version = "4", features = ["derive"] } -rpassword = "7.3.1" \ No newline at end of file +rpassword = "7.3.1" +tss-esapi = { version = "7.2", features = ["generate-bindings"] } +josekit = "0.8.4" +base64 = { version = "0.21.5", features = [] } +tpm2-policy = "0.6.0" +dirs = "5" + +[profile.release] +lto = "thin" diff --git a/flake.nix b/flake.nix index ec6ef15..fdb20f9 100644 --- a/flake.nix +++ b/flake.nix @@ -7,40 +7,55 @@ outputs = { self, nixpkgs, flake-utils }: flake-utils.lib.eachDefaultSystem (system: let + cargoToml = (builtins.fromTOML (builtins.readFile ./Cargo.toml)); pkgs = nixpkgs.legacyPackages.${system}; inherit (pkgs) stdenv lib; - in { - devShell = pkgs.mkShell { - shellHook = '' - export BINDGEN_EXTRA_CLANG_ARGS="$(< ${stdenv.cc}/nix-support/libc-crt1-cflags) \ - $(< ${stdenv.cc}/nix-support/libc-cflags) \ - $(< ${stdenv.cc}/nix-support/cc-cflags) \ - $(< ${stdenv.cc}/nix-support/libcxx-cxxflags) \ - ${ - lib.optionalString stdenv.cc.isClang - "-idirafter ${stdenv.cc.cc}/lib/clang/${ - lib.getVersion stdenv.cc.cc - }/include" - } \ - ${ - lib.optionalString stdenv.cc.isGNU - "-isystem ${stdenv.cc.cc}/include/c++/${ - lib.getVersion stdenv.cc.cc - } -isystem ${stdenv.cc.cc}/include/c++/${ - lib.getVersion stdenv.cc.cc - }/${stdenv.hostPlatform.config} -idirafter ${stdenv.cc.cc}/lib/gcc/${stdenv.hostPlatform.config}/${ - lib.getVersion stdenv.cc.cc - }/include" - } \ - " - ''; + in rec { + packages.default = pkgs.rustPlatform.buildRustPackage { + pname = cargoToml.package.name; + version = cargoToml.package.version; + src = self; + cargoLock.lockFile = ./Cargo.lock; + + doCheck = false; + nativeBuildInputs = with pkgs; [ llvmPackages.libclang llvmPackages.libcxxClang clang + pkg-config ]; LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; - buildInputs = with pkgs; [ libclang pkg-config openssl tpm2-tss ]; + buildInputs = with pkgs; [ openssl tpm2-tss ]; + + preBuild = '' + export BINDGEN_EXTRA_CLANG_ARGS="$(< ${stdenv.cc}/nix-support/libc-crt1-cflags) \ + $(< ${stdenv.cc}/nix-support/libc-cflags) \ + $(< ${stdenv.cc}/nix-support/cc-cflags) \ + $(< ${stdenv.cc}/nix-support/libcxx-cxxflags) \ + ${ + lib.optionalString stdenv.cc.isClang + "-idirafter ${stdenv.cc.cc}/lib/clang/${ + lib.getVersion stdenv.cc.cc + }/include" + } \ + ${ + lib.optionalString stdenv.cc.isGNU + "-isystem ${stdenv.cc.cc}/include/c++/${ + lib.getVersion stdenv.cc.cc + } -isystem ${stdenv.cc.cc}/include/c++/${ + lib.getVersion stdenv.cc.cc + }/${stdenv.hostPlatform.config} -idirafter ${stdenv.cc.cc}/lib/gcc/${stdenv.hostPlatform.config}/${ + lib.getVersion stdenv.cc.cc + }/include" + } \ + " + ''; + }; + + devShell = pkgs.mkShell { + shellHook = "${packages.default.preBuild}"; + inherit (packages.default) nativeBuildInputs buildInputs LIBCLANG_PATH; }; }); } diff --git a/result b/result new file mode 120000 index 0000000..b5c3c34 --- /dev/null +++ b/result @@ -0,0 +1 @@ +/nix/store/bwvjqn0n9xljxk646l0rqj6m0ym221m7-gnome-autounlock-keyring-0.1.0 \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index a332885..54bcc66 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,14 @@ +mod tpm; + use clap::{Parser, Subcommand}; -use clevis_pin_tpm2::tpm_objects::TPM2Config; use color_eyre::eyre::{bail, eyre, WrapErr}; use std::env; use std::fs::{read_to_string, File}; -use std::io::{stdin, Read, Write}; +use std::io::{Read, Write}; use std::os::unix::net::UnixStream; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; use std::process::exit; +use tpm::tpm_objects::TPM2Config; fn get_control_socket() -> Option { let gnome_var = env::var("GNOME_KEYRING_CONTROL") @@ -22,6 +24,7 @@ fn get_control_socket() -> Option { gnome_var.or(xdg_var) } +#[derive(Debug, Clone, Copy)] enum ControlOp { Initialize = 0, Unlock = 1, @@ -108,13 +111,19 @@ fn unlock_keyring(password: &[u8]) -> color_eyre::Result { #[derive(Parser)] struct Cli { + /// Defaults to CONFIG_DIR/gnome-keyring.tpm2 + #[arg(short, long)] + token_path: Option, + #[command(subcommand)] command: Commands, } #[derive(Subcommand)] enum Commands { + /// Unlock gnome keyring using encrypted password stored in tpm Unlock, + /// Enroll a password into the tpm to use when unlocking Enroll, } @@ -122,13 +131,17 @@ fn main() -> color_eyre::Result<()> { color_eyre::install().unwrap(); let cli = Cli::parse(); + let token_path = cli + .token_path + .or(dirs::config_dir().map(|el| el.join("gnome-keyring.tpm2"))) + .ok_or_else(|| eyre!("Token path not found"))?; + match cli.command { Commands::Unlock => { - let file = PathBuf::from("/home/vivian/.config/gnome_password.token"); - if file.exists() { - let token = read_to_string(file)?; - let password = clevis_pin_tpm2::perform_decrypt(token.as_bytes()) - .map_err(|err| eyre!("{err:?}"))?; + if token_path.exists() { + let token = read_to_string(token_path)?; + let password = + tpm::perform_decrypt(token.as_bytes()).map_err(|err| eyre!("{err:?}"))?; let res = unlock_keyring(password.as_slice())?; if res != ControlResult::Ok { eprintln!("Failed to unlock keyring: {res:?}"); @@ -147,10 +160,9 @@ fn main() -> color_eyre::Result<()> { exit(3); } - let token = - clevis_pin_tpm2::perform_encrypt(TPM2Config::default(), password.as_bytes()) - .map_err(|err| eyre!("{err:?}"))?; - let mut file = File::create("/home/vivian/.config/gnome_password.token")?; + let token = tpm::perform_encrypt(TPM2Config::default(), password.as_bytes()) + .map_err(|err| eyre!("{err:?}"))?; + let mut file = File::create(token_path)?; file.write_all(token.as_bytes())?; println!("Password enrolled successfully") } diff --git a/src/tpm/mod.rs b/src/tpm/mod.rs new file mode 100644 index 0000000..6210eee --- /dev/null +++ b/src/tpm/mod.rs @@ -0,0 +1,216 @@ +// Copyright 2020 Patrick Uiterwijk +// +// Licensed under the MIT license + +use std::convert::{TryFrom, TryInto}; + +use color_eyre::{ + eyre::{bail, Context, ContextCompat, Error}, + Result, +}; +use josekit::jwe::{alg::direct::DirectJweAlgorithm::Dir, enc::A256GCM}; +use serde::{Deserialize, Serialize}; +use tpm2_policy::TPMPolicyStep; +use tpm_objects::TPM2Config; +use tss_esapi::structures::SensitiveData; + +pub mod tpm_objects; +pub mod utils; + +pub fn perform_encrypt(cfg: TPM2Config, input: &[u8]) -> Result { + let key_type = match &cfg.key { + None => "ecc", + Some(key_type) => key_type, + }; + let key_public = tpm_objects::get_key_public(key_type, cfg.get_name_hash_alg())?; + + let mut ctx = utils::get_tpm2_ctx()?; + let key_handle = utils::get_tpm2_primary_key(&mut ctx, key_public)?; + + let policy_runner: TPMPolicyStep = TPMPolicyStep::try_from(&cfg)?; + + let pin_type = match policy_runner { + TPMPolicyStep::NoStep => "tpm2", + TPMPolicyStep::PCRs(_, _, _) => "tpm2", + _ => "tpm2plus", + }; + + let (_, policy_digest) = policy_runner.send_policy(&mut ctx, true)?; + + let mut jwk = josekit::jwk::Jwk::generate_oct_key(32).context("Error generating random JWK")?; + jwk.set_key_operations(vec!["encrypt", "decrypt"]); + let jwk_str = serde_json::to_string(&jwk.as_ref())?; + + let public = tpm_objects::create_tpm2b_public_sealed_object(policy_digest)?.try_into()?; + let jwk_str = SensitiveData::try_from(jwk_str.as_bytes().to_vec())?; + let jwk_result = ctx.execute_with_nullauth_session(|ctx| { + ctx.create(key_handle, public, None, Some(jwk_str), None, None) + })?; + + let jwk_priv = tpm_objects::get_tpm2b_private(jwk_result.out_private.into())?; + + let jwk_pub = tpm_objects::get_tpm2b_public(jwk_result.out_public.try_into()?)?; + + let private_hdr = ClevisInner { + pin: pin_type.to_string(), + tpm2: Tpm2Inner { + hash: cfg.hash.as_ref().unwrap_or(&"sha256".to_string()).clone(), + key: key_type.to_string(), + jwk_pub, + jwk_priv, + pcr_bank: cfg.pcr_bank.clone(), + pcr_ids: cfg.get_pcr_ids_str(), + policy_pubkey_path: cfg.policy_pubkey_path, + policy_ref: cfg.policy_ref, + policy_path: cfg.policy_path, + }, + }; + + let mut hdr = josekit::jwe::JweHeader::new(); + hdr.set_algorithm(Dir.name()); + hdr.set_content_encryption(A256GCM.name()); + hdr.set_claim( + "clevis", + Some(serde_json::value::to_value(private_hdr).context("Error serializing private header")?), + ) + .context("Error adding clevis claim")?; + + let encrypter = Dir + .encrypter_from_jwk(&jwk) + .context("Error creating direct encrypter")?; + let jwe_token = josekit::jwe::serialize_compact(input, &hdr, &encrypter) + .context("Error serializing JWE token")?; + + Ok(jwe_token) +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +struct Tpm2Inner { + hash: String, + #[serde( + deserialize_with = "utils::deserialize_as_base64_url_no_pad", + serialize_with = "utils::serialize_as_base64_url_no_pad" + )] + jwk_priv: Vec, + #[serde( + deserialize_with = "utils::deserialize_as_base64_url_no_pad", + serialize_with = "utils::serialize_as_base64_url_no_pad" + )] + jwk_pub: Vec, + key: String, + + // PCR Binding may be specified, may not + #[serde(skip_serializing_if = "Option::is_none")] + pcr_bank: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pcr_ids: Option, + + // Public key (in PEM format) for a wildcard policy that's OR'd with the PCR one + #[serde(skip_serializing_if = "Option::is_none")] + policy_pubkey_path: Option, + #[serde(skip_serializing_if = "Option::is_none")] + policy_ref: Option, + #[serde(skip_serializing_if = "Option::is_none")] + policy_path: Option, +} + +impl Tpm2Inner { + fn get_pcr_ids(&self) -> Option> { + Some( + self.pcr_ids + .as_ref()? + .split(',') + .map(|x| x.parse::().unwrap()) + .collect(), + ) + } +} + +impl TryFrom<&Tpm2Inner> for TPMPolicyStep { + type Error = Error; + + fn try_from(cfg: &Tpm2Inner) -> Result { + if cfg.pcr_ids.is_some() && cfg.policy_pubkey_path.is_some() { + Ok(TPMPolicyStep::Or([ + Box::new(TPMPolicyStep::PCRs( + utils::get_hash_alg_from_name(cfg.pcr_bank.as_ref()), + cfg.get_pcr_ids().unwrap(), + Box::new(TPMPolicyStep::NoStep), + )), + Box::new(utils::get_authorized_policy_step( + cfg.policy_pubkey_path.as_ref().unwrap(), + &cfg.policy_path, + &cfg.policy_ref, + )?), + Box::new(TPMPolicyStep::NoStep), + Box::new(TPMPolicyStep::NoStep), + Box::new(TPMPolicyStep::NoStep), + Box::new(TPMPolicyStep::NoStep), + Box::new(TPMPolicyStep::NoStep), + Box::new(TPMPolicyStep::NoStep), + ])) + } else if cfg.pcr_ids.is_some() { + Ok(TPMPolicyStep::PCRs( + utils::get_hash_alg_from_name(cfg.pcr_bank.as_ref()), + cfg.get_pcr_ids().unwrap(), + Box::new(TPMPolicyStep::NoStep), + )) + } else if cfg.policy_pubkey_path.is_some() { + utils::get_authorized_policy_step( + cfg.policy_pubkey_path.as_ref().unwrap(), + &cfg.policy_path, + &cfg.policy_ref, + ) + } else { + Ok(TPMPolicyStep::NoStep) + } + } +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +struct ClevisInner { + pin: String, + tpm2: Tpm2Inner, +} + +pub fn perform_decrypt(input: &[u8]) -> Result> { + let input = String::from_utf8(input.to_vec()).context("Error reading input")?; + let hdr = josekit::jwt::decode_header(&input).context("Error decoding header")?; + let hdr_clevis = hdr.claim("clevis").context("Error getting clevis claim")?; + let hdr_clevis: ClevisInner = + serde_json::from_value(hdr_clevis.clone()).context("Error deserializing clevis header")?; + + if hdr_clevis.pin != "tpm2" && hdr_clevis.pin != "tpm2plus" { + bail!("JWE pin mismatch"); + } + + let jwkpub = tpm_objects::build_tpm2b_public(&hdr_clevis.tpm2.jwk_pub)?.try_into()?; + let jwkpriv = tpm_objects::build_tpm2b_private(&hdr_clevis.tpm2.jwk_priv)?; + + let policy = TPMPolicyStep::try_from(&hdr_clevis.tpm2)?; + + let name_alg = utils::get_hash_alg_from_name(Some(&hdr_clevis.tpm2.hash)); + let key_public = tpm_objects::get_key_public(hdr_clevis.tpm2.key.as_str(), name_alg)?; + + let mut ctx = utils::get_tpm2_ctx()?; + let key_handle = utils::get_tpm2_primary_key(&mut ctx, key_public)?; + + let key = + ctx.execute_with_nullauth_session(|ctx| ctx.load(key_handle, jwkpriv.try_into()?, jwkpub))?; + + let (policy_session, _) = policy.send_policy(&mut ctx, false)?; + + let unsealed = ctx.execute_with_session(policy_session, |ctx| ctx.unseal(key.into()))?; + let unsealed = &unsealed.value(); + let mut jwk = josekit::jwk::Jwk::from_bytes(unsealed).context("Error unmarshaling JWK")?; + jwk.set_parameter("alg", None) + .context("Error removing the alg parameter")?; + let decrypter = Dir + .decrypter_from_jwk(&jwk) + .context("Error creating decrypter")?; + + let (payload, _) = + josekit::jwe::deserialize_compact(&input, &decrypter).context("Error decrypting JWE")?; + + Ok(payload) +} diff --git a/src/tpm/tpm_objects.rs b/src/tpm/tpm_objects.rs new file mode 100644 index 0000000..5d93c75 --- /dev/null +++ b/src/tpm/tpm_objects.rs @@ -0,0 +1,366 @@ +use color_eyre::eyre::{bail, eyre, Context, Error}; +use std::convert::TryFrom; + +use super::utils::get_authorized_policy_step; +use color_eyre::Result; +use serde::{Deserialize, Serialize}; +use tpm2_policy::TPMPolicyStep; +use tss_esapi::{ + attributes::object::ObjectAttributesBuilder, + constants::tss as tss_constants, + interface_types::{ + algorithm::{HashingAlgorithm, PublicAlgorithm}, + ecc::EccCurve, + }, + structures::{Digest, Public, SymmetricDefinitionObject}, +}; + +#[derive(Serialize, Deserialize, Default, std::fmt::Debug)] +pub struct TPM2Config { + pub hash: Option, + pub key: Option, + pub pcr_bank: Option, + // PCR IDs can be passed in as comma-separated string or json array + pub pcr_ids: Option, + pub pcr_digest: Option, + // Whether to use a policy. If this is specified without pubkey path or policy path, they get set to defaults + pub use_policy: Option, + // Public key (in JSON format) for a wildcard policy that's possibly OR'd with the PCR one + pub policy_pubkey_path: Option, + pub policy_ref: Option, + pub policy_path: Option, +} + +impl TryFrom<&TPM2Config> for TPMPolicyStep { + type Error = Error; + + fn try_from(cfg: &TPM2Config) -> Result { + if cfg.pcr_ids.is_some() && cfg.policy_pubkey_path.is_some() { + Ok(TPMPolicyStep::Or([ + Box::new(TPMPolicyStep::PCRs( + cfg.get_pcr_hash_alg(), + cfg.get_pcr_ids().unwrap(), + Box::new(TPMPolicyStep::NoStep), + )), + Box::new(get_authorized_policy_step( + cfg.policy_pubkey_path.as_ref().unwrap(), + &None, + &cfg.policy_ref, + )?), + Box::new(TPMPolicyStep::NoStep), + Box::new(TPMPolicyStep::NoStep), + Box::new(TPMPolicyStep::NoStep), + Box::new(TPMPolicyStep::NoStep), + Box::new(TPMPolicyStep::NoStep), + Box::new(TPMPolicyStep::NoStep), + ])) + } else if cfg.pcr_ids.is_some() { + Ok(TPMPolicyStep::PCRs( + cfg.get_pcr_hash_alg(), + cfg.get_pcr_ids().unwrap(), + Box::new(TPMPolicyStep::NoStep), + )) + } else if cfg.policy_pubkey_path.is_some() { + get_authorized_policy_step( + cfg.policy_pubkey_path.as_ref().unwrap(), + &None, + &cfg.policy_ref, + ) + } else { + Ok(TPMPolicyStep::NoStep) + } + } +} + +const DEFAULT_POLICY_PATH: &str = "/boot/clevis_policy.json"; +const DEFAULT_PUBKEY_PATH: &str = "/boot/clevis_pubkey.json"; +const DEFAULT_POLICY_REF: &str = ""; + +impl TPM2Config { + pub(super) fn get_pcr_hash_alg( + &self, + ) -> tss_esapi::interface_types::algorithm::HashingAlgorithm { + super::utils::get_hash_alg_from_name(self.pcr_bank.as_ref()) + } + + pub(super) fn get_name_hash_alg( + &self, + ) -> tss_esapi::interface_types::algorithm::HashingAlgorithm { + super::utils::get_hash_alg_from_name(self.hash.as_ref()) + } + + pub(super) fn get_pcr_ids(&self) -> Option> { + match &self.pcr_ids { + None => None, + Some(serde_json::Value::Array(vals)) => { + Some(vals.iter().map(|x| x.as_u64().unwrap()).collect()) + } + _ => panic!("Unexpected type found for pcr_ids"), + } + } + + pub(super) fn get_pcr_ids_str(&self) -> Option { + match &self.pcr_ids { + None => None, + Some(serde_json::Value::Array(vals)) => Some( + vals.iter() + .map(|x| x.as_u64().unwrap().to_string()) + .collect::>() + .join(","), + ), + _ => panic!("Unexpected type found for pcr_ids"), + } + } + + pub fn normalize(mut self) -> Result { + self.normalize_pcr_ids()?; + if self.pcr_ids.is_some() && self.pcr_bank.is_none() { + self.pcr_bank = Some("sha256".to_string()); + } + // Make use of the defaults if not specified + if self.use_policy.is_some() && self.use_policy.unwrap() { + if self.policy_path.is_none() { + self.policy_path = Some(DEFAULT_POLICY_PATH.to_string()); + } + if self.policy_pubkey_path.is_none() { + self.policy_pubkey_path = Some(DEFAULT_PUBKEY_PATH.to_string()); + } + if self.policy_ref.is_none() { + self.policy_ref = Some(DEFAULT_POLICY_REF.to_string()); + } + } else if self.policy_pubkey_path.is_some() + || self.policy_path.is_some() + || self.policy_ref.is_some() + { + eprintln!("To use a policy, please specifiy use_policy: true. Not specifying this will be a fatal error in a next release"); + } + if (self.policy_pubkey_path.is_some() + || self.policy_path.is_some() + || self.policy_ref.is_some()) + && (self.policy_pubkey_path.is_none() + || self.policy_path.is_none() + || self.policy_ref.is_none()) + { + bail!("Not all of policy pubkey, path and ref are specified",); + } + Ok(self) + } + + fn normalize_pcr_ids(&mut self) -> Result<()> { + // Normalize from array with one string to just string + if let Some(serde_json::Value::Array(vals)) = &self.pcr_ids { + if vals.len() == 1 { + if let serde_json::Value::String(val) = &vals[0] { + self.pcr_ids = Some(serde_json::Value::String(val.to_string())); + } + } + } + // Normalize pcr_ids from comma-separated string to array + if let Some(serde_json::Value::String(val)) = &self.pcr_ids { + // Was a string, do a split + let newval: Vec = val + .split(',') + .map(|x| serde_json::Value::String(x.trim().to_string())) + .collect(); + self.pcr_ids = Some(serde_json::Value::Array(newval)); + } + // Normalize pcr_ids from array of Strings to array of Numbers + if let Some(serde_json::Value::Array(vals)) = &self.pcr_ids { + let newvals: Result, _> = vals + .iter() + .map(|x| match x { + serde_json::Value::String(val) => { + match val.trim().parse::() { + Ok(res) => { + let new = serde_json::Value::Number(res); + if !new.is_u64() { + bail!("Non-positive string int"); + } + Ok(new) + } + Err(_) => Err(eyre!("Unparseable string int")), + } + } + serde_json::Value::Number(n) => { + let new = serde_json::Value::Number(n.clone()); + if !new.is_u64() { + return Err(eyre!("Non-positive int")); + } + Ok(new) + } + _ => Err(eyre!("Invalid value in pcr_ids")), + }) + .collect(); + self.pcr_ids = Some(serde_json::Value::Array(newvals?)); + } + + match &self.pcr_ids { + None => Ok(()), + // The normalization above would've caught any non-ints + Some(serde_json::Value::Array(_)) => Ok(()), + _ => Err(eyre!("Invalid type")), + } + } +} + +#[cfg(target_pointer_width = "64")] +type Sizedu = u64; +#[cfg(target_pointer_width = "32")] +type Sizedu = u32; + +pub(super) fn get_key_public( + key_type: &str, + name_alg: HashingAlgorithm, +) -> color_eyre::Result { + let object_attributes = ObjectAttributesBuilder::new() + .with_fixed_tpm(true) + .with_fixed_parent(true) + .with_sensitive_data_origin(true) + .with_user_with_auth(true) + .with_decrypt(true) + .with_sign_encrypt(false) + .with_restricted(true) + .build()?; + + let builder = tss_esapi::structures::PublicBuilder::new() + .with_object_attributes(object_attributes) + .with_name_hashing_algorithm(name_alg); + + match key_type { + "ecc" => builder + .with_public_algorithm(PublicAlgorithm::Ecc) + .with_ecc_parameters( + tss_esapi::structures::PublicEccParametersBuilder::new_restricted_decryption_key( + SymmetricDefinitionObject::AES_128_CFB, + EccCurve::NistP256, + ) + .build()?, + ) + .with_ecc_unique_identifier(Default::default()), + "rsa" => builder + .with_public_algorithm(PublicAlgorithm::Rsa) + .with_rsa_parameters( + tss_esapi::structures::PublicRsaParametersBuilder::new_restricted_decryption_key( + SymmetricDefinitionObject::AES_128_CFB, + tss_esapi::interface_types::key_bits::RsaKeyBits::Rsa2048, + tss_esapi::structures::RsaExponent::ZERO_EXPONENT, + ) + .build()?, + ) + .with_rsa_unique_identifier(Default::default()), + _ => return Err(eyre!("Unsupported key type used")), + } + .build() + .context("Error building public key") +} + +pub(super) fn create_tpm2b_public_sealed_object( + policy: Option, +) -> Result { + let mut object_attributes = ObjectAttributesBuilder::new() + .with_fixed_tpm(true) + .with_fixed_parent(true) + .with_no_da(true) + .with_admin_with_policy(true); + + if policy.is_none() { + object_attributes = object_attributes.with_user_with_auth(true); + } + let policy = match policy { + Some(p) => p, + None => Digest::try_from(vec![])?, + }; + + let mut params: tss_esapi::tss2_esys::TPMU_PUBLIC_PARMS = Default::default(); + params.keyedHashDetail.scheme.scheme = tss_constants::TPM2_ALG_NULL; + + Ok(tss_esapi::tss2_esys::TPM2B_PUBLIC { + size: std::mem::size_of::() as u16, + publicArea: tss_esapi::tss2_esys::TPMT_PUBLIC { + type_: tss_constants::TPM2_ALG_KEYEDHASH, + nameAlg: tss_constants::TPM2_ALG_SHA256, + objectAttributes: object_attributes.build()?.0, + authPolicy: tss_esapi::tss2_esys::TPM2B_DIGEST::from(policy), + parameters: params, + unique: Default::default(), + }, + }) +} + +pub(super) fn get_tpm2b_public(val: tss_esapi::tss2_esys::TPM2B_PUBLIC) -> Result> { + let mut offset = 0 as Sizedu; + let mut resp = Vec::with_capacity((val.size + 4) as usize); + + unsafe { + let res = tss_esapi::tss2_esys::Tss2_MU_TPM2B_PUBLIC_Marshal( + &val, + resp.as_mut_ptr(), + resp.capacity() as Sizedu, + &mut offset, + ); + if res != 0 { + bail!("Marshalling tpm2b_public failed"); + } + resp.set_len(offset as usize); + } + + Ok(resp) +} + +pub(super) fn get_tpm2b_private(val: tss_esapi::tss2_esys::TPM2B_PRIVATE) -> Result> { + let mut offset = 0 as Sizedu; + let mut resp = Vec::with_capacity((val.size + 4) as usize); + + unsafe { + let res = tss_esapi::tss2_esys::Tss2_MU_TPM2B_PRIVATE_Marshal( + &val, + resp.as_mut_ptr(), + resp.capacity() as Sizedu, + &mut offset, + ); + if res != 0 { + bail!("Marshalling tpm2b_private failed"); + } + resp.set_len(offset as usize); + } + + Ok(resp) +} + +pub(super) fn build_tpm2b_private(val: &[u8]) -> Result { + let mut resp = tss_esapi::tss2_esys::TPM2B_PRIVATE::default(); + let mut offset = 0 as Sizedu; + + unsafe { + let res = tss_esapi::tss2_esys::Tss2_MU_TPM2B_PRIVATE_Unmarshal( + val[..].as_ptr(), + val.len() as Sizedu, + &mut offset, + &mut resp, + ); + if res != 0 { + bail!("Unmarshalling tpm2b_private failed"); + } + } + + Ok(resp) +} + +pub(super) fn build_tpm2b_public(val: &[u8]) -> Result { + let mut resp = tss_esapi::tss2_esys::TPM2B_PUBLIC::default(); + let mut offset = 0 as Sizedu; + + unsafe { + let res = tss_esapi::tss2_esys::Tss2_MU_TPM2B_PUBLIC_Unmarshal( + val[..].as_ptr(), + val.len() as Sizedu, + &mut offset, + &mut resp, + ); + if res != 0 { + bail!("Unmarshalling tpm2b_public failed"); + } + } + + Ok(resp) +} diff --git a/src/tpm/utils.rs b/src/tpm/utils.rs new file mode 100644 index 0000000..dbeeaa2 --- /dev/null +++ b/src/tpm/utils.rs @@ -0,0 +1,110 @@ +use std::env; +use std::fs; +use std::str::FromStr; + +use base64::engine::general_purpose::URL_SAFE_NO_PAD; +use base64::Engine; +use color_eyre::eyre::WrapErr; +use color_eyre::Result; +use serde::Deserialize; +use tpm2_policy::{PublicKey, SignedPolicyList, TPMPolicyStep}; +use tss_esapi::{ + handles::KeyHandle, + interface_types::{algorithm::HashingAlgorithm, resource_handles::Hierarchy}, + structures::Public, + Context, Tcti, +}; + +pub(crate) fn get_authorized_policy_step( + policy_pubkey_path: &str, + policy_path: &Option, + policy_ref: &Option, +) -> Result { + let policy_ref = match policy_ref { + Some(policy_ref) => policy_ref.as_bytes().to_vec(), + None => vec![], + }; + + let signkey = { + let contents = + fs::read_to_string(policy_pubkey_path).context("Error reading policy signkey")?; + serde_json::from_str::(&contents) + .context("Error deserializing signing public key")? + }; + + let policies = match policy_path { + None => None, + Some(policy_path) => { + let contents = fs::read_to_string(policy_path).context("Error reading policy")?; + Some( + serde_json::from_str::(&contents) + .context("Error deserializing policy")?, + ) + } + }; + + Ok(TPMPolicyStep::Authorized { + signkey, + policy_ref, + policies, + next: Box::new(TPMPolicyStep::NoStep), + }) +} + +pub(crate) fn get_hash_alg_from_name(name: Option<&String>) -> HashingAlgorithm { + match name { + None => HashingAlgorithm::Sha256, + Some(val) => match val.to_lowercase().as_str() { + "sha1" => HashingAlgorithm::Sha1, + "sha256" => HashingAlgorithm::Sha256, + "sha384" => HashingAlgorithm::Sha384, + "sha512" => HashingAlgorithm::Sha512, + _ => panic!("Unsupported hash algo: {:?}", name), + }, + } +} + +pub(crate) fn serialize_as_base64_url_no_pad( + bytes: &[u8], + serializer: S, +) -> Result +where + S: serde::Serializer, +{ + serializer.serialize_str(&URL_SAFE_NO_PAD.encode(bytes)) +} + +pub(crate) fn deserialize_as_base64_url_no_pad<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + String::deserialize(deserializer).and_then(|string| { + URL_SAFE_NO_PAD + .decode(string) + .map_err(serde::de::Error::custom) + }) +} + +pub(crate) fn get_tpm2_ctx() -> Result { + let tcti_path = match env::var("TCTI") { + Ok(val) => val, + Err(_) => { + if std::path::Path::new("/dev/tpmrm0").exists() { + "device:/dev/tpmrm0".to_string() + } else { + "device:/dev/tpm0".to_string() + } + } + }; + + let tcti = Tcti::from_str(&tcti_path).context("Error parsing TCTI specification")?; + Context::new(tcti).context("Error initializing TPM2 context") +} + +pub(crate) fn get_tpm2_primary_key(ctx: &mut Context, pub_template: Public) -> Result { + ctx.execute_with_nullauth_session(|ctx| { + ctx.create_primary(Hierarchy::Owner, pub_template, None, None, None, None) + .map(|r| r.key_handle) + }) + .map_err(|e| e.into()) +}