use anyhow::{Context, Result};
use composefs::fsverity::{Algorithm, FsVerityHashValue};
pub const KARG_V2: &str = "composefs";
pub const KARG_COMPOSEFS_DIGEST: &str = "composefs.digest";
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ComposefsCmdline<ObjectID: FsVerityHashValue> {
V2 {
digest: ObjectID,
insecure: bool,
},
V1 {
digest: ObjectID,
insecure: bool,
},
}
impl<ObjectID: FsVerityHashValue> ComposefsCmdline<ObjectID> {
pub fn digest(&self) -> &ObjectID {
match self {
ComposefsCmdline::V2 { digest, .. } | ComposefsCmdline::V1 { digest, .. } => digest,
}
}
pub fn validate_digest<'a>(
&self,
acceptable: impl IntoIterator<Item = &'a ObjectID>,
) -> Result<&ObjectID>
where
ObjectID: 'a,
{
let acceptable: Vec<&ObjectID> = acceptable.into_iter().collect();
let uki_digest = self.digest();
if acceptable.contains(&uki_digest) {
return Ok(uki_digest);
}
let expected = acceptable
.iter()
.map(|id| format!("{id:?}"))
.collect::<Vec<_>>()
.join(", ");
anyhow::bail!(
"The UKI has the wrong composefs digest (is '{uki_digest:?}', should be one of [{expected}])"
)
}
pub fn is_insecure(&self) -> bool {
match self {
ComposefsCmdline::V1 { insecure, .. } | ComposefsCmdline::V2 { insecure, .. } => {
*insecure
}
}
}
pub fn new_v2(digest: ObjectID, insecure: bool) -> Self {
ComposefsCmdline::V2 { digest, insecure }
}
pub fn new_v1(digest: ObjectID, insecure: bool) -> Self {
ComposefsCmdline::V1 { digest, insecure }
}
pub fn from_cmdline(cmdline: &str) -> Result<Option<Self>> {
let expected_hex_len = size_of::<ObjectID>() * 2;
let v1_key_prefix = format!("{KARG_COMPOSEFS_DIGEST}=");
for token in split_cmdline(cmdline) {
let Some(val) = token.strip_prefix(&v1_key_prefix) else {
continue;
};
let (val_no_q, insecure) = if let Some(s) = val.strip_prefix('?') {
(s, true)
} else {
(val, false)
};
let (desc, hex) = parse_digest_value(val_no_q)
.with_context(|| format!("parsing {KARG_COMPOSEFS_DIGEST}= value: {val}"))?;
if !desc.algorithm.is_compatible::<ObjectID>() {
continue;
}
let digest = ObjectID::from_hex(hex).with_context(|| {
format!(
"parsing {KARG_COMPOSEFS_DIGEST}= hash: got {} hex chars, expected {} for {}",
hex.len(),
expected_hex_len,
ObjectID::ALGORITHM,
)
})?;
return Ok(Some(match desc.version {
1 => ComposefsCmdline::V1 { digest, insecure },
_ => ComposefsCmdline::V2 { digest, insecure },
}));
}
if let Some(val) = get_cmdline_value(cmdline, &format!("{KARG_V2}=")) {
let (hex, insecure) = if let Some(stripped) = val.strip_prefix('?') {
(stripped, true)
} else {
(val, false)
};
let digest = ObjectID::from_hex(hex).with_context(|| {
format!(
"parsing {KARG_V2}= hash: got {} hex chars, expected {} for {}",
hex.len(),
expected_hex_len,
ObjectID::ALGORITHM,
)
})?;
return Ok(Some(ComposefsCmdline::V2 { digest, insecure }));
}
Ok(None)
}
pub fn to_cmdline_arg(&self) -> String {
let verity_suffix = ObjectID::ALGORITHM.verity_suffix();
match self {
ComposefsCmdline::V1 {
digest,
insecure: false,
} => format!(
"{KARG_COMPOSEFS_DIGEST}=v1-{verity_suffix}:{}",
digest.to_hex()
),
ComposefsCmdline::V1 {
digest,
insecure: true,
} => format!(
"{KARG_COMPOSEFS_DIGEST}=?v1-{verity_suffix}:{}",
digest.to_hex()
),
ComposefsCmdline::V2 {
digest,
insecure: false,
} => {
format!("{KARG_V2}={}", digest.to_hex())
}
ComposefsCmdline::V2 {
digest,
insecure: true,
} => {
format!("{KARG_V2}=?{}", digest.to_hex())
}
}
}
}
pub fn split_cmdline(cmdline: &str) -> impl Iterator<Item = &str> {
let mut in_quotes = false;
cmdline.split(move |c: char| {
if c == '"' {
in_quotes = !in_quotes;
}
!in_quotes && c.is_ascii_whitespace()
})
}
pub fn get_cmdline_value<'a>(cmdline: &'a str, prefix: &str) -> Option<&'a str> {
split_cmdline(cmdline).find_map(|item| item.strip_prefix(prefix))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DigestDescriptor {
pub version: u32,
pub algorithm: Algorithm,
}
pub fn parse_digest_value(s: &str) -> Result<(DigestDescriptor, &str)> {
let (descriptor, hex) = s
.split_once(':')
.with_context(|| format!("expected '<version>-<hash>-<blocksize>:<hex>', got: {s}"))?;
let (version_str, hash_and_bs) = descriptor
.split_once('-')
.with_context(|| format!("expected 'v<N>-<hash>-<blocksize>', got: {descriptor}"))?;
let version = match version_str {
"v1" => 1,
"v2" => 2,
_ => anyhow::bail!("unsupported format version '{version_str}'"),
};
let algorithm: Algorithm = format!("fsverity-{hash_and_bs}")
.parse()
.with_context(|| format!("parsing algorithm from '{hash_and_bs}'"))?;
Ok((DigestDescriptor { version, algorithm }, hex))
}
pub fn make_cmdline_composefs(
id: &str,
insecure: bool,
version: composefs::erofs::format::FormatVersion,
algorithm: composefs::fsverity::Algorithm,
) -> String {
use composefs::erofs::format::FormatVersion;
match version {
FormatVersion::V0 | FormatVersion::V1 => {
let fmt_desc = format!("v1-{}", algorithm.verity_suffix());
if insecure {
format!("{KARG_COMPOSEFS_DIGEST}=?{fmt_desc}:{id}")
} else {
format!("{KARG_COMPOSEFS_DIGEST}={fmt_desc}:{id}")
}
}
FormatVersion::V2 => {
if insecure {
format!("{KARG_V2}=?{id}")
} else {
format!("{KARG_V2}={id}")
}
}
}
}
#[cfg(test)]
mod tests {
use composefs::fsverity::{Algorithm, Sha256HashValue, Sha512HashValue};
use super::*;
const SHA256_HEX: &str = "8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52";
const SHA512_HEX: &str = "6f06b5e82420abec546d6e6d3ddd612c50cfa9b707c129345b7ec16f456b92fe\
35df68999b042e1a6a70dfe75f2fed8cf9f67afd0bf08d2374678d75e2f65a02";
#[test]
fn test_composefs_cmdline_v2_round_trip() {
let digest = Sha256HashValue::from_hex(SHA256_HEX).unwrap();
let karg = ComposefsCmdline::new_v2(digest.clone(), false);
assert_eq!(karg.to_cmdline_arg(), format!("composefs={SHA256_HEX}"));
let parsed = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&karg.to_cmdline_arg())
.unwrap()
.unwrap();
assert_eq!(
parsed,
ComposefsCmdline::V2 {
digest,
insecure: false
}
);
}
#[test]
fn test_composefs_cmdline_v2_insecure_round_trip() {
let digest = Sha256HashValue::from_hex(SHA256_HEX).unwrap();
let karg = ComposefsCmdline::new_v2(digest.clone(), true);
assert_eq!(karg.to_cmdline_arg(), format!("composefs=?{SHA256_HEX}"));
let parsed = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&karg.to_cmdline_arg())
.unwrap()
.unwrap();
assert_eq!(
parsed,
ComposefsCmdline::V2 {
digest,
insecure: true
}
);
}
#[test]
fn test_composefs_cmdline_v1_round_trip_sha256() {
let digest = Sha256HashValue::from_hex(SHA256_HEX).unwrap();
let karg = ComposefsCmdline::new_v1(digest.clone(), false);
assert_eq!(
karg.to_cmdline_arg(),
format!("composefs.digest=v1-sha256-12:{SHA256_HEX}")
);
let parsed = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&karg.to_cmdline_arg())
.unwrap()
.unwrap();
assert_eq!(
parsed,
ComposefsCmdline::V1 {
digest,
insecure: false
}
);
}
#[test]
fn test_composefs_cmdline_v1_round_trip_sha512() {
let digest = Sha512HashValue::from_hex(SHA512_HEX).unwrap();
let karg = ComposefsCmdline::new_v1(digest.clone(), false);
assert_eq!(
karg.to_cmdline_arg(),
format!("composefs.digest=v1-sha512-12:{SHA512_HEX}")
);
let parsed = ComposefsCmdline::<Sha512HashValue>::from_cmdline(&karg.to_cmdline_arg())
.unwrap()
.unwrap();
assert_eq!(
parsed,
ComposefsCmdline::V1 {
digest,
insecure: false
}
);
}
#[test]
fn test_composefs_cmdline_v1_insecure_round_trip() {
let digest = Sha256HashValue::from_hex(SHA256_HEX).unwrap();
let karg = ComposefsCmdline::new_v1(digest.clone(), true);
assert_eq!(
karg.to_cmdline_arg(),
format!("composefs.digest=?v1-sha256-12:{SHA256_HEX}")
);
let parsed = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&karg.to_cmdline_arg())
.unwrap()
.unwrap();
assert_eq!(
parsed,
ComposefsCmdline::V1 {
digest,
insecure: true
}
);
assert!(parsed.is_insecure());
}
#[test]
fn test_composefs_cmdline_v1_takes_priority_over_v2() {
let hex_v1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
let hex_v2 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
let cmdline = format!("composefs={hex_v2} composefs.digest=v1-sha256-12:{hex_v1}");
let parsed = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&cmdline)
.unwrap()
.unwrap();
assert!(
matches!(&parsed, ComposefsCmdline::V1 { digest, .. } if digest.to_hex() == hex_v1),
"expected V1 variant with hex_v1, got {parsed:?}"
);
}
#[test]
fn test_composefs_cmdline_v1_cross_type_rejection() {
let cmdline = format!("composefs.digest=v1-sha512-12:{SHA512_HEX}");
let result = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&cmdline).unwrap();
assert!(
result.is_none(),
"sha256 parser should not match sha512 karg, got {result:?}"
);
let cmdline256 = format!("composefs.digest=v1-sha256-12:{SHA256_HEX}");
let result512 = ComposefsCmdline::<Sha512HashValue>::from_cmdline(&cmdline256).unwrap();
assert!(
result512.is_none(),
"sha512 parser should not match sha256 karg, got {result512:?}"
);
}
#[test]
fn test_composefs_cmdline_absent_returns_none() {
assert!(
ComposefsCmdline::<Sha256HashValue>::from_cmdline("quiet splash rw")
.unwrap()
.is_none()
);
assert!(
ComposefsCmdline::<Sha256HashValue>::from_cmdline("")
.unwrap()
.is_none()
);
}
#[test]
fn test_composefs_cmdline_invalid_hex_errors() {
let err = ComposefsCmdline::<Sha256HashValue>::from_cmdline(
"composefs.digest=v1-sha256-12:notahex",
)
.unwrap_err();
assert!(err.to_string().contains("composefs.digest="));
let err =
ComposefsCmdline::<Sha256HashValue>::from_cmdline("composefs=notahex").unwrap_err();
assert!(err.to_string().contains("composefs="));
}
#[test]
fn test_composefs_cmdline_unsupported_blocksize_errors() {
let err = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&format!(
"composefs.digest=v1-sha256-8:{SHA256_HEX}"
))
.unwrap_err();
let chain = format!("{err:#}");
assert!(
chain.contains("unsupported"),
"expected 'unsupported' in error chain, got: {chain}"
);
let err = ComposefsCmdline::<Sha512HashValue>::from_cmdline(&format!(
"composefs.digest=v1-sha512-99:{SHA512_HEX}"
))
.unwrap_err();
let chain = format!("{err:#}");
assert!(
chain.contains("unsupported"),
"expected 'unsupported' in error chain, got: {chain}"
);
let err = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&format!(
"composefs.digest=v3-sha256-12:{SHA256_HEX}"
))
.unwrap_err();
let chain = format!("{err:#}");
assert!(
chain.contains("unsupported format version"),
"expected version error, got: {chain}"
);
}
#[test]
fn test_composefs_digest_v2_parsed_as_v2() {
let cmdline = format!("composefs.digest=v2-sha256-12:{SHA256_HEX}");
let parsed = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&cmdline)
.unwrap()
.unwrap();
assert!(
matches!(parsed, ComposefsCmdline::V2 { .. }),
"expected V2 variant, got {parsed:?}"
);
assert_eq!(parsed.digest().to_hex(), SHA256_HEX);
assert!(!parsed.is_insecure());
let cmdline = format!("composefs.digest=?v2-sha512-12:{SHA512_HEX}");
let parsed = ComposefsCmdline::<Sha512HashValue>::from_cmdline(&cmdline)
.unwrap()
.unwrap();
assert!(matches!(
parsed,
ComposefsCmdline::V2 { insecure: true, .. }
));
}
#[test]
fn test_digest_accessor() {
let digest = Sha256HashValue::from_hex(SHA256_HEX).unwrap();
let v1 = ComposefsCmdline::new_v1(digest.clone(), false);
let v2 = ComposefsCmdline::new_v2(digest.clone(), false);
assert_eq!(v1.digest(), &digest);
assert_eq!(v2.digest(), &digest);
}
#[test]
fn test_from_cmdline_v1() {
let cmdline = format!("root=UUID=abc composefs.digest=v1-sha256-12:{SHA256_HEX} rw");
let result = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&cmdline)
.unwrap()
.unwrap();
assert!(matches!(result, ComposefsCmdline::V1 { .. }));
assert_eq!(result.digest().to_hex(), SHA256_HEX);
assert!(!result.is_insecure());
}
#[test]
fn test_from_cmdline_v2_fallback() {
let cmdline = format!("root=UUID=abc composefs={SHA256_HEX} rw");
let result = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&cmdline)
.unwrap()
.unwrap();
assert!(matches!(result, ComposefsCmdline::V2 { .. }));
assert_eq!(result.digest().to_hex(), SHA256_HEX);
assert!(!result.is_insecure());
}
#[test]
fn test_from_cmdline_missing_returns_none() {
let result = ComposefsCmdline::<Sha256HashValue>::from_cmdline("root=UUID=abc rw").unwrap();
assert!(result.is_none());
}
#[test]
fn test_from_cmdline_insecure_prefix() {
let cmdline = format!("composefs=?{SHA256_HEX}");
let result = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&cmdline)
.unwrap()
.unwrap();
assert!(result.is_insecure());
assert_eq!(result.digest().to_hex(), SHA256_HEX);
}
#[test]
fn test_make_cmdline_composefs_v1() {
use composefs::erofs::format::FormatVersion;
let result =
make_cmdline_composefs(SHA256_HEX, false, FormatVersion::V1, Algorithm::SHA256);
assert_eq!(
result,
format!("composefs.digest=v1-sha256-12:{SHA256_HEX}")
);
}
#[test]
fn test_make_cmdline_composefs_v1_sha512() {
use composefs::erofs::format::FormatVersion;
let result =
make_cmdline_composefs(SHA512_HEX, false, FormatVersion::V1, Algorithm::SHA512);
assert_eq!(
result,
format!("composefs.digest=v1-sha512-12:{SHA512_HEX}")
);
}
#[test]
fn test_validate_digest() {
let v1_digest = Sha256HashValue::from_hex(SHA256_HEX).unwrap();
let other = Sha256HashValue::from_hex(
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
)
.unwrap();
let karg = ComposefsCmdline::new_v2(v1_digest.clone(), false);
let acceptable = [&other, &v1_digest];
let matched = karg.validate_digest(acceptable.iter().copied()).unwrap();
assert_eq!(matched, &v1_digest);
let err = karg.validate_digest(std::iter::once(&other)).unwrap_err();
assert!(
err.to_string().contains("should be one of"),
"unexpected error message: {err}"
);
}
#[test]
fn test_make_cmdline_composefs_v2_insecure() {
use composefs::erofs::format::FormatVersion;
let result = make_cmdline_composefs(SHA256_HEX, true, FormatVersion::V2, Algorithm::SHA256);
assert_eq!(result, format!("composefs=?{SHA256_HEX}"));
}
}