diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e769a38..eb36cf2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,39 @@ jobs: toolchain: ${{matrix.rust}} - run: cargo test --all + test_fedora: + name: Test on Fedora + runs-on: ubuntu-latest + container: fedora:latest + steps: + - uses: actions/checkout@v2 + - name: Install dependencies + run: | + dnf install -y \ + tpm2-tss-devel \ + swtpm swtpm-tools \ + rust cargo clippy + - name: Start swtpm + run: | + mkdir /tmp/tpmdir + swtpm_setup --tpm2 \ + --tpmstate /tmp/tpmdir \ + --createek --decryption --create-ek-cert \ + --create-platform-cert \ + --display + swtpm socket --tpm2 \ + --tpmstate dir=/tmp/tpmdir \ + --flags startup-clear \ + --ctrl type=tcp,port=2322 \ + --server type=tcp,port=2321 \ + --daemon + - name: Run tests + run: | + TCTI=swtpm: cargo test --features key_tpm,key_openssl_pkey + - name: Run clippy + run: | + cargo clippy --features key_tpm,key_openssl_pkey --all + clippy: name: Clippy runs-on: ubuntu-latest diff --git a/Cargo.toml b/Cargo.toml index 17a9f27..1b44c91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ serde_repr = "0.1" serde_bytes = "0.11" serde_with = { version = "1.5", default_features = false } openssl = "0.10" +tss-esapi = { version = "6.1", optional = true } [dependencies.serde] version = "1.0" @@ -23,3 +24,4 @@ features = ["derive"] [features] default = ["key_openssl_pkey"] key_openssl_pkey = [] +key_tpm = ["tss-esapi"] diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs index 9adf66a..bb14eb7 100644 --- a/src/crypto/mod.rs +++ b/src/crypto/mod.rs @@ -6,6 +6,8 @@ use crate::{error::CoseError, sign::SignatureAlgorithm}; #[cfg(feature = "key_openssl_pkey")] mod openssl_pkey; +#[cfg(feature = "key_tpm")] +pub mod tpm; /// A public key that can verify an existing signature pub trait SigningPublicKey { @@ -45,6 +47,25 @@ pub fn ec_curve_to_parameters( )) } +fn merge_ec_signature(bytes_r: &[u8], bytes_s: &[u8], key_length: usize) -> Vec { + assert!(bytes_r.len() <= key_length); + assert!(bytes_s.len() <= key_length); + + let mut signature_bytes = vec![0u8; key_length * 2]; + + // This is big-endian encoding so padding might be added at the start if the factor is + // too short. + let offset_copy = key_length - bytes_r.len(); + signature_bytes[offset_copy..offset_copy + bytes_r.len()].copy_from_slice(bytes_r); + + // This is big-endian encoding so padding might be added at the start if the factor is + // too short. + let offset_copy = key_length - bytes_s.len() + key_length; + signature_bytes[offset_copy..offset_copy + bytes_s.len()].copy_from_slice(bytes_s); + + signature_bytes +} + /// A private key that can produce new signatures pub trait SigningPrivateKey: SigningPublicKey { /// Given a digest, returns a signature diff --git a/src/crypto/openssl_pkey.rs b/src/crypto/openssl_pkey.rs index 9fee8a1..2794507 100644 --- a/src/crypto/openssl_pkey.rs +++ b/src/crypto/openssl_pkey.rs @@ -100,22 +100,6 @@ where let bytes_r = signature.r().to_vec(); let bytes_s = signature.s().to_vec(); - // These should *never* exceed ceiling(key_length / 8) - assert!(bytes_r.len() <= key_length); - assert!(bytes_s.len() <= key_length); - - let mut signature_bytes = vec![0u8; key_length * 2]; - - // This is big-endian encoding so padding might be added at the start if the factor is - // too short. - let offset_copy = key_length - bytes_r.len(); - signature_bytes[offset_copy..offset_copy + bytes_r.len()].copy_from_slice(&bytes_r); - - // This is big-endian encoding so padding might be added at the start if the factor is - // too short. - let offset_copy = key_length - bytes_s.len() + key_length; - signature_bytes[offset_copy..offset_copy + bytes_s.len()].copy_from_slice(&bytes_s); - - Ok(signature_bytes) + Ok(super::merge_ec_signature(&bytes_r, &bytes_s, key_length)) } } diff --git a/src/crypto/tpm.rs b/src/crypto/tpm.rs new file mode 100644 index 0000000..e7b264f --- /dev/null +++ b/src/crypto/tpm.rs @@ -0,0 +1,184 @@ +//! TPM implementation for cryptography + +use std::{cell::RefCell, convert::TryInto}; + +use openssl::hash::MessageDigest; +use tss_esapi::{ + constants::{ + self as tpm_constants, + response_code::{FormatOneResponseCode, Tss2ResponseCode}, + }, + handles::KeyHandle, + interface_types::algorithm::HashingAlgorithm, + tss2_esys::{TPMT_PUBLIC, TPMT_SIG_SCHEME, TPMT_TK_HASHCHECK}, + utils::{AsymSchemeUnion, Signature, SignatureData}, + Context, Error as tpm_error, +}; + +use crate::{ + crypto::{SigningPrivateKey, SigningPublicKey}, + error::CoseError, + sign::SignatureAlgorithm, +}; + +const TSS2_RC_SIGNATURE: u32 = tpm_constants::tss::TPM2_RC_SIGNATURE + | tpm_constants::tss::TPM2_RC_2 + | tpm_constants::tss::TPM2_RC_P; + +/// A reference to a TPM key and corresponding context +pub struct TpmKey { + context: RefCell, + key_handle: KeyHandle, + + parameters: (SignatureAlgorithm, MessageDigest), + hash_alg: HashingAlgorithm, + key_length: usize, +} + +impl TpmKey { + fn public_to_parameters( + public: TPMT_PUBLIC, + ) -> Result<((SignatureAlgorithm, MessageDigest), HashingAlgorithm, usize), CoseError> { + match public.type_ { + tpm_constants::tss::TPM2_ALG_ECDSA => {} + tpm_constants::tss::TPM2_ALG_ECC => {} + type_ => { + return Err(CoseError::UnsupportedError(format!( + "Key algorithm {} is not supported, only ECDSA is currently supported", + type_ + ))) + } + } + // This is safe to do, because we checked the type above + let params = unsafe { public.parameters.eccDetail }; + let (param_sig_alg, key_length) = match params.curveID { + tpm_constants::tss::TPM2_ECC_NIST_P256 => (SignatureAlgorithm::ES256, 32), + tpm_constants::tss::TPM2_ECC_NIST_P384 => (SignatureAlgorithm::ES384, 48), + tpm_constants::tss::TPM2_ECC_NIST_P521 => (SignatureAlgorithm::ES512, 66), + curve_id => { + return Err(CoseError::UnsupportedError(format!( + "Key curve {} is not supported", + curve_id + ))) + } + }; + match params.scheme.scheme { + tpm_constants::tss::TPM2_ALG_ECDSA => {} + scheme => { + return Err(CoseError::UnsupportedError(format!( + "Key scheme {} is not supported", + scheme + ))) + } + } + + let scheme = unsafe { params.scheme.details.ecdsa }; + let param_hash_alg = match scheme.hashAlg { + tpm_constants::tss::TPM2_ALG_SHA256 => MessageDigest::sha256(), + tpm_constants::tss::TPM2_ALG_SHA384 => MessageDigest::sha384(), + tpm_constants::tss::TPM2_ALG_SHA512 => MessageDigest::sha512(), + hash_alg => { + return Err(CoseError::UnsupportedError(format!( + "Key hash alg {} is not supported", + hash_alg + ))) + } + }; + let hash_alg = scheme.hashAlg.try_into().map_err(|_| { + CoseError::UnsupportedError("Unsupported hashing algorithm".to_string()) + })?; + + Ok(((param_sig_alg, param_hash_alg), hash_alg, key_length)) + } + + /// Create a new TpmKey from a TPM Context and KeyHandle + pub fn new(mut context: Context, key_handle: KeyHandle) -> Result { + let (key_public, _, _) = context + .read_public(key_handle) + .map_err(CoseError::TpmError)?; + let (parameters, hash_alg, key_length) = + TpmKey::public_to_parameters(key_public.publicArea)?; + + Ok(TpmKey { + context: RefCell::new(context), + key_handle, + + parameters, + hash_alg, + key_length, + }) + } +} + +impl SigningPublicKey for TpmKey { + fn get_parameters(&self) -> Result<(SignatureAlgorithm, MessageDigest), CoseError> { + Ok(self.parameters) + } + + fn verify(&self, data: &[u8], signature: &[u8]) -> Result { + // Recover the R and S factors from the signature contained in the object + let (bytes_r, bytes_s) = signature.split_at(self.key_length); + + let signature = Signature { + scheme: AsymSchemeUnion::ECDSA(self.hash_alg), + signature: SignatureData::EcdsaSignature { + r: bytes_r.to_vec(), + s: bytes_s.to_vec(), + }, + }; + + let data = data.try_into().map_err(|_| { + CoseError::UnsupportedError("Invalid digest passed to verify".to_string()) + })?; + + let mut context = self.context.borrow_mut(); + + match context.verify_signature(self.key_handle, &data, signature) { + Ok(_) => Ok(true), + Err(tpm_error::Tss2Error(Tss2ResponseCode::FormatOne(FormatOneResponseCode( + TSS2_RC_SIGNATURE, + )))) => Ok(false), + Err(e) => Err(CoseError::TpmError(e)), + } + } +} + +impl SigningPrivateKey for TpmKey { + fn sign(&self, data: &[u8]) -> Result, CoseError> { + let scheme = TPMT_SIG_SCHEME { + scheme: tpm_constants::tss::TPM2_ALG_NULL, + details: Default::default(), + }; + let validation = TPMT_TK_HASHCHECK { + tag: tpm_constants::tss::TPM2_ST_HASHCHECK, + hierarchy: tpm_constants::tss::TPM2_RH_NULL, + digest: Default::default(), + }; + + let data = data + .try_into() + .map_err(|_| CoseError::UnsupportedError("Tried to sign invalid data".to_string()))?; + + let signature = { + let mut context = self.context.borrow_mut(); + + context + .sign( + self.key_handle, + &data, + scheme, + validation.try_into().expect("Unable to convert validation"), + ) + .map_err(CoseError::TpmError)? + }; + + match &signature.signature { + SignatureData::EcdsaSignature { r, s } => { + Ok(super::merge_ec_signature(r, s, self.key_length)) + } + _ => Err(CoseError::UnsupportedError( + "Unsupported signature data returned".to_string(), + )), + } + } +} diff --git a/src/error.rs b/src/error.rs index 8186bd4..cbb4fc8 100644 --- a/src/error.rs +++ b/src/error.rs @@ -26,6 +26,9 @@ pub enum CoseError { TagError(Option), /// Encryption could not be performed due to OpenSSL error. EncryptionError(openssl::error::ErrorStack), + /// TPM error occured + #[cfg(feature = "key_tpm")] + TpmError(tss_esapi::Error), } impl fmt::Display for CoseError { @@ -40,6 +43,8 @@ impl fmt::Display for CoseError { CoseError::TagError(Some(tag)) => write!(f, "Tag {} was not expected", tag), CoseError::TagError(None) => write!(f, "Expected tag is missing"), CoseError::EncryptionError(e) => write!(f, "Encryption error: {}", e), + #[cfg(feature = "key_tpm")] + CoseError::TpmError(e) => write!(f, "TPM error: {}", e), } } } diff --git a/src/sign.rs b/src/sign.rs index c9cdf45..6426ac0 100644 --- a/src/sign.rs +++ b/src/sign.rs @@ -1144,4 +1144,137 @@ mod tests { } } } + + #[cfg(feature = "key_tpm")] + mod tpm { + use super::TEXT; + use crate::{crypto::tpm::TpmKey, sign::*}; + + use tss_esapi::{ + attributes::SessionAttributesBuilder, + constants::SessionType, + interface_types::{ + algorithm::HashingAlgorithm, ecc::EccCurve, resource_handles::Hierarchy, + }, + structures::SymmetricDefinition, + utils::{create_unrestricted_signing_ecc_public, AsymSchemeUnion}, + Context, Tcti, + }; + + #[test] + fn cose_sign_tpm() { + let mut tpm_context = + Context::new(Tcti::from_environment_variable().expect("Failed to get TCTI")) + .expect("Failed to create context"); + let tpm_session = tpm_context + .start_auth_session( + None, + None, + None, + SessionType::Hmac, + SymmetricDefinition::AES_128_CFB, + HashingAlgorithm::Sha256, + ) + .expect("Error creating TPM session") + .expect("Expected AuthSession"); + let (session_attrs, session_attrs_mask) = SessionAttributesBuilder::new() + .with_decrypt(true) + .with_encrypt(true) + .build(); + tpm_context + .tr_sess_set_attributes(tpm_session, session_attrs, session_attrs_mask) + .expect("Error setting session attributes"); + tpm_context.set_sessions((Some(tpm_session), None, None)); + let prim_key = tpm_context + .create_primary( + Hierarchy::Owner, + &create_unrestricted_signing_ecc_public( + AsymSchemeUnion::ECDSA(HashingAlgorithm::Sha256), + EccCurve::NistP256, + ) + .expect("Error creating TPM2B_PUBLIC"), + None, + None, + None, + None, + ) + .expect("Unable to create primary key") + .key_handle; + let mut tpm_key = TpmKey::new(tpm_context, prim_key).expect("Error creating TpmKey"); + + let mut map = HeaderMap::new(); + map.insert(CborValue::Integer(4), CborValue::Bytes(b"11".to_vec())); + let cose_doc1 = CoseSign1::new(TEXT, &map, &mut tpm_key).unwrap(); + let tagged_bytes = cose_doc1.as_bytes(true).unwrap(); + + // Tag 6.18 should be present + assert_eq!(tagged_bytes[0], 6 << 5 | 18); + let cose_doc2 = CoseSign1::from_bytes(&tagged_bytes).unwrap(); + + assert_eq!( + cose_doc1.get_payload(None).unwrap(), + cose_doc2.get_payload(Some(&mut tpm_key)).unwrap() + ); + } + + #[test] + fn cose_sign_tpm_invalid_signature() { + let mut tpm_context = + Context::new(Tcti::from_environment_variable().expect("Failed to get TCTI")) + .expect("Failed to create context"); + let tpm_session = tpm_context + .start_auth_session( + None, + None, + None, + SessionType::Hmac, + SymmetricDefinition::AES_128_CFB, + HashingAlgorithm::Sha256, + ) + .expect("Error creating TPM session") + .expect("Expected AuthSession"); + let (session_attrs, session_attrs_mask) = SessionAttributesBuilder::new() + .with_decrypt(true) + .with_encrypt(true) + .build(); + tpm_context + .tr_sess_set_attributes(tpm_session, session_attrs, session_attrs_mask) + .expect("Error setting session attributes"); + tpm_context.set_sessions((Some(tpm_session), None, None)); + let prim_key = tpm_context + .create_primary( + Hierarchy::Owner, + &create_unrestricted_signing_ecc_public( + AsymSchemeUnion::ECDSA(HashingAlgorithm::Sha256), + EccCurve::NistP256, + ) + .expect("Error creating TPM2B_PUBLIC"), + None, + None, + None, + None, + ) + .expect("Unable to create primary key") + .key_handle; + let mut tpm_key = TpmKey::new(tpm_context, prim_key).expect("Error creating TpmKey"); + + let mut map = HeaderMap::new(); + map.insert(CborValue::Integer(4), CborValue::Bytes(b"11".to_vec())); + let mut cose_doc1 = CoseSign1::new(TEXT, &map, &mut tpm_key).unwrap(); + + // Mangle the signature + cose_doc1.signature[0] = 0; + + let tagged_bytes = cose_doc1.as_bytes(true).unwrap(); + let cose_doc2 = CoseSign1::from_bytes(&tagged_bytes).unwrap(); + + match cose_doc2.get_payload(Some(&mut tpm_key)) { + Ok(_) => panic!("Did not fail"), + Err(CoseError::UnverifiedSignature) => {} + Err(e) => { + panic!("Unexpected error: {:?}", e) + } + } + } + } }