diff --git a/Cargo.lock b/Cargo.lock index 2a5fbf859..56e5e363e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1365,6 +1365,7 @@ dependencies = [ "reqwest-middleware", "reqwest-retry", "serde", + "serde_bytes", "serde_json", "static_assertions", "tempfile", diff --git a/Cargo.toml b/Cargo.toml index 95ca202a6..d006826e3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ reqwest-middleware = "0.5.1" reqwest-retry = "0.9.1" retry-policies = "0.5.0" serde = "1.0.80" +serde_bytes = "0.11" serde_derive = "1.0.80" serde_json = { version = "1.0", features = ["raw_value"] } signal-hook = "0.3" diff --git a/keylime/Cargo.toml b/keylime/Cargo.toml index bb3d92ac9..7da3ef14f 100644 --- a/keylime/Cargo.toml +++ b/keylime/Cargo.toml @@ -30,6 +30,7 @@ reqwest.workspace = true reqwest-middleware.workspace = true reqwest-retry.workspace = true serde.workspace = true +serde_bytes.workspace = true serde_json.workspace = true static_assertions.workspace = true tempfile.workspace = true diff --git a/keylime/src/tpm.rs b/keylime/src/tpm.rs index 0e5ac069f..996e515a3 100644 --- a/keylime/src/tpm.rs +++ b/keylime/src/tpm.rs @@ -611,6 +611,34 @@ fn hash_alg_to_string(hash_alg: TssEsapiHashingAlgorithm) -> Result { static TPM_CTX: OnceLock>> = OnceLock::new(); +// Some TPMs store the EK cert in NVRAM wrapped in an ASN.1 OCTET STRING +// (tag 0x04) instead of raw DER (tag 0x30). Unwrap it here so it can be +// parsed as a normal certificate. +fn unwrap_octet_string_ek_cert(cert: &[u8]) -> Vec { + if cert.first() != Some(&0x04) { + return cert.to_vec(); + } + + match picky_asn1_der::from_bytes::(cert) { + Ok(inner) if inner.first() == Some(&0x30) => inner.into_vec(), + Ok(_) => { + warn!( + "EK certificate is OCTET-STRING-wrapped, but the inner \ + content does not start with a SEQUENCE tag; using it \ + as-is" + ); + cert.to_vec() + } + Err(e) => { + warn!( + "EK certificate looks OCTET-STRING-wrapped, but could \ + not be unwrapped: {e}" + ); + cert.to_vec() + } + } +} + impl Context<'_> { /// Creates a connection context. pub fn new() -> Result { @@ -722,13 +750,16 @@ impl Context<'_> { }; let cert = match ek::retrieve_ek_pubcert(&mut ctx, alg.into()) { - Ok(cert) => match self.check_ek_cert(&cert) { - Ok(cert_checked) => Some(cert_checked), - Err(_) => { - warn!("EK certificate in TPM NVRAM is not ASN.1 DER encoded"); - Some(cert) + Ok(cert) => { + let cert = unwrap_octet_string_ek_cert(&cert); + match self.check_ek_cert(&cert) { + Ok(cert_checked) => Some(cert_checked), + Err(_) => { + warn!("EK certificate in TPM NVRAM is not ASN.1 DER encoded"); + Some(cert) // now uses the unwrapped cert + } } - }, + } Err(_) => { warn!("No EK certificate found in TPM NVRAM"); None @@ -3044,6 +3075,52 @@ pub mod tests { } } + fn test_cert_der() -> Vec { + use std::path::Path; + + let cert_path = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("test-data") + .join("test-cert.pem"); + let pem = + std::fs::read(cert_path).expect("unable to read test-cert.pem"); + X509::from_pem(&pem) + .expect("unable to parse test-cert.pem") + .to_der() + .expect("unable to re-encode test cert as DER") + } + + #[test] + fn test_unwrap_octet_string_ek_cert_wrapped() { + let cert_der = test_cert_der(); + assert_eq!(cert_der[0], 0x30); + + let wrapped = picky_asn1_der::to_vec(&serde_bytes::ByteBuf::from( + cert_der.clone(), + )) + .expect("failed to wrap cert in OCTET STRING"); + assert_eq!(wrapped[0], 0x04); + + assert_eq!(unwrap_octet_string_ek_cert(&wrapped), cert_der); + } + + #[test] + fn test_unwrap_octet_string_ek_cert_not_wrapped() { + let cert_der = test_cert_der(); + assert_eq!(unwrap_octet_string_ek_cert(&cert_der), cert_der); + } + + #[test] + fn test_unwrap_octet_string_ek_cert_invalid_inner() { + // wrapped, but inner content isn't a cert + let not_a_cert = b"not a certificate".to_vec(); + let wrapped = + picky_asn1_der::to_vec(&serde_bytes::ByteBuf::from(not_a_cert)) + .expect("failed to wrap non-cert data in OCTET STRING"); + assert_eq!(wrapped[0], 0x04); + + assert_eq!(unwrap_octet_string_ek_cert(&wrapped), wrapped); + } + #[test] #[cfg(feature = "testing")] fn test_quote_encode_decode() {