use core::{fmt, hash::Hash, str::FromStr};
use hex::FromHexError;
use sha2::{Digest, Sha256, Sha512, digest::FixedOutputReset, digest::Output};
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout, Unaligned};
pub trait FsVerityHashValue
where
Self: Clone,
Self: From<Output<Self::Digest>>,
Self: FromBytes + Immutable + IntoBytes + KnownLayout + Unaligned,
Self: Hash + Eq,
Self: fmt::Debug,
Self: Send + Sync + Unpin + 'static,
{
type Digest: Digest + FixedOutputReset + fmt::Debug;
const ALGORITHM: Algorithm;
const EMPTY: Self;
fn from_hex(hex: impl AsRef<[u8]>) -> Result<Self, FromHexError> {
let mut value = Self::EMPTY;
hex::decode_to_slice(hex.as_ref(), value.as_mut_bytes())?;
Ok(value)
}
fn from_object_dir_and_basename(
dirnum: u8,
basename: impl AsRef<[u8]>,
) -> Result<Self, FromHexError> {
let expected_size = 2 * (size_of::<Self>() - 1);
let bytes = basename.as_ref();
if bytes.len() != expected_size {
return Err(FromHexError::InvalidStringLength);
}
let mut result = Self::EMPTY;
result.as_mut_bytes()[0] = dirnum;
hex::decode_to_slice(bytes, &mut result.as_mut_bytes()[1..])?;
Ok(result)
}
fn from_object_pathname(pathname: impl AsRef<[u8]>) -> Result<Self, FromHexError> {
let min_size = 2 * size_of::<Self>() + 1;
let bytes = pathname.as_ref();
if bytes.len() < min_size {
return Err(FromHexError::InvalidStringLength);
}
let trailing = &bytes[bytes.len() - min_size..];
let mut result = Self::EMPTY;
hex::decode_to_slice(&trailing[0..2], &mut result.as_mut_bytes()[0..1])?;
if trailing[2] != b'/' {
return Err(FromHexError::InvalidHexCharacter {
c: trailing[2] as char,
index: 2,
});
}
hex::decode_to_slice(&trailing[3..], &mut result.as_mut_bytes()[1..])?;
Ok(result)
}
fn to_object_pathname(&self) -> String {
format!(
"{:02x}/{}",
self.as_bytes()[0],
hex::encode(&self.as_bytes()[1..])
)
}
fn to_object_dir(&self) -> String {
format!("{:02x}", self.as_bytes()[0])
}
fn to_hex(&self) -> String {
hex::encode(self.as_bytes())
}
fn to_id(&self) -> String {
format!("{}:{}", Self::ALGORITHM.hash_name(), self.to_hex())
}
}
impl fmt::Debug for Sha256HashValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", Self::ALGORITHM.hash_name(), self.to_hex())
}
}
impl fmt::Debug for Sha512HashValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", Self::ALGORITHM.hash_name(), self.to_hex())
}
}
#[derive(Clone, Eq, FromBytes, Hash, Immutable, IntoBytes, KnownLayout, PartialEq, Unaligned)]
#[repr(C)]
pub struct Sha256HashValue([u8; 32]);
impl From<Output<Sha256>> for Sha256HashValue {
fn from(value: Output<Sha256>) -> Self {
Self(value.into())
}
}
impl FsVerityHashValue for Sha256HashValue {
type Digest = Sha256;
const ALGORITHM: Algorithm = Algorithm::SHA256;
const EMPTY: Self = Self([0; 32]);
}
#[derive(Clone, Eq, FromBytes, Hash, Immutable, IntoBytes, KnownLayout, PartialEq, Unaligned)]
#[repr(C)]
pub struct Sha512HashValue([u8; 64]);
impl From<Output<Sha512>> for Sha512HashValue {
fn from(value: Output<Sha512>) -> Self {
Self(value.into())
}
}
impl FsVerityHashValue for Sha512HashValue {
type Digest = Sha512;
const ALGORITHM: Algorithm = Algorithm::SHA512;
const EMPTY: Self = Self([0; 64]);
}
pub const DEFAULT_LG_BLOCKSIZE: u8 = 12;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Algorithm {
Sha256 {
lg_blocksize: u8,
},
Sha512 {
lg_blocksize: u8,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AlgorithmParseError {
MissingPrefix,
MissingSeparator,
UnknownHash(String),
InvalidBlockSize(String),
UnsupportedBlockSize(u8),
}
impl fmt::Display for AlgorithmParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingPrefix => write!(f, "algorithm must start with 'fsverity-'"),
Self::MissingSeparator => {
write!(f, "algorithm must be 'fsverity-<hash>-<lg_blocksize>'")
}
Self::UnknownHash(h) => {
write!(
f,
"unsupported hash algorithm '{h}' (expected sha256 or sha512)"
)
}
Self::InvalidBlockSize(s) => write!(f, "invalid lg_blocksize '{s}'"),
Self::UnsupportedBlockSize(n) => write!(
f,
"unsupported lg_blocksize {n} (only {DEFAULT_LG_BLOCKSIZE} is currently supported)"
),
}
}
}
impl std::error::Error for AlgorithmParseError {}
impl Algorithm {
pub const SHA256: Self = Self::Sha256 {
lg_blocksize: DEFAULT_LG_BLOCKSIZE,
};
pub const SHA512: Self = Self::Sha512 {
lg_blocksize: DEFAULT_LG_BLOCKSIZE,
};
pub fn for_hash<H: FsVerityHashValue>() -> Self {
H::ALGORITHM
}
pub const fn hash_name(&self) -> &'static str {
match self {
Self::Sha256 { .. } => "sha256",
Self::Sha512 { .. } => "sha512",
}
}
pub const fn kernel_id(&self) -> u8 {
match self {
Self::Sha256 { .. } => 1,
Self::Sha512 { .. } => 2,
}
}
pub const fn lg_blocksize(&self) -> u8 {
match self {
Self::Sha256 { lg_blocksize } | Self::Sha512 { lg_blocksize } => *lg_blocksize,
}
}
pub fn is_compatible<H: FsVerityHashValue>(&self) -> bool {
std::mem::discriminant(self) == std::mem::discriminant(&H::ALGORITHM)
}
}
impl FromStr for Algorithm {
type Err = AlgorithmParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let rest = s
.strip_prefix("fsverity-")
.ok_or(AlgorithmParseError::MissingPrefix)?;
let (hash, lg_bs) = rest
.rsplit_once('-')
.ok_or(AlgorithmParseError::MissingSeparator)?;
let lg_blocksize: u8 = lg_bs
.parse()
.map_err(|_| AlgorithmParseError::InvalidBlockSize(lg_bs.to_owned()))?;
if lg_blocksize != DEFAULT_LG_BLOCKSIZE {
return Err(AlgorithmParseError::UnsupportedBlockSize(lg_blocksize));
}
match hash {
"sha256" => Ok(Self::Sha256 { lg_blocksize }),
"sha512" => Ok(Self::Sha512 { lg_blocksize }),
other => Err(AlgorithmParseError::UnknownHash(other.to_owned())),
}
}
}
impl fmt::Display for Algorithm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "fsverity-{}-{}", self.hash_name(), self.lg_blocksize())
}
}
impl serde::Serialize for Algorithm {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> serde::Deserialize<'de> for Algorithm {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod test {
use super::*;
fn test_fsverity_hash<H: FsVerityHashValue>() {
let len = size_of::<H>();
let hexlen = len * 2;
let hex = H::EMPTY.to_hex();
assert_eq!(hex.as_bytes(), [b'0'].repeat(hexlen));
assert_eq!(
H::EMPTY.to_id(),
format!("{}:{}", H::ALGORITHM.hash_name(), hex)
);
assert_eq!(
format!("{:?}", H::EMPTY),
format!("{}:{}", H::ALGORITHM.hash_name(), hex)
);
assert_eq!(H::from_hex(&hex), Ok(H::EMPTY));
assert_eq!(H::from_hex("lol"), Err(FromHexError::OddLength));
assert_eq!(H::from_hex("lolo"), Err(FromHexError::InvalidStringLength));
assert_eq!(
H::from_hex([b'l'].repeat(hexlen)),
Err(FromHexError::InvalidHexCharacter { c: 'l', index: 0 })
);
assert_eq!(H::from_object_dir_and_basename(0, &hex[2..]), Ok(H::EMPTY));
assert_eq!(H::from_object_dir_and_basename(0, &hex[2..]), Ok(H::EMPTY));
assert_eq!(
H::from_object_dir_and_basename(0, "lol"),
Err(FromHexError::InvalidStringLength)
);
assert_eq!(
H::from_object_dir_and_basename(0, [b'l'].repeat(hexlen - 2)),
Err(FromHexError::InvalidHexCharacter { c: 'l', index: 0 })
);
assert_eq!(
H::from_object_pathname(format!("{}/{}", &hex[0..2], &hex[2..])),
Ok(H::EMPTY)
);
assert_eq!(
H::from_object_pathname(format!("../this/is/ignored/{}/{}", &hex[0..2], &hex[2..])),
Ok(H::EMPTY)
);
assert_eq!(
H::from_object_pathname(&hex),
Err(FromHexError::InvalidStringLength)
);
assert_eq!(
H::from_object_pathname("lol"),
Err(FromHexError::InvalidStringLength)
);
assert_eq!(
H::from_object_pathname([b'l'].repeat(hexlen + 1)),
Err(FromHexError::InvalidHexCharacter { c: 'l', index: 0 })
);
assert_eq!(
H::from_object_pathname(format!("{}0{}", &hex[0..2], &hex[2..])),
Err(FromHexError::InvalidHexCharacter { c: '0', index: 2 })
);
}
#[test]
fn test_sha256hashvalue() {
test_fsverity_hash::<Sha256HashValue>();
}
#[test]
fn test_sha512hashvalue() {
test_fsverity_hash::<Sha512HashValue>();
}
#[test]
fn test_algorithm_for_hash() {
let a256 = Algorithm::for_hash::<Sha256HashValue>();
assert_eq!(a256.hash_name(), "sha256");
assert_eq!(a256.kernel_id(), 1);
assert_eq!(a256.lg_blocksize(), 12);
assert_eq!(a256.to_string(), "fsverity-sha256-12");
assert!(a256.is_compatible::<Sha256HashValue>());
assert!(!a256.is_compatible::<Sha512HashValue>());
let a512 = Algorithm::for_hash::<Sha512HashValue>();
assert_eq!(a512.hash_name(), "sha512");
assert_eq!(a512.kernel_id(), 2);
assert_eq!(a512.to_string(), "fsverity-sha512-12");
assert!(a512.is_compatible::<Sha512HashValue>());
assert!(!a512.is_compatible::<Sha256HashValue>());
}
#[test]
fn test_algorithm_parse_roundtrip() {
for s in ["fsverity-sha256-12", "fsverity-sha512-12"] {
let alg: Algorithm = s.parse().unwrap();
assert_eq!(alg.to_string(), s);
}
}
#[test]
fn test_algorithm_parse_errors() {
let cases = [
("sha256-12", AlgorithmParseError::MissingPrefix),
("garbage", AlgorithmParseError::MissingPrefix),
("fsverity-sha256", AlgorithmParseError::MissingSeparator),
(
"fsverity-sha1-12",
AlgorithmParseError::UnknownHash("sha1".to_owned()),
),
(
"fsverity-sha256-abc",
AlgorithmParseError::InvalidBlockSize("abc".to_owned()),
),
(
"fsverity-sha256-16",
AlgorithmParseError::UnsupportedBlockSize(16),
),
];
for (input, expected) in cases {
let err = input.parse::<Algorithm>().unwrap_err();
assert_eq!(err, expected, "input: {input}");
}
}
#[test]
fn test_algorithm_equality() {
let a: Algorithm = "fsverity-sha512-12".parse().unwrap();
let b = Algorithm::for_hash::<Sha512HashValue>();
assert_eq!(a, b);
}
}