900 lines
30 KiB
Diff
900 lines
30 KiB
Diff
From d30000a1c3ae6cb8f6dc60c295e037614605b36e Mon Sep 17 00:00:00 2001
|
|
From: Daiki Ueno <dueno@redhat.com>
|
|
Date: Tue, 5 Jul 2022 16:24:32 +0900
|
|
Subject: [PATCH 1/4] algorithms: Add conversion between our hash algorithms
|
|
and OpenSSL's
|
|
|
|
Signed-off-by: Daiki Ueno <dueno@redhat.com>
|
|
---
|
|
src/algorithms.rs | 13 +++++++++++++
|
|
1 file changed, 13 insertions(+)
|
|
|
|
diff --git a/src/algorithms.rs b/src/algorithms.rs
|
|
index 414ef100..019f9360 100644
|
|
--- a/src/algorithms.rs
|
|
+++ b/src/algorithms.rs
|
|
@@ -1,5 +1,6 @@
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright 2021 Keylime Authors
|
|
+use openssl::hash::MessageDigest;
|
|
use serde::{Deserialize, Serialize};
|
|
use std::convert::TryFrom;
|
|
use std::fmt;
|
|
@@ -74,6 +75,18 @@ impl From<HashAlgorithm> for HashingAlgorithm {
|
|
}
|
|
}
|
|
|
|
+impl From<HashAlgorithm> for MessageDigest {
|
|
+ fn from(hash_algorithm: HashAlgorithm) -> Self {
|
|
+ match hash_algorithm {
|
|
+ HashAlgorithm::Sha1 => MessageDigest::sha1(),
|
|
+ HashAlgorithm::Sha256 => MessageDigest::sha256(),
|
|
+ HashAlgorithm::Sha384 => MessageDigest::sha384(),
|
|
+ HashAlgorithm::Sha512 => MessageDigest::sha512(),
|
|
+ HashAlgorithm::Sm3_256 => MessageDigest::sm3(),
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
pub enum EncryptionAlgorithm {
|
|
Rsa,
|
|
|
|
From a90aa5b0852d587114aca7a8e79306987c562624 Mon Sep 17 00:00:00 2001
|
|
From: Daiki Ueno <dueno@redhat.com>
|
|
Date: Tue, 5 Jul 2022 15:43:42 +0900
|
|
Subject: [PATCH 2/4] ima_entry: add IMA entry parser ported from Python
|
|
Keylime
|
|
|
|
Signed-off-by: Daiki Ueno <dueno@redhat.com>
|
|
---
|
|
src/ima_entry.rs | 516 +++++++++++++++++++++++++++++++++++++++++++++++
|
|
1 file changed, 516 insertions(+)
|
|
create mode 100644 src/ima_entry.rs
|
|
|
|
diff --git a/src/ima_entry.rs b/src/ima_entry.rs
|
|
new file mode 100644
|
|
index 00000000..7bb4a970
|
|
--- /dev/null
|
|
+++ b/src/ima_entry.rs
|
|
@@ -0,0 +1,516 @@
|
|
+// SPDX-License-Identifier: Apache-2.0
|
|
+// Copyright 2022 Keylime Authors
|
|
+
|
|
+// Parser for IMA ASCII entries.
|
|
+//
|
|
+// Implements the templates (modes) and types as defined in:
|
|
+// https://elixir.bootlin.com/linux/latest/source/security/integrity/ima/ima_template.c
|
|
+// https://www.kernel.org/doc/html/v5.12/security/IMA-templates.html
|
|
+
|
|
+use crate::algorithms::HashAlgorithm;
|
|
+use openssl::hash::MessageDigest;
|
|
+use std::convert::{TryFrom, TryInto};
|
|
+use std::io::{Error, ErrorKind, Result, Write};
|
|
+
|
|
+pub trait Encode {
|
|
+ fn encode(&self, writer: &mut dyn Write) -> Result<()>;
|
|
+}
|
|
+
|
|
+pub trait EncodeLegacy {
|
|
+ fn encode_legacy(&self, writer: &mut dyn Write) -> Result<()>;
|
|
+}
|
|
+
|
|
+#[derive(PartialEq)]
|
|
+pub struct Digest {
|
|
+ pub algorithm: HashAlgorithm,
|
|
+ value: Vec<u8>,
|
|
+}
|
|
+
|
|
+impl Digest {
|
|
+ pub fn new(algorithm: HashAlgorithm, value: &[u8]) -> Result<Self> {
|
|
+ let digest: MessageDigest = algorithm.into();
|
|
+ if value.len() != digest.size() {
|
|
+ return Err(Error::new(
|
|
+ ErrorKind::InvalidInput,
|
|
+ "invalid digest value",
|
|
+ ));
|
|
+ }
|
|
+ let mut v = Vec::with_capacity(digest.size());
|
|
+ v.extend_from_slice(value);
|
|
+ Ok(Self {
|
|
+ algorithm,
|
|
+ value: v,
|
|
+ })
|
|
+ }
|
|
+
|
|
+ pub fn value(&self) -> &[u8] {
|
|
+ &self.value
|
|
+ }
|
|
+
|
|
+ pub fn start(algorithm: HashAlgorithm) -> Self {
|
|
+ let digest: MessageDigest = algorithm.into();
|
|
+ Self {
|
|
+ algorithm,
|
|
+ value: vec![0x00u8; digest.size()],
|
|
+ }
|
|
+ }
|
|
+
|
|
+ pub fn ff(algorithm: HashAlgorithm) -> Self {
|
|
+ let digest: MessageDigest = algorithm.into();
|
|
+ Self {
|
|
+ algorithm,
|
|
+ value: vec![0xffu8; digest.size()],
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+impl TryFrom<&str> for Digest {
|
|
+ type Error = std::io::Error;
|
|
+
|
|
+ fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
|
|
+ let tokens: Vec<&str> = value.splitn(2, ':').collect();
|
|
+ if tokens.len() == 1 {
|
|
+ Ok(Digest {
|
|
+ algorithm: HashAlgorithm::Sha1,
|
|
+ value: hex::decode(&tokens[0]).map_err(|_| {
|
|
+ Error::new(
|
|
+ ErrorKind::InvalidInput,
|
|
+ "invalid hex encoding",
|
|
+ )
|
|
+ })?,
|
|
+ })
|
|
+ } else {
|
|
+ Ok(Digest {
|
|
+ algorithm: tokens[0].try_into().map_err(|_| {
|
|
+ Error::new(ErrorKind::InvalidInput, "invalid algorithm")
|
|
+ })?,
|
|
+ value: hex::decode(&tokens[1]).map_err(|_| {
|
|
+ Error::new(
|
|
+ ErrorKind::InvalidInput,
|
|
+ "invalid hex encoding",
|
|
+ )
|
|
+ })?,
|
|
+ })
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+impl Encode for Digest {
|
|
+ fn encode(&self, writer: &mut dyn Write) -> Result<()> {
|
|
+ if self.algorithm == HashAlgorithm::Sha1 {
|
|
+ writer.write_all(&(self.value.len() as u32).to_le_bytes())?;
|
|
+ writer.write_all(&self.value)?;
|
|
+ } else {
|
|
+ let algorithm = format!("{}", self.algorithm);
|
|
+ let total_len = algorithm.len() + 2 + self.value.len();
|
|
+ writer.write_all(&(total_len as u32).to_le_bytes())?;
|
|
+ writer.write_all(algorithm.as_bytes())?;
|
|
+ writer.write_all(&[58u8, 0u8])?;
|
|
+ writer.write_all(&self.value)?;
|
|
+ }
|
|
+ Ok(())
|
|
+ }
|
|
+}
|
|
+
|
|
+impl EncodeLegacy for Digest {
|
|
+ fn encode_legacy(&self, writer: &mut dyn Write) -> Result<()> {
|
|
+ writer.write_all(&self.value)?;
|
|
+ Ok(())
|
|
+ }
|
|
+}
|
|
+
|
|
+struct Name {
|
|
+ name: String,
|
|
+}
|
|
+
|
|
+impl TryFrom<&str> for Name {
|
|
+ type Error = std::io::Error;
|
|
+
|
|
+ fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
|
|
+ Ok(Self {
|
|
+ name: value.to_string(),
|
|
+ })
|
|
+ }
|
|
+}
|
|
+
|
|
+const TCG_EVENT_NAME_LEN_MAX: usize = 255;
|
|
+
|
|
+impl Encode for Name {
|
|
+ fn encode(&self, writer: &mut dyn Write) -> Result<()> {
|
|
+ let bytes = self.name.as_bytes();
|
|
+ writer.write_all(&((bytes.len() + 1) as u32).to_le_bytes())?;
|
|
+ writer.write_all(bytes)?;
|
|
+ writer.write_all(&[0u8])?; // NUL
|
|
+ Ok(())
|
|
+ }
|
|
+}
|
|
+
|
|
+impl EncodeLegacy for Name {
|
|
+ fn encode_legacy(&self, writer: &mut dyn Write) -> Result<()> {
|
|
+ let bytes = self.name.as_bytes();
|
|
+ writer.write_all(bytes)?;
|
|
+ writer.write_all(&vec![0u8; TCG_EVENT_NAME_LEN_MAX - bytes.len()])?;
|
|
+ Ok(())
|
|
+ }
|
|
+}
|
|
+
|
|
+struct Signature {
|
|
+ value: Vec<u8>,
|
|
+}
|
|
+
|
|
+impl TryFrom<&str> for Signature {
|
|
+ type Error = std::io::Error;
|
|
+
|
|
+ fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
|
|
+ let value = hex::decode(value).map_err(|_| {
|
|
+ Error::new(ErrorKind::InvalidInput, "invalid hex encoding")
|
|
+ })?;
|
|
+ // basic checks on signature
|
|
+ if value.len() < 9 {
|
|
+ return Err(Error::new(
|
|
+ ErrorKind::InvalidInput,
|
|
+ "invalid signature",
|
|
+ ));
|
|
+ }
|
|
+ let sig_size = u16::from_be_bytes(value[7..9].try_into().unwrap()); //#[allow_ci]
|
|
+ if (sig_size as usize) + 9 != value.len() {
|
|
+ return Err(Error::new(
|
|
+ ErrorKind::InvalidInput,
|
|
+ "invalid signature",
|
|
+ ));
|
|
+ }
|
|
+ Ok(Self { value })
|
|
+ }
|
|
+}
|
|
+
|
|
+impl Encode for Signature {
|
|
+ fn encode(&self, writer: &mut dyn Write) -> Result<()> {
|
|
+ writer.write_all(&(self.value.len() as u32).to_le_bytes())?;
|
|
+ writer.write_all(&self.value)?;
|
|
+ Ok(())
|
|
+ }
|
|
+}
|
|
+
|
|
+struct Buffer {
|
|
+ value: Vec<u8>,
|
|
+}
|
|
+
|
|
+impl TryFrom<&str> for Buffer {
|
|
+ type Error = std::io::Error;
|
|
+
|
|
+ fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
|
|
+ Ok(Self {
|
|
+ value: hex::decode(value).map_err(|_| {
|
|
+ Error::new(ErrorKind::InvalidInput, "invalid hex encoding")
|
|
+ })?,
|
|
+ })
|
|
+ }
|
|
+}
|
|
+
|
|
+impl Encode for Buffer {
|
|
+ fn encode(&self, writer: &mut dyn Write) -> Result<()> {
|
|
+ writer.write_all(&(self.value.len() as u32).to_le_bytes())?;
|
|
+ writer.write_all(&self.value)?;
|
|
+ Ok(())
|
|
+ }
|
|
+}
|
|
+
|
|
+pub trait EventData: Encode {
|
|
+ fn path(&self) -> &str;
|
|
+}
|
|
+
|
|
+struct Ima {
|
|
+ digest: Digest,
|
|
+ path: Name,
|
|
+}
|
|
+
|
|
+impl TryFrom<&str> for Ima {
|
|
+ type Error = std::io::Error;
|
|
+
|
|
+ fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
|
|
+ let tokens: Vec<&str> = value.splitn(2, ' ').collect();
|
|
+ if tokens.len() != 2 {
|
|
+ return Err(Error::new(ErrorKind::InvalidInput, value));
|
|
+ }
|
|
+ Ok(Self {
|
|
+ digest: Digest::try_from(tokens[0])?,
|
|
+ path: Name::try_from(tokens[1])?,
|
|
+ })
|
|
+ }
|
|
+}
|
|
+
|
|
+impl EventData for Ima {
|
|
+ fn path(&self) -> &str {
|
|
+ &self.path.name
|
|
+ }
|
|
+}
|
|
+
|
|
+impl Encode for Ima {
|
|
+ fn encode(&self, writer: &mut dyn Write) -> Result<()> {
|
|
+ self.digest.encode_legacy(writer)?;
|
|
+ self.path.encode_legacy(writer)?;
|
|
+ Ok(())
|
|
+ }
|
|
+}
|
|
+
|
|
+struct ImaNg {
|
|
+ digest: Digest,
|
|
+ path: Name,
|
|
+}
|
|
+
|
|
+impl TryFrom<&str> for ImaNg {
|
|
+ type Error = std::io::Error;
|
|
+
|
|
+ fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
|
|
+ let tokens: Vec<&str> = value.splitn(2, ' ').collect();
|
|
+ if tokens.len() != 2 {
|
|
+ return Err(Error::new(ErrorKind::InvalidInput, value));
|
|
+ }
|
|
+
|
|
+ Ok(Self {
|
|
+ digest: Digest::try_from(tokens[0])?,
|
|
+ path: Name::try_from(tokens[1])?,
|
|
+ })
|
|
+ }
|
|
+}
|
|
+
|
|
+impl EventData for ImaNg {
|
|
+ fn path(&self) -> &str {
|
|
+ &self.path.name
|
|
+ }
|
|
+}
|
|
+
|
|
+impl Encode for ImaNg {
|
|
+ fn encode(&self, writer: &mut dyn Write) -> Result<()> {
|
|
+ self.digest.encode(writer)?;
|
|
+ self.path.encode(writer)?;
|
|
+ Ok(())
|
|
+ }
|
|
+}
|
|
+
|
|
+struct ImaSig {
|
|
+ digest: Digest,
|
|
+ path: Name,
|
|
+ signature: Option<Signature>,
|
|
+}
|
|
+
|
|
+impl EventData for ImaSig {
|
|
+ fn path(&self) -> &str {
|
|
+ &self.path.name
|
|
+ }
|
|
+}
|
|
+
|
|
+impl TryFrom<&str> for ImaSig {
|
|
+ type Error = std::io::Error;
|
|
+
|
|
+ fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
|
|
+ // extract signature first
|
|
+ let (value, signature) = value
|
|
+ .rsplit_once(' ')
|
|
+ .ok_or_else(|| Error::new(ErrorKind::InvalidInput, value))?;
|
|
+
|
|
+ // parse d-ng|n-ng as in ima-ng
|
|
+ let tokens: Vec<&str> = value.splitn(2, ' ').collect();
|
|
+ if tokens.len() != 2 {
|
|
+ return Err(Error::new(ErrorKind::InvalidInput, value));
|
|
+ }
|
|
+
|
|
+ let digest = Digest::try_from(tokens[0])?;
|
|
+ let path = Name::try_from(tokens[1])?;
|
|
+ let signature = if !signature.is_empty() {
|
|
+ Some(Signature::try_from(signature)?)
|
|
+ } else {
|
|
+ None
|
|
+ };
|
|
+
|
|
+ Ok(Self {
|
|
+ digest,
|
|
+ path,
|
|
+ signature,
|
|
+ })
|
|
+ }
|
|
+}
|
|
+
|
|
+impl Encode for ImaSig {
|
|
+ fn encode(&self, writer: &mut dyn Write) -> Result<()> {
|
|
+ self.digest.encode(writer)?;
|
|
+ self.path.encode(writer)?;
|
|
+ if let Some(signature) = &self.signature {
|
|
+ signature.encode(writer)?;
|
|
+ } else {
|
|
+ writer.write_all(&0u32.to_le_bytes())?;
|
|
+ }
|
|
+ Ok(())
|
|
+ }
|
|
+}
|
|
+
|
|
+struct ImaBuf {
|
|
+ digest: Digest,
|
|
+ name: Name,
|
|
+ data: Buffer,
|
|
+}
|
|
+
|
|
+impl TryFrom<&str> for ImaBuf {
|
|
+ type Error = std::io::Error;
|
|
+
|
|
+ fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
|
|
+ let tokens: Vec<&str> = value.splitn(3, ' ').collect();
|
|
+ if tokens.len() != 3 {
|
|
+ return Err(Error::new(ErrorKind::InvalidInput, value));
|
|
+ }
|
|
+ Ok(Self {
|
|
+ digest: Digest::try_from(tokens[0])?,
|
|
+ name: Name::try_from(tokens[1])?,
|
|
+ data: Buffer::try_from(tokens[2])?,
|
|
+ })
|
|
+ }
|
|
+}
|
|
+
|
|
+impl EventData for ImaBuf {
|
|
+ fn path(&self) -> &str {
|
|
+ &self.name.name
|
|
+ }
|
|
+}
|
|
+
|
|
+impl Encode for ImaBuf {
|
|
+ fn encode(&self, writer: &mut dyn Write) -> Result<()> {
|
|
+ self.digest.encode(writer)?;
|
|
+ self.name.encode(writer)?;
|
|
+ self.data.encode(writer)?;
|
|
+ Ok(())
|
|
+ }
|
|
+}
|
|
+
|
|
+pub struct Entry {
|
|
+ pub template_hash: Digest,
|
|
+ pub event_data: Box<dyn EventData>,
|
|
+}
|
|
+
|
|
+impl TryFrom<&str> for Entry {
|
|
+ type Error = std::io::Error;
|
|
+
|
|
+ fn try_from(value: &str) -> std::result::Result<Self, Self::Error> {
|
|
+ let tokens: Vec<&str> = value.splitn(4, ' ').collect();
|
|
+ if tokens.len() != 4 {
|
|
+ return Err(Error::new(ErrorKind::InvalidInput, value));
|
|
+ }
|
|
+
|
|
+ let template_hash = Digest {
|
|
+ algorithm: HashAlgorithm::Sha1,
|
|
+ value: hex::decode(&tokens[1]).map_err(|_| {
|
|
+ Error::new(ErrorKind::InvalidInput, "invalid hex encoding")
|
|
+ })?,
|
|
+ };
|
|
+ let mode = tokens[2];
|
|
+ let event = tokens[3];
|
|
+
|
|
+ match mode {
|
|
+ "ima" => Ok(Self {
|
|
+ template_hash,
|
|
+ event_data: Box::new(Ima::try_from(event)?),
|
|
+ }),
|
|
+ "ima-ng" => Ok(Self {
|
|
+ template_hash,
|
|
+ event_data: Box::new(ImaNg::try_from(event)?),
|
|
+ }),
|
|
+ "ima-sig" => Ok(Self {
|
|
+ template_hash,
|
|
+ event_data: Box::new(ImaSig::try_from(event)?),
|
|
+ }),
|
|
+ "ima-buf" => Ok(Self {
|
|
+ template_hash,
|
|
+ event_data: Box::new(ImaBuf::try_from(event)?),
|
|
+ }),
|
|
+ template => Err(Error::new(
|
|
+ ErrorKind::Other,
|
|
+ format!("unrecognized template \"{}\"", template,),
|
|
+ )),
|
|
+ }
|
|
+ }
|
|
+}
|
|
+
|
|
+// Unit Testing
|
|
+#[cfg(test)]
|
|
+mod tests {
|
|
+ use super::*;
|
|
+
|
|
+ #[test]
|
|
+ fn test_parse_ima() {
|
|
+ let entry: Entry = "10 d7026dc672344d3ee372217bdbc7395947788671 ima 6f66d1d8e2fffcc12dfcb78c04b81fe5b8bbae4e /usr/bin/kmod"
|
|
+ .try_into().expect("unable to parse ima template");
|
|
+ assert_eq!(entry.event_data.path(), "/usr/bin/kmod");
|
|
+ let mut buf = vec![];
|
|
+ entry
|
|
+ .event_data
|
|
+ .encode(&mut buf)
|
|
+ .expect("unable to encode event data");
|
|
+ assert_eq!(
|
|
+ &buf,
|
|
+ &hex::decode("6f66d1d8e2fffcc12dfcb78c04b81fe5b8bbae4e2f7573722f62696e2f6b6d6f640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000").unwrap(), //#[allow_ci]
|
|
+ );
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn test_parse_ima_ng() {
|
|
+ let entry: Entry = "10 7936eb315fb4e74b99e7d461bc5c96049e1ee092 ima-ng sha1:bc026ae66d81713e4e852465e980784dc96651f8 /usr/lib/systemd/systemd"
|
|
+ .try_into().expect("unable to parse ima-ng template");
|
|
+ assert_eq!(entry.event_data.path(), "/usr/lib/systemd/systemd");
|
|
+ let mut buf = vec![];
|
|
+ entry
|
|
+ .event_data
|
|
+ .encode(&mut buf)
|
|
+ .expect("unable to encode event data");
|
|
+ assert_eq!(
|
|
+ &buf,
|
|
+ &hex::decode("14000000bc026ae66d81713e4e852465e980784dc96651f8190000002f7573722f6c69622f73797374656d642f73797374656d6400").unwrap(), //#[allow_ci]
|
|
+ );
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn test_parse_ima_sig() {
|
|
+ let entry: Entry = "10 06e804489a77ddab51b9ef27e17053c0e5d503bd ima-sig sha1:1cb84b12db45d7da8de58ba6744187db84082f0e /usr/bin/zmore 030202531f402500483046022100bff9c02dc7b270c83cc94bfec10eecd42831de2cdcb04f024369a14623bc3a91022100cc4d015ae932fb98d6846645ed7d1bb1afd4621ec9089bc087126f191886dd31"
|
|
+ .try_into().expect("unable to parse ima-sig template");
|
|
+ assert_eq!(entry.event_data.path(), "/usr/bin/zmore");
|
|
+ let mut buf = vec![];
|
|
+ entry
|
|
+ .event_data
|
|
+ .encode(&mut buf)
|
|
+ .expect("unable to encode event data");
|
|
+ assert_eq!(
|
|
+ &buf,
|
|
+ &hex::decode("140000001cb84b12db45d7da8de58ba6744187db84082f0e0f0000002f7573722f62696e2f7a6d6f72650051000000030202531f402500483046022100bff9c02dc7b270c83cc94bfec10eecd42831de2cdcb04f024369a14623bc3a91022100cc4d015ae932fb98d6846645ed7d1bb1afd4621ec9089bc087126f191886dd31").unwrap(), //#[allow_ci]
|
|
+ );
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn test_parse_ima_sig_missing() {
|
|
+ let entry: Entry = "10 5426cf3031a43f5bfca183d79950698a95a728f6 ima-sig sha256:f1125b940480d20ad841d26d5ea253edc0704b5ec1548c891edf212cb1a9365e /lib/modules/5.4.48-openpower1/kernel/drivers/usb/common/usb-common.ko "
|
|
+ .try_into().expect("unable to parse ima-sig template without signature");
|
|
+ assert_eq!(entry.event_data.path(), "/lib/modules/5.4.48-openpower1/kernel/drivers/usb/common/usb-common.ko");
|
|
+ let mut buf = vec![];
|
|
+ entry
|
|
+ .event_data
|
|
+ .encode(&mut buf)
|
|
+ .expect("unable to encode event data");
|
|
+ assert_eq!(
|
|
+ &buf,
|
|
+ &hex::decode("280000007368613235363a00f1125b940480d20ad841d26d5ea253edc0704b5ec1548c891edf212cb1a9365e470000002f6c69622f6d6f64756c65732f352e342e34382d6f70656e706f776572312f6b65726e656c2f647269766572732f7573622f636f6d6d6f6e2f7573622d636f6d6d6f6e2e6b6f0000000000").unwrap(), //#[allow_ci]
|
|
+ );
|
|
+ }
|
|
+
|
|
+ #[test]
|
|
+ fn test_parse_ima_buf() {
|
|
+ let entry: Entry = "10 b7862dbbf1383ac6c7cca7f02d981a081aacb1f1 ima-buf sha1:6e0e6fc8a188ef4f059638949adca4d221946906 device_resume 6e616d653d544553543b757569643d43525950542d5645524954592d39656633326535623635623034343234613561386562343436636630653731332d544553543b63617061636974793d303b6d616a6f723d3235333b6d696e6f723d303b6d696e6f725f636f756e743d313b6e756d5f746172676574733d313b6163746976655f7461626c655f686173683d346565383065333365353635643336333430356634303238393436653837623365396563306335383661666639656630656436663561653762656237326431333b"
|
|
+ .try_into().expect("unable to parse ima-buf template");
|
|
+ assert_eq!(entry.event_data.path(), "device_resume");
|
|
+ let mut buf = vec![];
|
|
+ entry
|
|
+ .event_data
|
|
+ .encode(&mut buf)
|
|
+ .expect("unable to encode event data");
|
|
+ assert_eq!(
|
|
+ &buf,
|
|
+ &hex::decode("140000006e0e6fc8a188ef4f059638949adca4d2219469060e0000006465766963655f726573756d6500ce0000006e616d653d544553543b757569643d43525950542d5645524954592d39656633326535623635623034343234613561386562343436636630653731332d544553543b63617061636974793d303b6d616a6f723d3235333b6d696e6f723d303b6d696e6f725f636f756e743d313b6e756d5f746172676574733d313b6163746976655f7461626c655f686173683d346565383065333365353635643336333430356634303238393436653837623365396563306335383661666639656630656436663561653762656237326431333b").unwrap(), //#[allow_ci]
|
|
+ );
|
|
+ }
|
|
+}
|
|
|
|
From 7decf3b0985235e28f36c1b445def535d615c8df Mon Sep 17 00:00:00 2001
|
|
From: Daiki Ueno <dueno@redhat.com>
|
|
Date: Sun, 15 May 2022 17:46:36 +0200
|
|
Subject: [PATCH 3/4] ima_emulator: Support PCR hash algorithms other than
|
|
SHA-1
|
|
|
|
This ports the recent changes to keylime_ima_emulator in the Python
|
|
stack, which allows PCR hash algorithms other than SHA-1 through the
|
|
command-line options.
|
|
|
|
Signed-off-by: Daiki Ueno <dueno@redhat.com>
|
|
---
|
|
src/algorithms.rs | 1 -
|
|
src/ima_emulator.rs | 176 ++++++++++++++++++++++++++++++--------------
|
|
2 files changed, 122 insertions(+), 55 deletions(-)
|
|
|
|
diff --git a/src/algorithms.rs b/src/algorithms.rs
|
|
index 019f9360..3386abd7 100644
|
|
--- a/src/algorithms.rs
|
|
+++ b/src/algorithms.rs
|
|
@@ -10,7 +10,6 @@ use tss_esapi::{
|
|
AsymmetricAlgorithm, HashingAlgorithm, SignatureSchemeAlgorithm,
|
|
},
|
|
structures::{HashScheme, SignatureScheme},
|
|
- tss2_esys::TPMT_SIG_SCHEME,
|
|
};
|
|
|
|
// This error needs to be public because we implement TryFrom for public types
|
|
diff --git a/src/ima_emulator.rs b/src/ima_emulator.rs
|
|
index ba590bbc..c09e41b6 100644
|
|
--- a/src/ima_emulator.rs
|
|
+++ b/src/ima_emulator.rs
|
|
@@ -1,28 +1,32 @@
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright 2021 Keylime Authors
|
|
|
|
+mod algorithms;
|
|
+use crate::algorithms::HashAlgorithm;
|
|
+mod ima_entry;
|
|
+use openssl::hash::{hash, MessageDigest};
|
|
+
|
|
use log::*;
|
|
|
|
-use std::convert::TryFrom;
|
|
+use clap::Parser;
|
|
+use std::collections::HashMap;
|
|
+use std::convert::{TryFrom, TryInto};
|
|
use std::fs::File;
|
|
use std::io::prelude::*;
|
|
use std::io::BufReader;
|
|
+use std::path::{Path, PathBuf};
|
|
|
|
use thiserror::Error;
|
|
|
|
use tss_esapi::{
|
|
abstraction::pcr,
|
|
handles::PcrHandle,
|
|
- interface_types::algorithm::HashingAlgorithm,
|
|
structures::{Digest, DigestValues, PcrSelectionListBuilder, PcrSlot},
|
|
Context, Tcti,
|
|
};
|
|
|
|
const IMA_ML: &str = "/sys/kernel/security/ima/ascii_runtime_measurements";
|
|
|
|
-const START_HASH: &[u8; 20] = &[0u8; 20];
|
|
-const FF_HASH: &[u8; 20] = &[0xffu8; 20];
|
|
-
|
|
#[derive(Error, Debug)]
|
|
enum ImaEmulatorError {
|
|
#[error("Invalid envvar")]
|
|
@@ -31,8 +35,14 @@ enum ImaEmulatorError {
|
|
TssEsapiError(#[from] tss_esapi::Error),
|
|
#[error("Decoding error")]
|
|
FromHexError(#[from] hex::FromHexError),
|
|
+ #[error("Algorithm error")]
|
|
+ AlgorithmError(#[from] algorithms::AlgorithmError),
|
|
+ #[error("OpenSSL error")]
|
|
+ OpenSSLError(#[from] openssl::error::ErrorStack),
|
|
#[error("I/O error")]
|
|
IoError(#[from] std::io::Error),
|
|
+ #[error("Integer parsing error")]
|
|
+ ParseInt(#[from] std::num::ParseIntError),
|
|
#[error("{0}")]
|
|
Other(String),
|
|
}
|
|
@@ -41,57 +51,75 @@ type Result<T> = std::result::Result<T, ImaEmulatorError>;
|
|
|
|
fn ml_extend(
|
|
context: &mut Context,
|
|
- ml: &str,
|
|
+ ml: &Path,
|
|
mut position: usize,
|
|
+ ima_hash_alg: HashAlgorithm,
|
|
+ pcr_hash_alg: HashAlgorithm,
|
|
search_hash: Option<&Digest>,
|
|
) -> Result<usize> {
|
|
let f = File::open(ml)?;
|
|
let mut reader = BufReader::new(f);
|
|
- let mut running_hash: Vec<u8> = Vec::from(&START_HASH[..]);
|
|
+ let ima_digest: MessageDigest = ima_hash_alg.into();
|
|
+ let ima_start_hash = ima_entry::Digest::start(ima_hash_alg);
|
|
+ let pcr_digest: MessageDigest = pcr_hash_alg.into();
|
|
+ let mut running_hash = ima_entry::Digest::start(pcr_hash_alg);
|
|
+ let ff_hash = ima_entry::Digest::ff(pcr_hash_alg);
|
|
for line in reader.by_ref().lines().skip(position) {
|
|
let line = line?;
|
|
if line.is_empty() {
|
|
continue;
|
|
}
|
|
- let tokens: Vec<&str> = line.splitn(5, ' ').collect();
|
|
- if tokens.len() < 5 {
|
|
- error!("invalid measurement list file line: -{}-", line);
|
|
- }
|
|
+
|
|
+ let entry: ima_entry::Entry = line.as_str().try_into()?;
|
|
+
|
|
position += 1;
|
|
|
|
- let path = tokens[4];
|
|
- let template_hash = hex::decode(tokens[1])?;
|
|
- let template_hash = if template_hash == START_HASH {
|
|
- Digest::try_from(&FF_HASH[..])
|
|
+ // Set correct hash for time of measure, time of use (ToMToU) errors
|
|
+ // and if a file is already opened for write.
|
|
+ // https://elixir.bootlin.com/linux/v5.12.12/source/security/integrity/ima/ima_main.c#L101
|
|
+ let pcr_template_hash = if entry.template_hash == ima_start_hash {
|
|
+ Digest::try_from(ff_hash.value())
|
|
} else {
|
|
- Digest::try_from(template_hash)
|
|
+ let mut event_data = vec![];
|
|
+ entry.event_data.encode(&mut event_data)?;
|
|
+ let pcr_event_hash = hash(pcr_digest, &event_data)?;
|
|
+ let ima_event_hash = hash(ima_digest, &event_data)?;
|
|
+ if ima_event_hash.as_ref() != entry.template_hash.value() {
|
|
+ return Err(ImaEmulatorError::Other(
|
|
+ "IMA template hash doesn't match".to_string(),
|
|
+ ));
|
|
+ }
|
|
+ Digest::try_from(pcr_event_hash.as_ref())
|
|
}?;
|
|
|
|
match search_hash {
|
|
None => {
|
|
println!(
|
|
"extending hash {} for {}",
|
|
- hex::encode(template_hash.value()),
|
|
- &path
|
|
+ hex::encode(pcr_template_hash.value()),
|
|
+ entry.event_data.path(),
|
|
);
|
|
let mut vals = DigestValues::new();
|
|
- vals.set(HashingAlgorithm::Sha1, template_hash);
|
|
- // TODO: Add support for other hash algorithms
|
|
+ vals.set(pcr_hash_alg.into(), pcr_template_hash);
|
|
context.execute_with_nullauth_session(|ctx| {
|
|
ctx.pcr_extend(PcrHandle::Pcr10, vals)
|
|
})?;
|
|
}
|
|
Some(search_hash) => {
|
|
- let mut hasher = openssl::sha::Sha1::new();
|
|
- hasher.update(&running_hash);
|
|
- hasher.update(&template_hash);
|
|
- running_hash = hasher.finish().into();
|
|
- let digest = Digest::try_from(running_hash.as_slice())?;
|
|
+ let mut hasher = openssl::hash::Hasher::new(pcr_digest)?;
|
|
+ hasher.update(running_hash.value())?;
|
|
+ hasher.update(&pcr_template_hash)?;
|
|
+ running_hash =
|
|
+ ima_entry::Digest::new(pcr_hash_alg, &hasher.finish()?)?;
|
|
+ let digest = Digest::try_from(running_hash.value())?;
|
|
let mut vals = DigestValues::new();
|
|
- vals.set(HashingAlgorithm::Sha1, digest.clone());
|
|
+ vals.set(pcr_hash_alg.into(), digest.clone());
|
|
|
|
if digest == *search_hash {
|
|
- println!("Located last IMA file updated: {}", path);
|
|
+ println!(
|
|
+ "Located last IMA file updated: {}",
|
|
+ entry.event_data.path()
|
|
+ );
|
|
return Ok(position);
|
|
}
|
|
}
|
|
@@ -106,7 +134,20 @@ fn ml_extend(
|
|
Ok(position)
|
|
}
|
|
|
|
+#[derive(Parser)]
|
|
+#[clap(about)]
|
|
+struct Args {
|
|
+ #[clap(long = "hash_algs", short = 'a', default_value = "sha1")]
|
|
+ hash_algs: Vec<String>,
|
|
+ #[clap(long, short = 'i', default_value = "sha1")]
|
|
+ ima_hash_alg: String,
|
|
+ #[clap(long, short = 'f', default_value = IMA_ML)]
|
|
+ ima_log: PathBuf,
|
|
+}
|
|
+
|
|
fn main() -> std::result::Result<(), ImaEmulatorError> {
|
|
+ let args = Args::parse();
|
|
+
|
|
let tcti =
|
|
match Tcti::from_environment_variable() {
|
|
Ok(tcti) => tcti,
|
|
@@ -124,37 +165,64 @@ fn main() -> std::result::Result<(), ImaEmulatorError> {
|
|
));
|
|
}
|
|
|
|
- // check if pcr is clean
|
|
- let pcr_list = PcrSelectionListBuilder::new()
|
|
- .with_selection(HashingAlgorithm::Sha1, &[PcrSlot::Slot10])
|
|
- .build()?;
|
|
- let pcr_data = context
|
|
- .execute_without_session(|ctx| pcr::read_all(ctx, pcr_list))?;
|
|
- let digest = pcr_data
|
|
- .pcr_bank(HashingAlgorithm::Sha1)
|
|
- .ok_or_else(|| {
|
|
- ImaEmulatorError::Other(
|
|
- "IMA slot does not have SHA-1 bank".to_string(),
|
|
- )
|
|
- })?
|
|
- .get_digest(PcrSlot::Slot10)
|
|
- .ok_or_else(|| {
|
|
- ImaEmulatorError::Other(
|
|
- "could not read value from IMA PCR".to_string(),
|
|
- )
|
|
- })?;
|
|
-
|
|
- let mut pos = 0;
|
|
-
|
|
- if digest.value() != START_HASH {
|
|
- log::warn!("IMA PCR is not empty, trying to find the last updated file in the measurement list...");
|
|
- pos = ml_extend(&mut context, IMA_ML, 0, Some(digest))?;
|
|
+ let ima_hash_alg: HashAlgorithm =
|
|
+ args.ima_hash_alg.as_str().try_into()?;
|
|
+ let mut positions = HashMap::new();
|
|
+ for pcr_hash_alg in args.hash_algs {
|
|
+ let pcr_hash_alg: HashAlgorithm = pcr_hash_alg.as_str().try_into()?;
|
|
+ positions.insert(pcr_hash_alg, 0usize);
|
|
+ }
|
|
+
|
|
+ for (pcr_hash_alg, position) in positions.iter_mut() {
|
|
+ // check if pcr is clean
|
|
+ let pcr_list = PcrSelectionListBuilder::new()
|
|
+ .with_selection((*pcr_hash_alg).into(), &[PcrSlot::Slot10])
|
|
+ .build()?;
|
|
+ let pcr_data = context
|
|
+ .execute_without_session(|ctx| pcr::read_all(ctx, pcr_list))?;
|
|
+ let digest = pcr_data
|
|
+ .pcr_bank((*pcr_hash_alg).into())
|
|
+ .ok_or_else(|| {
|
|
+ ImaEmulatorError::Other(format!(
|
|
+ "IMA slot does not have {} bank",
|
|
+ *pcr_hash_alg,
|
|
+ ))
|
|
+ })?
|
|
+ .get_digest(PcrSlot::Slot10)
|
|
+ .ok_or_else(|| {
|
|
+ ImaEmulatorError::Other(
|
|
+ "could not read value from IMA PCR".to_string(),
|
|
+ )
|
|
+ })?;
|
|
+
|
|
+ let pcr_digest: MessageDigest = (*pcr_hash_alg).into();
|
|
+ let pcr_start_hash = vec![0x00u8; pcr_digest.size()];
|
|
+ if digest.value() != pcr_start_hash {
|
|
+ log::warn!("IMA PCR is not empty, trying to find the last updated file in the measurement list...");
|
|
+ *position = ml_extend(
|
|
+ &mut context,
|
|
+ &args.ima_log,
|
|
+ *position,
|
|
+ ima_hash_alg,
|
|
+ *pcr_hash_alg,
|
|
+ Some(digest),
|
|
+ )?;
|
|
+ }
|
|
}
|
|
|
|
- println!("Monitoring {}", IMA_ML);
|
|
+ println!("Monitoring {}", args.ima_log.display());
|
|
|
|
loop {
|
|
- pos = ml_extend(&mut context, IMA_ML, pos, None)?;
|
|
+ for (pcr_hash_alg, position) in positions.iter_mut() {
|
|
+ *position = ml_extend(
|
|
+ &mut context,
|
|
+ &args.ima_log,
|
|
+ *position,
|
|
+ ima_hash_alg,
|
|
+ *pcr_hash_alg,
|
|
+ None,
|
|
+ )?;
|
|
+ }
|
|
|
|
// FIXME: We could poll IMA_ML as in the python implementation, though
|
|
// the file is not pollable:
|
|
|
|
From d0e2dd7dc9f24df1eb5ff3f8cde95e5e561992d6 Mon Sep 17 00:00:00 2001
|
|
From: Karel Srot <ksrot@redhat.com>
|
|
Date: Wed, 6 Jul 2022 22:02:53 +0200
|
|
Subject: [PATCH 4/4] Enable usage of Rust IMA emulator in E2E tests.
|
|
|
|
Instalation of Rust IMA emulator has been implemented in
|
|
https://github.com/RedHat-SP-Security/keylime-tests/pull/141
|
|
|
|
Signed-off-by: Karel Srot <ksrot@redhat.com>
|
|
---
|
|
packit-ci.fmf | 1 +
|
|
1 file changed, 1 insertion(+)
|
|
|
|
diff --git a/packit-ci.fmf b/packit-ci.fmf
|
|
index 9a9f9581..064bc0ae 100644
|
|
--- a/packit-ci.fmf
|
|
+++ b/packit-ci.fmf
|
|
@@ -4,6 +4,7 @@
|
|
|
|
environment:
|
|
TPM_BINARY_MEASUREMENTS: /var/tmp/binary_bios_measurements
|
|
+ RUST_IMA_EMULATOR: 1
|
|
|
|
prepare:
|
|
how: shell
|