diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore index f4a898c..d1263a0 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,6 @@ Cargo.lock /tests/policy_working.json /tests/privatekey.pem /tests/publickey.json + +.direnv +.idea \ No newline at end of file diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..8401458 --- /dev/null +++ b/flake.lock @@ -0,0 +1,60 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1701680307, + "narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "4022d587cbbfd70fe950c1e2083a02621806a725", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1702926307, + "narHash": "sha256-GVwGcCEuO1hKecwMQvFUyTg61KCoR3QkKbhfPMe5R5Q=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "5a9be42754cee0d35d893cbed08737486e5f5e6d", + "type": "github" + }, + "original": { + "owner": "nixos", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..ec6ef15 --- /dev/null +++ b/flake.nix @@ -0,0 +1,46 @@ +{ + inputs = { + nixpkgs.url = "github:nixos/nixpkgs"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + 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" + } \ + " + ''; + nativeBuildInputs = with pkgs; [ + llvmPackages.libclang + llvmPackages.libcxxClang + clang + ]; + LIBCLANG_PATH = "${pkgs.llvmPackages.libclang.lib}/lib"; + buildInputs = with pkgs; [ libclang pkg-config openssl tpm2-tss ]; + }; + }); +} diff --git a/src/cli.rs b/src/cli.rs index e5caa70..7c5162c 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -1,198 +1,7 @@ -use std::convert::TryFrom; -use anyhow::{anyhow, bail, Error, Result}; -use serde::{Deserialize, Serialize}; -use tpm2_policy::TPMPolicyStep; -use crate::utils::get_authorized_policy_step; +use anyhow::{bail, Result}; -#[derive(Serialize, Deserialize, std::fmt::Debug)] -pub(super) 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) - } - } -} - -pub(crate) const DEFAULT_POLICY_PATH: &str = "/boot/clevis_policy.json"; -pub(crate) const DEFAULT_PUBKEY_PATH: &str = "/boot/clevis_pubkey.json"; -pub(crate) const DEFAULT_POLICY_REF: &str = ""; - -impl TPM2Config { - pub(super) fn get_pcr_hash_alg( - &self, - ) -> tss_esapi::interface_types::algorithm::HashingAlgorithm { - crate::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 { - crate::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"), - } - } - - 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(anyhow!("Unparseable string int")), - } - } - serde_json::Value::Number(n) => { - let new = serde_json::Value::Number(n.clone()); - if !new.is_u64() { - return Err(anyhow!("Non-positive int")); - } - Ok(new) - } - _ => Err(anyhow!("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(anyhow!("Invalid type")), - } - } -} #[derive(Debug)] pub(super) enum ActionMode { @@ -202,6 +11,12 @@ pub(super) enum ActionMode { Help, } +use crate::TPM2Config; + +pub const DEFAULT_POLICY_PATH: &str = "/boot/clevis_policy.json"; +pub const DEFAULT_PUBKEY_PATH: &str = "/boot/clevis_pubkey.json"; +pub const DEFAULT_POLICY_REF: &str = ""; + pub(super) fn get_mode_and_cfg(args: &[String]) -> Result<(ActionMode, Option)> { if args.len() > 1 && args[1] == "--summary" { return Ok((ActionMode::Summary, None)); diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..2f3cb6c --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,215 @@ +// Copyright 2020 Patrick Uiterwijk +// +// Licensed under the MIT license + +use std::convert::{TryFrom, TryInto}; + +use std::io::{self, Write}; + +use anyhow::{bail, Context, 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: Vec) -> 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: Vec) -> Result> { + let input = String::from_utf8(input).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 = crate::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/main.rs b/src/main.rs index e177125..5d3a90c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,224 +1,12 @@ -// Copyright 2020 Patrick Uiterwijk -// -// Licensed under the MIT license +use std::{ + env, + io::{stdout, Read, Write}, +}; -use std::convert::{TryFrom, TryInto}; -use std::env; -use std::io::{self, Read, Write}; - -use anyhow::{bail, Context, Error, Result}; -use josekit::jwe::{alg::direct::DirectJweAlgorithm::Dir, enc::A256GCM}; -use serde::{Deserialize, Serialize}; -use tpm2_policy::TPMPolicyStep; -use tss_esapi::structures::SensitiveData; +use anyhow::Result; +use clevis_pin_tpm2::{perform_decrypt, perform_encrypt, tpm_objects::TPM2Config}; mod cli; -mod tpm_objects; -mod utils; - -use cli::TPM2Config; - -fn perform_encrypt(cfg: TPM2Config, input: Vec) -> 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")?; - - io::stdout().write_all(jwe_token.as_bytes())?; - - Ok(()) -} - -#[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, -} - -fn perform_decrypt(input: Vec) -> Result<()> { - let input = String::from_utf8(input).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 = crate::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")?; - - io::stdout().write_all(&payload)?; - - Ok(()) -} fn print_summary() { println!("Encrypts using a TPM2.0 chip binding policy"); @@ -227,8 +15,8 @@ fn print_summary() { fn print_help() { eprintln!( " -Usage (encryption): clevis encrypt tpm2 CONFIG < PLAINTEXT > JWE -Usage (decryption): clevis decrypt tpm2 CONFIG < JWE > PLAINTEXT +Usage (encryption): clevis encrypt CONFIG < PLAINTEXT > JWE +Usage (decryption): clevis decrypt CONFIG < JWE > PLAINTEXT Encrypts or decrypts using a TPM2.0 chip binding policy @@ -279,14 +67,23 @@ fn main() -> Result<()> { }; let mut input = Vec::new(); - if let Err(e) = io::stdin().read_to_end(&mut input) { + if let Err(e) = std::io::stdin().read_to_end(&mut input) { eprintln!("Error getting input token: {}", e); std::process::exit(1); } match mode { - cli::ActionMode::Encrypt => perform_encrypt(cfg.unwrap(), input), - cli::ActionMode::Decrypt => perform_decrypt(input), + cli::ActionMode::Encrypt => { + let token = perform_encrypt(cfg.unwrap(), input)?; + stdout().write_all(token.as_bytes())?; + Ok(()) + } + cli::ActionMode::Decrypt => { + let output = perform_decrypt(input)?; + stdout().write_all(&output)?; + + Ok(()) + } cli::ActionMode::Summary => unreachable!(), cli::ActionMode::Help => unreachable!(), } diff --git a/src/tpm_objects.rs b/src/tpm_objects.rs index a9495b3..c4e6a51 100644 --- a/src/tpm_objects.rs +++ b/src/tpm_objects.rs @@ -1,6 +1,9 @@ use std::convert::TryFrom; -use anyhow::{anyhow, bail, Context, Result}; +use crate::utils::get_authorized_policy_step; +use anyhow::{anyhow, bail, Context, Error, Result}; +use serde::{Deserialize, Serialize}; +use tpm2_policy::TPMPolicyStep; use tss_esapi::{ attributes::object::ObjectAttributesBuilder, constants::tss as tss_constants, @@ -11,6 +14,194 @@ use tss_esapi::{ structures::{Digest, Public, SymmetricDefinitionObject}, }; +#[derive(Serialize, Deserialize, 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) + } + } +} + +pub(crate) const DEFAULT_POLICY_PATH: &str = "/boot/clevis_policy.json"; +pub(crate) const DEFAULT_PUBKEY_PATH: &str = "/boot/clevis_pubkey.json"; +pub(crate) const DEFAULT_POLICY_REF: &str = ""; + +impl TPM2Config { + pub(super) fn get_pcr_hash_alg( + &self, + ) -> tss_esapi::interface_types::algorithm::HashingAlgorithm { + crate::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 { + crate::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(anyhow!("Unparseable string int")), + } + } + serde_json::Value::Number(n) => { + let new = serde_json::Value::Number(n.clone()); + if !new.is_u64() { + return Err(anyhow!("Non-positive int")); + } + Ok(new) + } + _ => Err(anyhow!("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(anyhow!("Invalid type")), + } + } +} + #[cfg(target_pointer_width = "64")] type Sizedu = u64; #[cfg(target_pointer_width = "32")] diff --git a/src/utils.rs b/src/utils.rs index a411140..6c6814a 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -76,7 +76,7 @@ where D: serde::Deserializer<'de>, { String::deserialize(deserializer).and_then(|string| { - base64::decode_config(&string, base64::URL_SAFE_NO_PAD).map_err(serde::de::Error::custom) + base64::decode_config(string, base64::URL_SAFE_NO_PAD).map_err(serde::de::Error::custom) }) }