tpm2 working

This commit is contained in:
Vivian 2023-12-19 09:00:07 +01:00
parent 83a4181842
commit b36aa43b3a
8 changed files with 862 additions and 89 deletions

140
Cargo.lock generated
View file

@ -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"

View file

@ -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"
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"

View file

@ -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;
};
});
}

1
result Symbolic link
View file

@ -0,0 +1 @@
/nix/store/bwvjqn0n9xljxk646l0rqj6m0ym221m7-gnome-autounlock-keyring-0.1.0

View file

@ -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<PathBuf> {
let gnome_var = env::var("GNOME_KEYRING_CONTROL")
@ -22,6 +24,7 @@ fn get_control_socket() -> Option<PathBuf> {
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<ControlResult> {
#[derive(Parser)]
struct Cli {
/// Defaults to CONFIG_DIR/gnome-keyring.tpm2
#[arg(short, long)]
token_path: Option<PathBuf>,
#[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")
}

216
src/tpm/mod.rs Normal file
View file

@ -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<String> {
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<u8>,
#[serde(
deserialize_with = "utils::deserialize_as_base64_url_no_pad",
serialize_with = "utils::serialize_as_base64_url_no_pad"
)]
jwk_pub: Vec<u8>,
key: String,
// PCR Binding may be specified, may not
#[serde(skip_serializing_if = "Option::is_none")]
pcr_bank: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pcr_ids: Option<String>,
// 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<String>,
#[serde(skip_serializing_if = "Option::is_none")]
policy_ref: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
policy_path: Option<String>,
}
impl Tpm2Inner {
fn get_pcr_ids(&self) -> Option<Vec<u64>> {
Some(
self.pcr_ids
.as_ref()?
.split(',')
.map(|x| x.parse::<u64>().unwrap())
.collect(),
)
}
}
impl TryFrom<&Tpm2Inner> for TPMPolicyStep {
type Error = Error;
fn try_from(cfg: &Tpm2Inner) -> Result<Self> {
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<Vec<u8>> {
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)
}

366
src/tpm/tpm_objects.rs Normal file
View file

@ -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<String>,
pub key: Option<String>,
pub pcr_bank: Option<String>,
// PCR IDs can be passed in as comma-separated string or json array
pub pcr_ids: Option<serde_json::Value>,
pub pcr_digest: Option<String>,
// Whether to use a policy. If this is specified without pubkey path or policy path, they get set to defaults
pub use_policy: Option<bool>,
// Public key (in JSON format) for a wildcard policy that's possibly OR'd with the PCR one
pub policy_pubkey_path: Option<String>,
pub policy_ref: Option<String>,
pub policy_path: Option<String>,
}
impl TryFrom<&TPM2Config> for TPMPolicyStep {
type Error = Error;
fn try_from(cfg: &TPM2Config) -> Result<Self> {
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<Vec<u64>> {
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<String> {
match &self.pcr_ids {
None => None,
Some(serde_json::Value::Array(vals)) => Some(
vals.iter()
.map(|x| x.as_u64().unwrap().to_string())
.collect::<Vec<String>>()
.join(","),
),
_ => panic!("Unexpected type found for pcr_ids"),
}
}
pub fn normalize(mut self) -> Result<TPM2Config> {
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<serde_json::Value> = 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<Vec<serde_json::Value>, _> = vals
.iter()
.map(|x| match x {
serde_json::Value::String(val) => {
match val.trim().parse::<serde_json::Number>() {
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<Public> {
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<Digest>,
) -> Result<tss_esapi::tss2_esys::TPM2B_PUBLIC> {
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::<tss_esapi::tss2_esys::TPMT_PUBLIC>() 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<Vec<u8>> {
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<Vec<u8>> {
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<tss_esapi::tss2_esys::TPM2B_PRIVATE> {
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<tss_esapi::tss2_esys::TPM2B_PUBLIC> {
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)
}

110
src/tpm/utils.rs Normal file
View file

@ -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<String>,
policy_ref: &Option<String>,
) -> Result<TPMPolicyStep> {
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::<PublicKey>(&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::<SignedPolicyList>(&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<S>(
bytes: &[u8],
serializer: S,
) -> Result<S::Ok, S::Error>
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<Vec<u8>, 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<tss_esapi::Context> {
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<KeyHandle> {
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())
}