From a2650d5234b35c2f939657494be73db37b46b2c4 Mon Sep 17 00:00:00 2001 From: Patrick Uiterwijk Date: Fri, 14 Feb 2020 11:50:13 +0100 Subject: [PATCH] Initial commit --- .gitignore | 18 + Cargo.toml | 14 + LICENSE | 287 ++++++++ README.md | 2 + src/main.rs | 1337 +++++++++++++++++++++++++++++++++++++ tests/policy_broken.json | 1 + tests/policy_pubkey.json | 1 + tests/policy_working.json | 1 + tests/test_pcr | 24 + tests/test_policy | 18 + 10 files changed, 1703 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 src/main.rs create mode 100644 tests/policy_broken.json create mode 100644 tests/policy_pubkey.json create mode 100644 tests/policy_working.json create mode 100755 tests/test_pcr create mode 100755 tests/test_policy diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..154e80c --- /dev/null +++ b/.gitignore @@ -0,0 +1,18 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + + +#Added by cargo +# +#already existing elements are commented out + +/target +#**/*.rs.bk diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..7854471 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "clevis-pin-tpm2" +version = "0.1.0" +authors = ["Patrick Uiterwijk "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tss-esapi = "4.0.5-alpha.1" +serde = "1.0" +biscuit = { git = "https://github.com/lawliet89/biscuit", branch = "master" } +serde_json = "1.0" +base64 = "0.12.1" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..4153cd3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,287 @@ + EUROPEAN UNION PUBLIC LICENCE v. 1.2 + EUPL © the European Union 2007, 2016 + +This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined +below) which is provided under the terms of this Licence. Any use of the Work, +other than as authorised under this Licence is prohibited (to the extent such +use is covered by a right of the copyright holder of the Work). + +The Work is provided under the terms of this Licence when the Licensor (as +defined below) has placed the following notice immediately following the +copyright notice for the Work: + + Licensed under the EUPL + +or has expressed by any other means his willingness to license under the EUPL. + +1. Definitions + +In this Licence, the following terms have the following meaning: + +- ‘The Licence’: this Licence. + +- ‘The Original Work’: the work or software distributed or communicated by the + Licensor under this Licence, available as Source Code and also as Executable + Code as the case may be. + +- ‘Derivative Works’: the works or software that could be created by the + Licensee, based upon the Original Work or modifications thereof. This Licence + does not define the extent of modification or dependence on the Original Work + required in order to classify a work as a Derivative Work; this extent is + determined by copyright law applicable in the country mentioned in Article 15. + +- ‘The Work’: the Original Work or its Derivative Works. + +- ‘The Source Code’: the human-readable form of the Work which is the most + convenient for people to study and modify. + +- ‘The Executable Code’: any code which has generally been compiled and which is + meant to be interpreted by a computer as a program. + +- ‘The Licensor’: the natural or legal person that distributes or communicates + the Work under the Licence. + +- ‘Contributor(s)’: any natural or legal person who modifies the Work under the + Licence, or otherwise contributes to the creation of a Derivative Work. + +- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of + the Work under the terms of the Licence. + +- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, + renting, distributing, communicating, transmitting, or otherwise making + available, online or offline, copies of the Work or providing access to its + essential functionalities at the disposal of any other natural or legal + person. + +2. Scope of the rights granted by the Licence + +The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +sublicensable licence to do the following, for the duration of copyright vested +in the Original Work: + +- use the Work in any circumstance and for all usage, +- reproduce the Work, +- modify the Work, and make Derivative Works based upon the Work, +- communicate to the public, including the right to make available or display + the Work or copies thereof to the public and perform publicly, as the case may + be, the Work, +- distribute the Work or copies thereof, +- lend and rent the Work or copies thereof, +- sublicense rights in the Work or copies thereof. + +Those rights can be exercised on any media, supports and formats, whether now +known or later invented, as far as the applicable law permits so. + +In the countries where moral rights apply, the Licensor waives his right to +exercise his moral right to the extent allowed by law in order to make effective +the licence of the economic rights here above listed. + +The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to +any patents held by the Licensor, to the extent necessary to make use of the +rights granted on the Work under this Licence. + +3. Communication of the Source Code + +The Licensor may provide the Work either in its Source Code form, or as +Executable Code. If the Work is provided as Executable Code, the Licensor +provides in addition a machine-readable copy of the Source Code of the Work +along with each copy of the Work that the Licensor distributes or indicates, in +a notice following the copyright notice attached to the Work, a repository where +the Source Code is easily and freely accessible for as long as the Licensor +continues to distribute or communicate the Work. + +4. Limitations on copyright + +Nothing in this Licence is intended to deprive the Licensee of the benefits from +any exception or limitation to the exclusive rights of the rights owners in the +Work, of the exhaustion of those rights or of other applicable limitations +thereto. + +5. Obligations of the Licensee + +The grant of the rights mentioned above is subject to some restrictions and +obligations imposed on the Licensee. Those obligations are the following: + +Attribution right: The Licensee shall keep intact all copyright, patent or +trademarks notices and all notices that refer to the Licence and to the +disclaimer of warranties. The Licensee must include a copy of such notices and a +copy of the Licence with every copy of the Work he/she distributes or +communicates. The Licensee must cause any Derivative Work to carry prominent +notices stating that the Work has been modified and the date of modification. + +Copyleft clause: If the Licensee distributes or communicates copies of the +Original Works or Derivative Works, this Distribution or Communication will be +done under the terms of this Licence or of a later version of this Licence +unless the Original Work is expressly distributed only under this version of the +Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee +(becoming Licensor) cannot offer or impose any additional terms or conditions on +the Work or Derivative Work that alter or restrict the terms of the Licence. + +Compatibility clause: If the Licensee Distributes or Communicates Derivative +Works or copies thereof based upon both the Work and another work licensed under +a Compatible Licence, this Distribution or Communication can be done under the +terms of this Compatible Licence. For the sake of this clause, ‘Compatible +Licence’ refers to the licences listed in the appendix attached to this Licence. +Should the Licensee's obligations under the Compatible Licence conflict with +his/her obligations under this Licence, the obligations of the Compatible +Licence shall prevail. + +Provision of Source Code: When distributing or communicating copies of the Work, +the Licensee will provide a machine-readable copy of the Source Code or indicate +a repository where this Source will be easily and freely available for as long +as the Licensee continues to distribute or communicate the Work. + +Legal Protection: This Licence does not grant permission to use the trade names, +trademarks, service marks, or names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the copyright notice. + +6. Chain of Authorship + +The original Licensor warrants that the copyright in the Original Work granted +hereunder is owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each Contributor warrants that the copyright in the modifications he/she brings +to the Work are owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. + +Each time You accept the Licence, the original Licensor and subsequent +Contributors grant You a licence to their contributions to the Work, under the +terms of this Licence. + +7. Disclaimer of Warranty + +The Work is a work in progress, which is continuously improved by numerous +Contributors. It is not a finished work and may therefore contain defects or +‘bugs’ inherent to this type of development. + +For the above reason, the Work is provided under the Licence on an ‘as is’ basis +and without warranties of any kind concerning the Work, including without +limitation merchantability, fitness for a particular purpose, absence of defects +or errors, accuracy, non-infringement of intellectual property rights other than +copyright as stated in Article 6 of this Licence. + +This disclaimer of warranty is an essential part of the Licence and a condition +for the grant of any rights to the Work. + +8. Disclaimer of Liability + +Except in the cases of wilful misconduct or damages directly caused to natural +persons, the Licensor will in no event be liable for any direct or indirect, +material or moral, damages of any kind, arising out of the Licence or of the use +of the Work, including without limitation, damages for loss of goodwill, work +stoppage, computer failure or malfunction, loss of data or any commercial +damage, even if the Licensor has been advised of the possibility of such damage. +However, the Licensor will be liable under statutory product liability laws as +far such laws apply to the Work. + +9. Additional agreements + +While distributing the Work, You may choose to conclude an additional agreement, +defining obligations or services consistent with this Licence. However, if +accepting obligations, You may act only on your own behalf and on your sole +responsibility, not on behalf of the original Licensor or any other Contributor, +and only if You agree to indemnify, defend, and hold each Contributor harmless +for any liability incurred by, or claims asserted against such Contributor by +the fact You have accepted any warranty or additional liability. + +10. Acceptance of the Licence + +The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ +placed under the bottom of a window displaying the text of this Licence or by +affirming consent in any other similar way, in accordance with the rules of +applicable law. Clicking on that icon indicates your clear and irrevocable +acceptance of this Licence and all of its terms and conditions. + +Similarly, you irrevocably accept this Licence and all of its terms and +conditions by exercising any rights granted to You by Article 2 of this Licence, +such as the use of the Work, the creation by You of a Derivative Work or the +Distribution or Communication by You of the Work or copies thereof. + +11. Information to the public + +In case of any Distribution or Communication of the Work by means of electronic +communication by You (for example, by offering to download the Work from a +remote location) the distribution channel or media (for example, a website) must +at least provide to the public the information requested by the applicable law +regarding the Licensor, the Licence and the way it may be accessible, concluded, +stored and reproduced by the Licensee. + +12. Termination of the Licence + +The Licence and the rights granted hereunder will terminate automatically upon +any breach by the Licensee of the terms of the Licence. + +Such a termination will not terminate the licences of any person who has +received the Work from the Licensee under the Licence, provided such persons +remain in full compliance with the Licence. + +13. Miscellaneous + +Without prejudice of Article 9 above, the Licence represents the complete +agreement between the Parties as to the Work. + +If any provision of the Licence is invalid or unenforceable under applicable +law, this will not affect the validity or enforceability of the Licence as a +whole. Such provision will be construed or reformed so as necessary to make it +valid and enforceable. + +The European Commission may publish other linguistic versions or new versions of +this Licence or updated versions of the Appendix, so far this is required and +reasonable, without reducing the scope of the rights granted by the Licence. New +versions of the Licence will be published with a unique version number. + +All linguistic versions of this Licence, approved by the European Commission, +have identical value. Parties can take advantage of the linguistic version of +their choice. + +14. Jurisdiction + +Without prejudice to specific agreement between parties, + +- any litigation resulting from the interpretation of this License, arising + between the European Union institutions, bodies, offices or agencies, as a + Licensor, and any Licensee, will be subject to the jurisdiction of the Court + of Justice of the European Union, as laid down in article 272 of the Treaty on + the Functioning of the European Union, + +- any litigation arising between other parties and resulting from the + interpretation of this License, will be subject to the exclusive jurisdiction + of the competent court where the Licensor resides or conducts its primary + business. + +15. Applicable Law + +Without prejudice to specific agreement between parties, + +- this Licence shall be governed by the law of the European Union Member State + where the Licensor has his seat, resides or has his registered office, + +- this licence shall be governed by Belgian law if the Licensor has no seat, + residence or registered office inside a European Union Member State. + +Appendix + +‘Compatible Licences’ according to Article 5 EUPL are: + +- GNU General Public License (GPL) v. 2, v. 3 +- GNU Affero General Public License (AGPL) v. 3 +- Open Software License (OSL) v. 2.1, v. 3.0 +- Eclipse Public License (EPL) v. 1.0 +- CeCILL v. 2.0, v. 2.1 +- Mozilla Public Licence (MPL) v. 2 +- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 +- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for + works other than software +- European Union Public Licence (EUPL) v. 1.1, v. 1.2 +- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong + Reciprocity (LiLiQ-R+). + +The European Commission may update this Appendix to later versions of the above +licences without producing a new version of the EUPL, as long as they provide +the rights granted in Article 2 of this Licence and protect the covered Source +Code from exclusive appropriation. + +All other changes or additions to this Appendix require the production of a new +EUPL version. diff --git a/README.md b/README.md new file mode 100644 index 0000000..70648cb --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# clevis-pin-tpm2 +Rewritten Clevis TPM2 PIN diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..e68a46d --- /dev/null +++ b/src/main.rs @@ -0,0 +1,1337 @@ +// Copyright 2020 Patrick Uiterwijk +// +// Licensed under the EUPL-1.2-or-later +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use std::convert::{TryFrom, TryInto}; +use std::error::Error; +use std::fmt; +use std::fs; + +extern crate base64; +extern crate biscuit; +extern crate serde; +extern crate serde_json; +extern crate tss_esapi; + +use std::env; +use std::io::{self, Read, Write}; + +use biscuit::jwe; +use biscuit::CompactJson; + +use tss_esapi::constants; +use tss_esapi::tss2_esys::{ESYS_TR, ESYS_TR_NONE, ESYS_TR_RH_OWNER}; +use tss_esapi::utils; +use tss_esapi::utils::tcti; +use tss_esapi::Context; + +use serde::{Deserialize, Serialize}; + +pub fn serialize_as_base64_url_no_pad(bytes: &[u8], serializer: S) -> Result +where + S: serde::Serializer, +{ + serializer.serialize_str(&base64::encode_config(bytes, base64::URL_SAFE_NO_PAD)) +} + +pub 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| { + base64::decode_config(&string, base64::URL_SAFE_NO_PAD).map_err(serde::de::Error::custom) + }) +} + +pub fn serialize_as_base64(bytes: &[u8], serializer: S) -> Result +where + S: serde::Serializer, +{ + serializer.serialize_str(&base64::encode(bytes)) +} + +pub fn deserialize_as_base64<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + String::deserialize(deserializer) + .and_then(|string| base64::decode(&string).map_err(serde::de::Error::custom)) +} + +#[derive(Debug)] +enum PinError { + Text(&'static str), + NoCommand, + Serde(serde_json::Error), + IO(std::io::Error), + TPM(tss_esapi::response_code::Error), + JWE(biscuit::errors::Error), + Base64Decoding(base64::DecodeError), + Utf8(std::str::Utf8Error), +} + +impl PinError {} + +impl fmt::Display for PinError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + PinError::Text(e) => write!(f, "Error: {}", e), + PinError::Serde(err) => { + write!(f, "Serde error: ")?; + err.fmt(f) + } + PinError::IO(err) => { + write!(f, "IO error: ")?; + err.fmt(f) + } + PinError::TPM(err) => { + write!(f, "TPM error: ")?; + err.fmt(f) + } + PinError::JWE(err) => { + write!(f, "JWE error: ")?; + err.fmt(f) + } + PinError::Base64Decoding(err) => { + write!(f, "Base64 Decoding error: ")?; + err.fmt(f) + } + PinError::Utf8(err) => { + write!(f, "UTF8 error: ")?; + err.fmt(f) + } + PinError::NoCommand => write!(f, "No command provided"), + } + } +} + +impl Error for PinError {} + +impl From<&'static str> for PinError { + fn from(err: &'static str) -> Self { + PinError::Text(err) + } +} + +impl From for PinError { + fn from(err: serde_json::Error) -> Self { + PinError::Serde(err) + } +} + +impl From for PinError { + fn from(err: std::io::Error) -> Self { + PinError::IO(err) + } +} + +impl From for PinError { + fn from(err: tss_esapi::response_code::Error) -> Self { + PinError::TPM(err) + } +} + +impl From for PinError { + fn from(err: biscuit::errors::Error) -> Self { + PinError::JWE(err) + } +} + +impl From for PinError { + fn from(err: base64::DecodeError) -> Self { + PinError::Base64Decoding(err) + } +} + +impl From for PinError { + fn from(err: std::str::Utf8Error) -> Self { + PinError::Utf8(err) + } +} + +#[derive(Debug)] +enum TPMPolicyStep { + NoStep, + PCRs( + tss_esapi::utils::algorithm_specifiers::HashingAlgorithm, + Vec, + Box, + ), + Authorized { + signkey: PublicKey, + policy_ref: Vec, + policies: Option, + next: Box, + }, + Or([Box; 8]), +} + +fn create_and_set_tpm2_session( + ctx: &mut tss_esapi::Context, + session_type: tss_esapi::tss2_esys::TPM2_SE, +) -> Result { + let session = ctx.start_auth_session( + ESYS_TR_NONE, + ESYS_TR_NONE, + &[], + session_type, + utils::TpmtSymDefBuilder::aes_256_cfb(), + tss_esapi::constants::TPM2_ALG_SHA256, + )?; + let session_attr = utils::TpmaSessionBuilder::new() + .with_flag(tss_esapi::constants::TPMA_SESSION_DECRYPT) + .with_flag(tss_esapi::constants::TPMA_SESSION_ENCRYPT) + .build(); + + ctx.tr_sess_set_attributes(session, session_attr)?; + + ctx.set_sessions((session, ESYS_TR_NONE, ESYS_TR_NONE)); + + Ok(session) +} + +impl TPMPolicyStep { + /// Sends the generate policy to the TPM2, and sets the authorized policy as active + /// Returns the policy_digest for authInfo + fn send_policy( + self, + ctx: &mut tss_esapi::Context, + trial_policy: bool, + ) -> Result, PinError> { + let pol_type = if trial_policy { + tss_esapi::constants::TPM2_SE_TRIAL + } else { + tss_esapi::constants::TPM2_SE_POLICY + }; + + let session = ctx.start_auth_session( + ESYS_TR_NONE, + ESYS_TR_NONE, + &[], + pol_type, + utils::TpmtSymDefBuilder::aes_256_cfb(), + tss_esapi::constants::TPM2_ALG_SHA256, + )?; + let session_attr = utils::TpmaSessionBuilder::new() + .with_flag(tss_esapi::constants::TPMA_SESSION_DECRYPT) + .with_flag(tss_esapi::constants::TPMA_SESSION_ENCRYPT) + .build(); + ctx.tr_sess_set_attributes(session, session_attr)?; + + match self { + TPMPolicyStep::NoStep => { + create_and_set_tpm2_session(ctx, tss_esapi::constants::TPM2_SE_HMAC)?; + Ok(None) + } + _ => { + self._send_policy(ctx, session)?; + + let pol_digest = ctx.policy_get_digest(session)?; + + if trial_policy { + create_and_set_tpm2_session(ctx, tss_esapi::constants::TPM2_SE_HMAC)?; + } else { + ctx.set_sessions((session, ESYS_TR_NONE, ESYS_TR_NONE)); + } + Ok(Some(pol_digest)) + } + } + } + + fn _send_policy( + self, + ctx: &mut tss_esapi::Context, + policy_session: tss_esapi::tss2_esys::ESYS_TR, + ) -> Result<(), PinError> { + match self { + TPMPolicyStep::NoStep => Ok(()), + + TPMPolicyStep::PCRs(pcr_hash_alg, pcr_ids, next) => { + let pcr_ids: Result, PinError> = + pcr_ids.iter().map(|x| pcr_id_to_slot(x)).collect(); + let pcr_ids: Vec = pcr_ids?; + + let pcr_sel = tss_esapi::utils::PcrSelectionsBuilder::new() + .with_selection(pcr_hash_alg, &pcr_ids) + .build(); + + // Ensure PCR reading occurs with no sessions (we don't use audit sessions) + let old_ses = ctx.sessions(); + ctx.set_sessions((ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE)); + let (_update_counter, pcr_sel, pcr_data) = ctx.pcr_read(pcr_sel)?; + ctx.set_sessions(old_ses); + + let concatenated_pcr_values: Vec<&[u8]> = pcr_ids + .iter() + .map(|x| { + pcr_data + .pcr_bank(pcr_hash_alg) + .unwrap() + .pcr_value(*x) + .unwrap() + .value() + }) + .collect(); + let concatenated_pcr_values = concatenated_pcr_values.as_slice().concat(); + + let (hashed_data, _ticket) = ctx.hash( + &concatenated_pcr_values, + tss_esapi::utils::algorithm_specifiers::HashingAlgorithm::Sha256, + tss_esapi::utils::Hierarchy::Owner, + )?; + + ctx.policy_pcr(policy_session, &hashed_data, pcr_sel)?; + next._send_policy(ctx, policy_session) + } + + TPMPolicyStep::Authorized { + signkey, + policy_ref, + policies, + next, + } => { + let policy_ref = tss_esapi::utils::Digest::try_from(policy_ref)?; + + let tpm_signkey = tss_esapi::tss2_esys::TPM2B_PUBLIC::try_from(&signkey)?; + let loaded_key = + ctx.load_external_public(&tpm_signkey, tss_esapi::utils::Hierarchy::Owner)?; + let loaded_key_name = ctx.tr_get_name(loaded_key)?; + + let (approved_policy, check_ticket) = match policies { + None => { + /* Some TPMs don't seem to like the Null ticket.. Let's just use a dummy + let null_ticket = tss_esapi::tss2_esys::TPMT_TK_VERIFIED { + tag: tss_esapi::constants::TPM2_ST_VERIFIED, + hierarchy: tss_esapi::tss2_esys::ESYS_TR_RH_NULL, + digest: tss_esapi::tss2_esys::TPM2B_DIGEST { + size: 32, + buffer: [0; 64], + }, + }; + */ + let dummy_ticket = get_dummy_ticket(ctx); + (tss_esapi::utils::Digest::try_from(vec![])?, dummy_ticket) + } + Some(policies) => find_and_play_applicable_policy( + ctx, + &policies, + policy_session, + policy_ref.value(), + signkey.get_signing_scheme(), + loaded_key, + )?, + }; + + ctx.policy_authorize( + policy_session, + approved_policy, + tss_esapi::tss2_esys::TPM2B_DIGEST::try_from(policy_ref)?, + loaded_key_name, + check_ticket, + )?; + + next._send_policy(ctx, policy_session) + } + + _ => Err(PinError::Text("Policy not implemented")), + } + } +} + +fn find_and_play_applicable_policy( + ctx: &mut tss_esapi::Context, + policies: &[SignedPolicy], + policy_session: ESYS_TR, + policy_ref: &[u8], + scheme: utils::AsymSchemeUnion, + loaded_key: ESYS_TR, +) -> Result< + ( + tss_esapi::utils::Digest, + tss_esapi::tss2_esys::TPMT_TK_VERIFIED, + ), + PinError, +> { + for policy in policies { + if policy.policy_ref != policy_ref { + continue; + } + + if let Some(policy_digest) = play_policy(ctx, &policy, policy_session)? { + // aHash ≔ H_{aHashAlg}(approvedPolicy || policyRef) + let mut ahash = Vec::new(); + ahash.write_all(&policy_digest)?; + ahash.write_all(&policy_ref)?; + + let ahash = ctx + .hash( + &ahash, + tss_esapi::utils::algorithm_specifiers::HashingAlgorithm::Sha256, + tss_esapi::utils::Hierarchy::Null, + )? + .0; + let signature = tss_esapi::utils::Signature { + scheme, + signature: tss_esapi::utils::SignatureData::RsaSignature(policy.signature.clone()), + }; + let tkt = ctx.verify_signature(loaded_key, &ahash, &signature.try_into()?)?; + + return Ok((policy_digest, tkt)); + } + } + + Err(PinError::Text("No matching authorized policy found")) +} + +// This function would do a simple check whether the policy has a chance for success. +// It does explicitly not change policy_session +fn check_policy_feasibility(_ctx: &mut tss_esapi::Context, _policy: &SignedPolicy) -> Result { + Ok(true) + // TODO: Implement this, to check whether the PCRs in this branch would match +} + +fn play_policy( + ctx: &mut tss_esapi::Context, + policy: &SignedPolicy, + policy_session: ESYS_TR, +) -> Result, PinError> { + if !check_policy_feasibility(ctx, policy)? { + return Ok(None) + } + + for step in &policy.steps { + let tpmstep = TPMPolicyStep::try_from(step)?; + tpmstep._send_policy(ctx, policy_session)?; + } + + Ok(Some(ctx.policy_get_digest(policy_session)?)) +} + +// It turns out that a Null ticket does not work for some TPMs, so let's just generate +// a dummy ticket. This is a valid ticket, but over a totally useless piece of data. +fn get_dummy_ticket(context: &mut tss_esapi::Context) -> tss_esapi::tss2_esys::TPMT_TK_VERIFIED { + let old_ses = context.sessions(); + context.set_sessions((ESYS_TR_NONE, ESYS_TR_NONE, ESYS_TR_NONE)); + create_and_set_tpm2_session(context, tss_esapi::constants::TPM2_SE_HMAC).unwrap(); + + let signing_key_pub = utils::create_unrestricted_signing_rsa_public( + tss_esapi::utils::AsymSchemeUnion::RSASSA( + tss_esapi::utils::algorithm_specifiers::HashingAlgorithm::Sha256, + ), + 2048, + 0, + ) + .unwrap(); + + let key_handle = context + .create_primary_key(ESYS_TR_RH_OWNER, &signing_key_pub, &[], &[], &[], &[]) + .unwrap(); + let ahash = context + .hash( + &[0x1, 0x2], + tss_esapi::utils::algorithm_specifiers::HashingAlgorithm::Sha256, + tss_esapi::utils::Hierarchy::Null, + ) + .unwrap() + .0; + + let scheme = tss_esapi::tss2_esys::TPMT_SIG_SCHEME { + scheme: tss_esapi::constants::TPM2_ALG_NULL, + details: Default::default(), + }; + let validation = tss_esapi::tss2_esys::TPMT_TK_HASHCHECK { + tag: tss_esapi::constants::TPM2_ST_HASHCHECK, + hierarchy: tss_esapi::constants::TPM2_RH_NULL, + digest: Default::default(), + }; + // A signature over just the policy_digest, since the policy_ref is empty + let signature = context + .sign(key_handle, &ahash, scheme, &validation) + .unwrap(); + let tkt = context + .verify_signature(key_handle, &ahash, &signature.try_into().unwrap()) + .unwrap(); + + context.set_sessions(old_ses); + + tkt +} + +#[derive(Serialize, Deserialize, std::fmt::Debug)] +struct TPM2Config { + hash: Option, + key: Option, + pcr_bank: Option, + // PCR IDs can be passed in as comma-separated string or json array + pcr_ids: Option, + pcr_digest: Option, + // Public key (in JSON format) for a wildcard policy that's possibly OR'd with the PCR one + policy_pubkey_path: Option, + policy_ref: Option, + policy_path: Option, +} + +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)?; + serde_json::from_str::(&contents)? + }; + + let policies = match policy_path { + None => None, + Some(policy_path) => { + let contents = fs::read_to_string(policy_path)?; + Some(serde_json::from_str::(&contents)?) + } + }; + + Ok(TPMPolicyStep::Authorized { + signkey, + policy_ref, + policies, + next: Box::new(TPMPolicyStep::NoStep), + }) +} + +impl TryFrom<&SignedPolicyStep> for TPMPolicyStep { + type Error = PinError; + + fn try_from(spolicy: &SignedPolicyStep) -> Result { + match spolicy { + SignedPolicyStep::PCRs{pcr_ids, hash_algorithm, value: _} => { + Ok(TPMPolicyStep::PCRs( + get_pcr_hash_alg_from_name(Some(&hash_algorithm)), + pcr_ids.iter().map(|x| *x as u64).collect(), + Box::new(TPMPolicyStep::NoStep), + )) + }, + } + } +} + +impl TryFrom<&TPM2Config> for TPMPolicyStep { + type Error = PinError; + + 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) + } + } +} + +fn pcr_id_to_slot(pcr: &u64) -> Result { + match pcr { + 0 => Ok(tss_esapi::utils::PcrSlot::Slot0), + 1 => Ok(tss_esapi::utils::PcrSlot::Slot1), + 2 => Ok(tss_esapi::utils::PcrSlot::Slot2), + 3 => Ok(tss_esapi::utils::PcrSlot::Slot3), + 4 => Ok(tss_esapi::utils::PcrSlot::Slot4), + 5 => Ok(tss_esapi::utils::PcrSlot::Slot5), + 6 => Ok(tss_esapi::utils::PcrSlot::Slot6), + 7 => Ok(tss_esapi::utils::PcrSlot::Slot7), + 8 => Ok(tss_esapi::utils::PcrSlot::Slot8), + 9 => Ok(tss_esapi::utils::PcrSlot::Slot9), + 10 => Ok(tss_esapi::utils::PcrSlot::Slot10), + 11 => Ok(tss_esapi::utils::PcrSlot::Slot11), + 12 => Ok(tss_esapi::utils::PcrSlot::Slot12), + 13 => Ok(tss_esapi::utils::PcrSlot::Slot13), + 14 => Ok(tss_esapi::utils::PcrSlot::Slot14), + 15 => Ok(tss_esapi::utils::PcrSlot::Slot15), + 16 => Ok(tss_esapi::utils::PcrSlot::Slot16), + 17 => Ok(tss_esapi::utils::PcrSlot::Slot17), + 18 => Ok(tss_esapi::utils::PcrSlot::Slot18), + 19 => Ok(tss_esapi::utils::PcrSlot::Slot19), + 20 => Ok(tss_esapi::utils::PcrSlot::Slot20), + 21 => Ok(tss_esapi::utils::PcrSlot::Slot21), + 22 => Ok(tss_esapi::utils::PcrSlot::Slot22), + 23 => Ok(tss_esapi::utils::PcrSlot::Slot23), + _ => Err(PinError::Text("Invalid PCR slot requested")), + } +} + +fn get_pcr_hash_alg_from_name( + name: Option<&String>, +) -> tss_esapi::utils::algorithm_specifiers::HashingAlgorithm { + match name { + None => tss_esapi::utils::algorithm_specifiers::HashingAlgorithm::Sha256, + Some(val) => match val.to_lowercase().as_str() { + "sha1" => tss_esapi::utils::algorithm_specifiers::HashingAlgorithm::Sha1, + "sha256" => tss_esapi::utils::algorithm_specifiers::HashingAlgorithm::Sha256, + "sha384" => tss_esapi::utils::algorithm_specifiers::HashingAlgorithm::Sha384, + "sha512" => tss_esapi::utils::algorithm_specifiers::HashingAlgorithm::Sha512, + _ => panic!(format!("Unsupported hash algo: {:?}", name)), + }, + } +} + +impl TPM2Config { + fn get_pcr_hash_alg(&self) -> tss_esapi::utils::algorithm_specifiers::HashingAlgorithm { + get_pcr_hash_alg_from_name(self.pcr_bank.as_ref()) + } + + 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"), + } + } + + 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()); + } + 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()) + { + return Err(PinError::Text( + "Not all of policy pubkey, path and ref are specified", + )); + } + Ok(self) + } + + fn normalize_pcr_ids(&mut self) -> Result<(), PinError> { + // 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.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.parse::() { + Ok(res) => { + let new = serde_json::Value::Number(res); + if !new.is_u64() { + return Err("Non-positive string int"); + } + Ok(new) + } + Err(_) => Err("Unparseable string int"), + }, + serde_json::Value::Number(n) => { + let new = serde_json::Value::Number(n.clone()); + if !new.is_u64() { + return Err("Non-positive int"); + } + Ok(new) + } + _ => Err("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(PinError::Text("Invalid type")), + } + } +} + +// if ! tpm2_createprimary -Q -H "$auth" -g "$hash" -G "$key" -C "$TMP"/primary.context; then + +#[derive(Debug)] +enum ActionMode { + Encrypt, + Decrypt, +} + +fn get_mode_and_cfg(args: &[String]) -> Result<(ActionMode, Option), PinError> { + let (mode, cfgstr) = if args[0].contains("encrypt") && args.len() == 2 { + (ActionMode::Encrypt, Some(&args[1])) + } else if args[0].contains("decrypt") { + (ActionMode::Decrypt, None) + } else if args.len() > 1 { + if args[1] == "encrypt" && args.len() == 3 { + (ActionMode::Encrypt, Some(&args[2])) + } else if args[1] == "decrypt" { + (ActionMode::Decrypt, None) + } else { + return Err(PinError::NoCommand); + } + } else { + return Err(PinError::NoCommand); + }; + + let cfg: Option = match cfgstr { + None => None, + Some(cfgstr) => Some(serde_json::from_str::(cfgstr)?.normalize()?), + }; + + Ok((mode, cfg)) +} + +fn create_tpm2b_public_sealed_object( + policy: Option, +) -> Result { + let mut object_attributes = utils::ObjectAttributes(0); + object_attributes.set_fixed_tpm(true); + object_attributes.set_fixed_parent(true); + object_attributes.set_no_da(true); + object_attributes.set_admin_with_policy(true); + + if policy.is_none() { + object_attributes.set_user_with_auth(true); + } + let policy = match policy { + Some(p) => p, + None => tss_esapi::utils::Digest::try_from(vec![])?, + }; + + let mut params: tss_esapi::tss2_esys::TPMU_PUBLIC_PARMS = Default::default(); + params.keyedHashDetail.scheme.scheme = tss_esapi::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_esapi::constants::TPM2_ALG_KEYEDHASH, + nameAlg: tss_esapi::constants::TPM2_ALG_SHA256, + objectAttributes: object_attributes.0, + authPolicy: tss_esapi::tss2_esys::TPM2B_DIGEST::try_from(policy)?, + parameters: params, + unique: Default::default(), + }, + }) +} + +fn perform_encrypt(cfg: TPM2Config, input: &str) -> Result<(), PinError> { + let key_type = match &cfg.key { + None => "ecc", + Some(key_type) => key_type, + }; + let key_public = get_key_public(&key_type)?; + + let mut ctx = get_tpm2_ctx()?; + let key_handle = 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 key_bytes: Vec = ctx.get_random(32)?; + let mut jwk = biscuit::jwk::JWK::new_octet_key(&key_bytes, biscuit::Empty {}); + jwk.common.algorithm = Some(biscuit::jwa::Algorithm::ContentEncryption( + biscuit::jwa::ContentEncryptionAlgorithm::A256GCM, + )); + jwk.common.key_operations = Some(vec![ + biscuit::jwk::KeyOperations::Encrypt, + biscuit::jwk::KeyOperations::Decrypt, + ]); + let jwk_str = serde_json::to_string(&jwk)?; + + let public = create_tpm2b_public_sealed_object(policy_digest)?; + let (jwk_priv, jwk_pub) = + ctx.create_key(key_handle, &public, &[], jwk_str.as_bytes(), &[], &[])?; + + let jwk_priv = get_tpm2b_private(jwk_priv)?; + + let jwk_pub = get_tpm2b_public(jwk_pub)?; + + let hdr: biscuit::jwe::Header = biscuit::jwe::Header { + registered: biscuit::jwe::RegisteredHeader { + cek_algorithm: biscuit::jwa::KeyManagementAlgorithm::DirectSymmetricKey, + enc_algorithm: biscuit::jwa::ContentEncryptionAlgorithm::A256GCM, + compression_algorithm: None, + media_type: None, + content_type: None, + web_key_url: None, + web_key: None, + key_id: None, + x509_url: None, + x509_chain: None, + x509_fingerprint: None, + critical: None, + }, + cek_algorithm: biscuit::jwe::CekAlgorithmHeader { + nonce: None, + tag: None, + }, + private: ClevisHeader { + clevis: ClevisInner { + pin: pin_type.to_string(), + tpm2: Tpm2Inner { + hash: "sha256".to_string(), + 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 rand_nonce = ctx.get_random(12)?; + let jwe_enc_options = biscuit::jwa::EncryptionOptions::AES_GCM { nonce: rand_nonce }; + + let jwe_token = biscuit::jwe::Compact::new_decrypted(hdr, input.as_bytes().to_vec()); + let jwe_token_compact = jwe_token.encrypt(&jwk, &jwe_enc_options)?; + let encoded_token = jwe_token_compact.encrypted()?.encode(); + io::stdout().write_all(encoded_token.as_bytes())?; + + Ok(()) +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +struct Tpm2Inner { + hash: String, + #[serde( + deserialize_with = "deserialize_as_base64_url_no_pad", + serialize_with = "serialize_as_base64_url_no_pad" + )] + jwk_priv: Vec, + #[serde( + deserialize_with = "deserialize_as_base64_url_no_pad", + serialize_with = "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 = PinError; + + 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( + get_pcr_hash_alg_from_name(cfg.pcr_bank.as_ref()), + cfg.get_pcr_ids().unwrap(), + Box::new(TPMPolicyStep::NoStep), + )), + Box::new(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( + get_pcr_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() { + 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, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +struct ClevisHeader { + clevis: ClevisInner, +} + +impl CompactJson for Tpm2Inner {} +impl CompactJson for ClevisHeader {} +impl CompactJson for ClevisInner {} + +fn get_tpm2b_public(val: tss_esapi::tss2_esys::TPM2B_PUBLIC) -> Result, PinError> { + let mut offset = 0 as u64; + 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 u64, + &mut offset, + ); + if res != 0 { + return Err(PinError::Text("Marshalling tpm2b_public failed")); + } + resp.set_len(offset as usize); + } + + Ok(resp) +} + +fn get_tpm2b_private(val: tss_esapi::tss2_esys::TPM2B_PRIVATE) -> Result, PinError> { + let mut offset = 0 as u64; + 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 u64, + &mut offset, + ); + if res != 0 { + return Err(PinError::Text("Marshalling tpm2b_private failed")); + } + resp.set_len(offset as usize); + } + + Ok(resp) +} + +fn build_tpm2b_private(val: &[u8]) -> Result { + let mut resp = tss_esapi::tss2_esys::TPM2B_PRIVATE::default(); + let mut offset = 0 as u64; + + unsafe { + let res = tss_esapi::tss2_esys::Tss2_MU_TPM2B_PRIVATE_Unmarshal( + val[..].as_ptr(), + val.len() as u64, + &mut offset, + &mut resp, + ); + if res != 0 { + return Err(PinError::Text("Unmarshalling tpm2b_private failed")); + } + } + + Ok(resp) +} + +fn build_tpm2b_public(val: &[u8]) -> Result { + let mut resp = tss_esapi::tss2_esys::TPM2B_PUBLIC::default(); + let mut offset = 0 as u64; + + unsafe { + let res = tss_esapi::tss2_esys::Tss2_MU_TPM2B_PUBLIC_Unmarshal( + val[..].as_ptr(), + val.len() as u64, + &mut offset, + &mut resp, + ); + if res != 0 { + return Err(PinError::Text("Unmarshalling tpm2b_public failed")); + } + } + + Ok(resp) +} + +fn create_restricted_ecc_public() -> tss_esapi::tss2_esys::TPM2B_PUBLIC { + let ecc_params = utils::TpmsEccParmsBuilder::new_restricted_decryption_key( + utils::algorithm_specifiers::Cipher::aes_128_cfb(), + utils::algorithm_specifiers::EllipticCurve::NistP256, + ) + .build() + .unwrap(); + let mut object_attributes = utils::ObjectAttributes(0); + object_attributes.set_fixed_tpm(true); + object_attributes.set_fixed_parent(true); + object_attributes.set_sensitive_data_origin(true); + object_attributes.set_user_with_auth(true); + object_attributes.set_decrypt(true); + object_attributes.set_sign_encrypt(false); + object_attributes.set_restricted(true); + + utils::Tpm2BPublicBuilder::new() + .with_type(constants::TPM2_ALG_ECC) + .with_name_alg(constants::TPM2_ALG_SHA256) + .with_object_attributes(object_attributes) + .with_parms(utils::PublicParmsUnion::EccDetail(ecc_params)) + .build() + .unwrap() +} + +fn get_key_public(key_type: &str) -> Result { + match key_type { + "ecc" => Ok(create_restricted_ecc_public()), + "rsa" => Ok(tss_esapi::utils::create_restricted_decryption_rsa_public( + utils::algorithm_specifiers::Cipher::aes_128_cfb(), + 2048, + 0, + )?), + _ => Err(PinError::Text("Unsupported key type used")), + } +} + +#[derive(Debug, Serialize, Deserialize)] +enum SignedPolicyStep { + PCRs { + pcr_ids: Vec, + hash_algorithm: String, + #[serde( + deserialize_with = "deserialize_as_base64", + serialize_with = "serialize_as_base64" + )] + value: Vec, + }, +} + +#[derive(Debug, Serialize, Deserialize)] +struct SignedPolicy { + // policy_ref contains the policy_ref used in the aHash, used to determine the policy to use from a list + #[serde( + deserialize_with = "deserialize_as_base64", + serialize_with = "serialize_as_base64" + )] + policy_ref: Vec, + // steps contains the policy steps that are signed + steps: Vec, + // signature contains the signature over aHash + #[serde( + deserialize_with = "deserialize_as_base64", + serialize_with = "serialize_as_base64" + )] + signature: Vec, +} + +type SignedPolicyList = Vec; + +#[derive(Debug, Serialize, Deserialize)] +enum RSAPublicKeyScheme { + RSAPSS, + RSASSA, +} + +impl RSAPublicKeyScheme { + fn to_scheme(&self, hash_algo: &HashAlgo) -> tss_esapi::utils::AsymSchemeUnion { + match self { + RSAPublicKeyScheme::RSAPSS => { + tss_esapi::utils::AsymSchemeUnion::RSAPSS(hash_algo.into()) + } + RSAPublicKeyScheme::RSASSA => { + tss_esapi::utils::AsymSchemeUnion::RSASSA(hash_algo.into()) + } + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub enum HashAlgo { + SHA1, + SHA256, + SHA384, + SHA512, + SM3_256, + SHA3_256, + SHA3_384, + SHA3_512, +} + +impl HashAlgo { + fn to_tpmi_alg_hash(&self) -> tss_esapi::tss2_esys::TPMI_ALG_HASH { + let alg: tss_esapi::utils::algorithm_specifiers::HashingAlgorithm = self.into(); + alg.into() + } +} + +impl From<&HashAlgo> for tss_esapi::utils::algorithm_specifiers::HashingAlgorithm { + fn from(halg: &HashAlgo) -> Self { + match halg { + HashAlgo::SHA1 => tss_esapi::utils::algorithm_specifiers::HashingAlgorithm::Sha1, + HashAlgo::SHA256 => tss_esapi::utils::algorithm_specifiers::HashingAlgorithm::Sha256, + HashAlgo::SHA384 => tss_esapi::utils::algorithm_specifiers::HashingAlgorithm::Sha384, + HashAlgo::SHA512 => tss_esapi::utils::algorithm_specifiers::HashingAlgorithm::Sha512, + HashAlgo::SM3_256 => tss_esapi::utils::algorithm_specifiers::HashingAlgorithm::Sm3_256, + HashAlgo::SHA3_256 => { + tss_esapi::utils::algorithm_specifiers::HashingAlgorithm::Sha3_256 + } + HashAlgo::SHA3_384 => { + tss_esapi::utils::algorithm_specifiers::HashingAlgorithm::Sha3_384 + } + HashAlgo::SHA3_512 => { + tss_esapi::utils::algorithm_specifiers::HashingAlgorithm::Sha3_512 + } + } + } +} + +#[derive(Debug, Serialize, Deserialize)] +enum PublicKey { + RSA { + scheme: RSAPublicKeyScheme, + hashing_algo: HashAlgo, + exponent: u32, + #[serde( + deserialize_with = "deserialize_as_base64_url_no_pad", + serialize_with = "serialize_as_base64_url_no_pad" + )] + modulus: Vec, + }, +} + +impl PublicKey { + fn get_signing_scheme(&self) -> tss_esapi::utils::AsymSchemeUnion { + match self { + PublicKey::RSA { + scheme, + hashing_algo, + exponent: _, + modulus: _, + } => scheme.to_scheme(hashing_algo), + } + } +} + +impl TryFrom<&PublicKey> for tss_esapi::tss2_esys::TPM2B_PUBLIC { + type Error = PinError; + + fn try_from(publickey: &PublicKey) -> Result { + match publickey { + PublicKey::RSA { + scheme, + hashing_algo, + modulus, + exponent, + } => { + let mut object_attributes = tss_esapi::utils::ObjectAttributes(0); + object_attributes.set_fixed_tpm(false); + object_attributes.set_fixed_parent(false); + object_attributes.set_sensitive_data_origin(false); + object_attributes.set_user_with_auth(true); + object_attributes.set_decrypt(false); + object_attributes.set_sign_encrypt(true); + object_attributes.set_restricted(false); + + let len = modulus.len(); + let mut buffer = [0_u8; 512]; + buffer[..len].clone_from_slice(&modulus[..len]); + let rsa_uniq = Box::new(tss_esapi::tss2_esys::TPM2B_PUBLIC_KEY_RSA { + size: len as u16, + buffer, + }); + + Ok(tss_esapi::utils::Tpm2BPublicBuilder::new() + .with_type(tss_esapi::constants::TPM2_ALG_RSA) + .with_name_alg(hashing_algo.to_tpmi_alg_hash()) + .with_parms(tss_esapi::utils::PublicParmsUnion::RsaDetail( + tss_esapi::utils::TpmsRsaParmsBuilder::new_unrestricted_signing_key( + scheme.to_scheme(&hashing_algo), + (modulus.len() * 8) as u16, + *exponent, + ) + .build()?, + )) + .with_object_attributes(object_attributes) + .with_unique(tss_esapi::utils::PublicIdUnion::Rsa(rsa_uniq)) + .build()?) + } + } + } +} + +fn perform_decrypt(input: &str) -> Result<(), PinError> { + let token = biscuit::Compact::decode(input.trim()); + let hdr: biscuit::jwe::Header = token.part(0)?; + + if hdr.private.clevis.pin != "tpm2" && hdr.private.clevis.pin != "tpm2plus" { + return Err(PinError::Text("JWE pin mismatch")); + } + + let jwkpub = build_tpm2b_public(&hdr.private.clevis.tpm2.jwk_pub)?; + let jwkpriv = build_tpm2b_private(&hdr.private.clevis.tpm2.jwk_priv)?; + + let policy = TPMPolicyStep::try_from(&hdr.private.clevis.tpm2)?; + + let key_public = get_key_public(hdr.private.clevis.tpm2.key.as_str())?; + + let mut ctx = get_tpm2_ctx()?; + let key_handle = get_tpm2_primary_key(&mut ctx, &key_public)?; + + create_and_set_tpm2_session(&mut ctx, tss_esapi::constants::TPM2_SE_HMAC)?; + let key = ctx.load(key_handle, jwkpriv, jwkpub)?; + + policy.send_policy(&mut ctx, false)?; + + let unsealed = ctx.unseal(key)?; + let unsealed = &unsealed.value(); + let unsealed = std::str::from_utf8(unsealed)?; + let jwk: biscuit::jwk::JWK = serde_json::from_str(unsealed)?; + + let token: biscuit::jwe::Compact, biscuit::Empty> = jwe::Compact::Encrypted(token); + + let token = token.decrypt( + &jwk, + biscuit::jwa::KeyManagementAlgorithm::DirectSymmetricKey, + biscuit::jwa::ContentEncryptionAlgorithm::A256GCM, + )?; + // We just decrypted the token, there should be a payload + let payload = token.payload()?; + + io::stdout().write_all(payload)?; + + Ok(()) +} + +fn read_input_token() -> Result { + let mut buffer = String::new(); + io::stdin().read_to_string(&mut buffer)?; + if buffer.is_empty() { + return Err(PinError::Text("No data provided")); + } + Ok(buffer) +} + +fn main() { + let args: Vec = env::args().collect(); + let (mode, cfg) = match get_mode_and_cfg(&args) { + Err(e) => { + eprintln!("Error during parsing operation: {}", e); + std::process::exit(1); + } + Ok((mode, cfg)) => (mode, cfg), + }; + + let input = match read_input_token() { + Err(e) => { + eprintln!("Error getting input token: {}", e); + std::process::exit(1); + } + Ok(input) => input, + }; + + if let Err(e) = match mode { + ActionMode::Encrypt => perform_encrypt(cfg.unwrap(), &input), + ActionMode::Decrypt => perform_decrypt(&input), + } { + eprintln!("Error executing command: {}", e); + std::process::exit(2); + } +} + +fn get_tpm2_ctx() -> Result { + unsafe { Context::new(tcti::Tcti::Tabrmd(Default::default())) } +} + +fn get_tpm2_primary_key( + ctx: &mut Context, + pub_template: &tss_esapi::tss2_esys::TPM2B_PUBLIC, +) -> Result { + let cur_sessions = ctx.sessions(); + + create_and_set_tpm2_session(ctx, tss_esapi::constants::TPM2_SE_HMAC)?; + let key_handle = ctx.create_primary_key(ESYS_TR_RH_OWNER, pub_template, &[], &[], &[], &[])?; + + ctx.set_sessions(cur_sessions); + Ok(key_handle) +} diff --git a/tests/policy_broken.json b/tests/policy_broken.json new file mode 100644 index 0000000..005556a --- /dev/null +++ b/tests/policy_broken.json @@ -0,0 +1 @@ +[{"policy_ref":"","steps":[{"PCRs":{"pcr_ids":[0,1],"hash_algorithm":"SHA256","value":"i6a2jXsO3ybwuRGTLWrIJo99SGSyKCMx4ebbkM6ojYo="}}],"signature":"ap2GF35SunTFBE3YFXRO3zMz3dGHZ/kkxXeYPULF+7iOodP5veJLOLUkF95qZY8fkrXMT4MA5S6CqPkp5tw2PFchSsD2uyGk98tLlo9rR9kuGcalFiUL/9OVdO7Pchb/OJruUm7PmXy54SP+HITc4wihxavjPfC0ZrjW1yZAqWCukZTzE9NdyOQQV5Rmf2VdZTFyQ4zDVKkWkh3YSz4rDTI2WSl5Cos8EM7DwaPspiB3TUQUfxauUrUW/BojWMpynZs0jiO4UcjfLLiEN+4bOP3Wr7urxjALfBENF67oAtaQjLaXofNwXQ8+1fqZYG6R58ax8QALv1fgo+uSqAiQLQ=="}] \ No newline at end of file diff --git a/tests/policy_pubkey.json b/tests/policy_pubkey.json new file mode 100644 index 0000000..406136a --- /dev/null +++ b/tests/policy_pubkey.json @@ -0,0 +1 @@ +{"RSA":{"scheme":"RSASSA","hashing_algo":"SHA256","exponent":65537,"modulus":"5o5RSkQxeo9MqL1j8QAzlCmrpWbho9JA3HrR2XPuF-bLMRGUlZ45FrueMNwsUaurHVM7HdRNsdpHDujCtOzIDh3K5_K3b-pw5zUhCGGLQ_BVLBnXYLa8e-n15_RxnUwJbj3WfN0zZCAjj34zIjag6QbQOmOYZ58DaFzqo2KjnJbbGhOCZXxoT3N6m6-XmdRFB7DGNSKg5mrUh1gwl7pT9qQrGiIFzn35YBEtreOnOsWaLzr-v2NoNjTI3WMTjRB6DiGazIlNhI9uZixjHEk74H09LHwpLIQ77c_0rbgFBpNlDJ50HAt_8ifGJWl4V3_XhGum701i4YkWkfHr2Y2HdQ"}} \ No newline at end of file diff --git a/tests/policy_working.json b/tests/policy_working.json new file mode 100644 index 0000000..d5b4653 --- /dev/null +++ b/tests/policy_working.json @@ -0,0 +1 @@ +[{"policy_ref":"","steps":[{"PCRs":{"pcr_ids":[0,1],"hash_algorithm":"SHA256","value":"Oyx4fNrXgQllaAVP3nS5FFIr6wLQdftw8e+jGJGlCiQ="}}],"signature":"b0ANA+UUfeCWqq4DNfB4Ew386GBCNO5bwpGtiXqZf0nOuhyUoqhRIVOOMR1BzGD9dS+/ZloxtXNK1UjyF2f1kymNdNEi5x7m7D/qrWVyV0JAaFdosYUtek/GURvt41fswJV6D4n1gETkCEHG+ClYBPMXn9/1BlJRLgGClg83MefkHMRw3RrimwoF+/byCJXrCwLRBS1TT2pMHseMvhq29FBs/Bn1ZDPQ/DdmwEYPqyrtmkVhS+3yLgQzvK/XsYkoi38j2Uu7Cyv4eh7ZBbaS/+bSC18lqua0VvBauNfQ4AoDaVd3y7i2Rq+dhwZjhzE2k830Ec3ySqRE6JuAUNFCOw=="}] \ No newline at end of file diff --git a/tests/test_pcr b/tests/test_pcr new file mode 100755 index 0000000..2782394 --- /dev/null +++ b/tests/test_pcr @@ -0,0 +1,24 @@ +#!/bin/bash +cargo build || (echo "Failed to build"; exit 1) +echo "Working: no sealing" | ./target/debug/clevis-pin-tpm2 encrypt '{}' | ./target/debug/clevis-pin-tpm2 decrypt || (echo "Failed: no sealing"; exit 1) +echo "Working: no sealing (clevis decrypt)" | ./target/debug/clevis-pin-tpm2 encrypt '{}' | clevis decrypt || (echo "Failed: no sealing (clevis decrypt)"; exit 1) +echo "Working: no sealing (clevis encrypt)" | clevis encrypt tpm2 '{}' | ./target/debug/clevis-pin-tpm2 decrypt || (echo "Failed: no sealing (clevis encrypt)"; exit 1) +echo "Working: with PCRs" | ./target/debug/clevis-pin-tpm2 encrypt '{"pcr_ids":[23]}' | ./target/debug/clevis-pin-tpm2 decrypt || (echo "Failed: with PCRs"; exit 1) +echo "Working: with PCRs (clevis decrypt)" | ./target/debug/clevis-pin-tpm2 encrypt '{"pcr_ids":[23]}' | clevis decrypt || (echo "Failed: with PCRs (clevis decrypt)"; exit 1) +echo "Working: with PCRs (clevis encrypt)" | clevis encrypt tpm2 '{"pcr_ids":[23]}' | ./target/debug/clevis-pin-tpm2 decrypt || (echo "Failed: with PCRs (clevis encrypt)"; exit 1) +# Negative test (PCR change) +token=$(echo Failed | ./target/debug/clevis-pin-tpm2 encrypt '{"pcr_ids":[23]}') +tpm2_pcrevent -Q README.md 23 +res=$(echo "$token" | ./target/debug/clevis-pin-tpm2 decrypt 2>/dev/null) +ret=$? +if [ $ret == 0 -a "$res" == "Failed" ] +then + echo "Managed to decrypt after changing PCR" + exit 1 +elif [ $ret == 0 -o "$res" != "" ] +then + echo "Something went wrong" + exit 1 +else + echo "Working: with PCRs and change" +fi diff --git a/tests/test_policy b/tests/test_policy new file mode 100755 index 0000000..b52e418 --- /dev/null +++ b/tests/test_policy @@ -0,0 +1,18 @@ +#!/bin/bash +cargo build || (echo "Failed to build"; exit 1) +echo "Working: Policy" | ./target/debug/clevis-pin-tpm2 encrypt '{"policy_pubkey_path":"./tests/policy_pubkey.json", "policy_ref": "", "policy_path": "./tests/policy_working.json"}' | ./target/debug/clevis-pin-tpm2 decrypt +# Negative test (non-valid policy) +token=$(echo Failed | ./target/debug/clevis-pin-tpm2 encrypt '{"policy_pubkey_path":"./tests/policy_pubkey.json", "policy_ref": "", "policy_path": "./tests/policy_broken.json"}') +res=$(echo "$token" | ./target/debug/clevis-pin-tpm2 decrypt 2>/dev/null) +ret=$? +if [ $ret == 0 -a "$res" == "Failed" ] +then + echo "Managed to decrypt with invalid policy" + exit 1 +elif [ $ret == 0 -o "$res" != "" ] +then + echo "Something went wrong" + exit 1 +else + echo "Working: with policy" +fi