1use anyhow::{Context, Result};
9use composefs::fsverity::{Algorithm, FsVerityHashValue};
10
11pub const KARG_V2: &str = "composefs";
17
18pub const KARG_COMPOSEFS_DIGEST: &str = "composefs.digest";
29
30#[derive(Debug, Clone, PartialEq, Eq)]
47pub enum ComposefsCmdline<ObjectID: FsVerityHashValue> {
48 V2 {
53 digest: ObjectID,
55 insecure: bool,
58 },
59 V1 {
67 digest: ObjectID,
69 insecure: bool,
72 },
73}
74
75impl<ObjectID: FsVerityHashValue> ComposefsCmdline<ObjectID> {
76 pub fn digest(&self) -> &ObjectID {
80 match self {
81 ComposefsCmdline::V2 { digest, .. } | ComposefsCmdline::V1 { digest, .. } => digest,
82 }
83 }
84
85 pub fn validate_digest<'a>(
93 &self,
94 acceptable: impl IntoIterator<Item = &'a ObjectID>,
95 ) -> Result<&ObjectID>
96 where
97 ObjectID: 'a,
98 {
99 let acceptable: Vec<&ObjectID> = acceptable.into_iter().collect();
100 let uki_digest = self.digest();
101 if acceptable.contains(&uki_digest) {
102 return Ok(uki_digest);
103 }
104 let expected = acceptable
105 .iter()
106 .map(|id| format!("{id:?}"))
107 .collect::<Vec<_>>()
108 .join(", ");
109 anyhow::bail!(
110 "The UKI has the wrong composefs digest (is '{uki_digest:?}', should be one of [{expected}])"
111 )
112 }
113
114 pub fn is_insecure(&self) -> bool {
116 match self {
117 ComposefsCmdline::V1 { insecure, .. } | ComposefsCmdline::V2 { insecure, .. } => {
118 *insecure
119 }
120 }
121 }
122
123 pub fn new_v2(digest: ObjectID, insecure: bool) -> Self {
125 ComposefsCmdline::V2 { digest, insecure }
126 }
127
128 pub fn new_v1(digest: ObjectID, insecure: bool) -> Self {
130 ComposefsCmdline::V1 { digest, insecure }
131 }
132
133 pub fn from_cmdline(cmdline: &str) -> Result<Option<Self>> {
146 let expected_hex_len = size_of::<ObjectID>() * 2;
147
148 let v1_key_prefix = format!("{KARG_COMPOSEFS_DIGEST}=");
153 for token in split_cmdline(cmdline) {
154 let Some(val) = token.strip_prefix(&v1_key_prefix) else {
155 continue;
156 };
157 let (val_no_q, insecure) = if let Some(s) = val.strip_prefix('?') {
158 (s, true)
159 } else {
160 (val, false)
161 };
162 let (desc, hex) = parse_digest_value(val_no_q)
163 .with_context(|| format!("parsing {KARG_COMPOSEFS_DIGEST}= value: {val}"))?;
164 if !desc.algorithm.is_compatible::<ObjectID>() {
165 continue;
167 }
168 let digest = ObjectID::from_hex(hex).with_context(|| {
169 format!(
170 "parsing {KARG_COMPOSEFS_DIGEST}= hash: got {} hex chars, expected {} for {}",
171 hex.len(),
172 expected_hex_len,
173 ObjectID::ALGORITHM,
174 )
175 })?;
176 return Ok(Some(match desc.version {
177 1 => ComposefsCmdline::V1 { digest, insecure },
178 _ => ComposefsCmdline::V2 { digest, insecure },
179 }));
180 }
181
182 if let Some(val) = get_cmdline_value(cmdline, &format!("{KARG_V2}=")) {
184 let (hex, insecure) = if let Some(stripped) = val.strip_prefix('?') {
185 (stripped, true)
186 } else {
187 (val, false)
188 };
189 let digest = ObjectID::from_hex(hex).with_context(|| {
190 format!(
191 "parsing {KARG_V2}= hash: got {} hex chars, expected {} for {}",
192 hex.len(),
193 expected_hex_len,
194 ObjectID::ALGORITHM,
195 )
196 })?;
197 return Ok(Some(ComposefsCmdline::V2 { digest, insecure }));
198 }
199
200 Ok(None)
201 }
202
203 pub fn to_cmdline_arg(&self) -> String {
210 let verity_suffix = ObjectID::ALGORITHM.verity_suffix();
211 match self {
212 ComposefsCmdline::V1 {
213 digest,
214 insecure: false,
215 } => format!(
216 "{KARG_COMPOSEFS_DIGEST}=v1-{verity_suffix}:{}",
217 digest.to_hex()
218 ),
219 ComposefsCmdline::V1 {
220 digest,
221 insecure: true,
222 } => format!(
223 "{KARG_COMPOSEFS_DIGEST}=?v1-{verity_suffix}:{}",
224 digest.to_hex()
225 ),
226 ComposefsCmdline::V2 {
227 digest,
228 insecure: false,
229 } => {
230 format!("{KARG_V2}={}", digest.to_hex())
231 }
232 ComposefsCmdline::V2 {
233 digest,
234 insecure: true,
235 } => {
236 format!("{KARG_V2}=?{}", digest.to_hex())
237 }
238 }
239 }
240}
241
242pub fn split_cmdline(cmdline: &str) -> impl Iterator<Item = &str> {
248 let mut in_quotes = false;
249
250 cmdline.split(move |c: char| {
251 if c == '"' {
252 in_quotes = !in_quotes;
253 }
254 !in_quotes && c.is_ascii_whitespace()
255 })
256}
257
258pub fn get_cmdline_value<'a>(cmdline: &'a str, prefix: &str) -> Option<&'a str> {
266 split_cmdline(cmdline).find_map(|item| item.strip_prefix(prefix))
267}
268
269#[derive(Debug, Clone, Copy, PartialEq, Eq)]
271pub struct DigestDescriptor {
272 pub version: u32,
274 pub algorithm: Algorithm,
276}
277
278pub fn parse_digest_value(s: &str) -> Result<(DigestDescriptor, &str)> {
283 let (descriptor, hex) = s
285 .split_once(':')
286 .with_context(|| format!("expected '<version>-<hash>-<blocksize>:<hex>', got: {s}"))?;
287
288 let (version_str, hash_and_bs) = descriptor
290 .split_once('-')
291 .with_context(|| format!("expected 'v<N>-<hash>-<blocksize>', got: {descriptor}"))?;
292
293 let version = match version_str {
294 "v1" => 1,
295 "v2" => 2,
296 _ => anyhow::bail!("unsupported format version '{version_str}'"),
297 };
298
299 let algorithm: Algorithm = format!("fsverity-{hash_and_bs}")
301 .parse()
302 .with_context(|| format!("parsing algorithm from '{hash_and_bs}'"))?;
303
304 Ok((DigestDescriptor { version, algorithm }, hex))
305}
306
307pub fn make_cmdline_composefs(
321 id: &str,
322 insecure: bool,
323 version: composefs::erofs::format::FormatVersion,
324 algorithm: composefs::fsverity::Algorithm,
325) -> String {
326 use composefs::erofs::format::FormatVersion;
327 match version {
328 FormatVersion::V0 | FormatVersion::V1 => {
330 let fmt_desc = format!("v1-{}", algorithm.verity_suffix());
331 if insecure {
332 format!("{KARG_COMPOSEFS_DIGEST}=?{fmt_desc}:{id}")
333 } else {
334 format!("{KARG_COMPOSEFS_DIGEST}={fmt_desc}:{id}")
335 }
336 }
337 FormatVersion::V2 => {
338 if insecure {
339 format!("{KARG_V2}=?{id}")
340 } else {
341 format!("{KARG_V2}={id}")
342 }
343 }
344 }
345}
346
347#[cfg(test)]
348mod tests {
349 use composefs::fsverity::{Algorithm, Sha256HashValue, Sha512HashValue};
350
351 use super::*;
352
353 const SHA256_HEX: &str = "8b7df143d91c716ecfa5fc1730022f6b421b05cedee8fd52b1fc65a96030ad52";
354 const SHA512_HEX: &str = "6f06b5e82420abec546d6e6d3ddd612c50cfa9b707c129345b7ec16f456b92fe\
355 35df68999b042e1a6a70dfe75f2fed8cf9f67afd0bf08d2374678d75e2f65a02";
356
357 #[test]
358 fn test_composefs_cmdline_v2_round_trip() {
359 let digest = Sha256HashValue::from_hex(SHA256_HEX).unwrap();
360 let karg = ComposefsCmdline::new_v2(digest.clone(), false);
361 assert_eq!(karg.to_cmdline_arg(), format!("composefs={SHA256_HEX}"));
362
363 let parsed = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&karg.to_cmdline_arg())
364 .unwrap()
365 .unwrap();
366 assert_eq!(
367 parsed,
368 ComposefsCmdline::V2 {
369 digest,
370 insecure: false
371 }
372 );
373 }
374
375 #[test]
376 fn test_composefs_cmdline_v2_insecure_round_trip() {
377 let digest = Sha256HashValue::from_hex(SHA256_HEX).unwrap();
378 let karg = ComposefsCmdline::new_v2(digest.clone(), true);
379 assert_eq!(karg.to_cmdline_arg(), format!("composefs=?{SHA256_HEX}"));
380
381 let parsed = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&karg.to_cmdline_arg())
382 .unwrap()
383 .unwrap();
384 assert_eq!(
385 parsed,
386 ComposefsCmdline::V2 {
387 digest,
388 insecure: true
389 }
390 );
391 }
392
393 #[test]
394 fn test_composefs_cmdline_v1_round_trip_sha256() {
395 let digest = Sha256HashValue::from_hex(SHA256_HEX).unwrap();
396 let karg = ComposefsCmdline::new_v1(digest.clone(), false);
397 assert_eq!(
398 karg.to_cmdline_arg(),
399 format!("composefs.digest=v1-sha256-12:{SHA256_HEX}")
400 );
401
402 let parsed = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&karg.to_cmdline_arg())
403 .unwrap()
404 .unwrap();
405 assert_eq!(
406 parsed,
407 ComposefsCmdline::V1 {
408 digest,
409 insecure: false
410 }
411 );
412 }
413
414 #[test]
415 fn test_composefs_cmdline_v1_round_trip_sha512() {
416 let digest = Sha512HashValue::from_hex(SHA512_HEX).unwrap();
417 let karg = ComposefsCmdline::new_v1(digest.clone(), false);
418 assert_eq!(
419 karg.to_cmdline_arg(),
420 format!("composefs.digest=v1-sha512-12:{SHA512_HEX}")
421 );
422
423 let parsed = ComposefsCmdline::<Sha512HashValue>::from_cmdline(&karg.to_cmdline_arg())
424 .unwrap()
425 .unwrap();
426 assert_eq!(
427 parsed,
428 ComposefsCmdline::V1 {
429 digest,
430 insecure: false
431 }
432 );
433 }
434
435 #[test]
436 fn test_composefs_cmdline_v1_insecure_round_trip() {
437 let digest = Sha256HashValue::from_hex(SHA256_HEX).unwrap();
438 let karg = ComposefsCmdline::new_v1(digest.clone(), true);
439 assert_eq!(
440 karg.to_cmdline_arg(),
441 format!("composefs.digest=?v1-sha256-12:{SHA256_HEX}")
442 );
443
444 let parsed = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&karg.to_cmdline_arg())
445 .unwrap()
446 .unwrap();
447 assert_eq!(
448 parsed,
449 ComposefsCmdline::V1 {
450 digest,
451 insecure: true
452 }
453 );
454 assert!(parsed.is_insecure());
455 }
456
457 #[test]
458 fn test_composefs_cmdline_v1_takes_priority_over_v2() {
459 let hex_v1 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
461 let hex_v2 = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
462 let cmdline = format!("composefs={hex_v2} composefs.digest=v1-sha256-12:{hex_v1}");
463
464 let parsed = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&cmdline)
465 .unwrap()
466 .unwrap();
467 assert!(
468 matches!(&parsed, ComposefsCmdline::V1 { digest, .. } if digest.to_hex() == hex_v1),
469 "expected V1 variant with hex_v1, got {parsed:?}"
470 );
471 }
472
473 #[test]
474 fn test_composefs_cmdline_v1_cross_type_rejection() {
475 let cmdline = format!("composefs.digest=v1-sha512-12:{SHA512_HEX}");
477 let result = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&cmdline).unwrap();
478 assert!(
479 result.is_none(),
480 "sha256 parser should not match sha512 karg, got {result:?}"
481 );
482
483 let cmdline256 = format!("composefs.digest=v1-sha256-12:{SHA256_HEX}");
485 let result512 = ComposefsCmdline::<Sha512HashValue>::from_cmdline(&cmdline256).unwrap();
486 assert!(
487 result512.is_none(),
488 "sha512 parser should not match sha256 karg, got {result512:?}"
489 );
490 }
491
492 #[test]
493 fn test_composefs_cmdline_absent_returns_none() {
494 assert!(
495 ComposefsCmdline::<Sha256HashValue>::from_cmdline("quiet splash rw")
496 .unwrap()
497 .is_none()
498 );
499 assert!(
500 ComposefsCmdline::<Sha256HashValue>::from_cmdline("")
501 .unwrap()
502 .is_none()
503 );
504 }
505
506 #[test]
507 fn test_composefs_cmdline_invalid_hex_errors() {
508 let err = ComposefsCmdline::<Sha256HashValue>::from_cmdline(
510 "composefs.digest=v1-sha256-12:notahex",
511 )
512 .unwrap_err();
513 assert!(err.to_string().contains("composefs.digest="));
514
515 let err =
516 ComposefsCmdline::<Sha256HashValue>::from_cmdline("composefs=notahex").unwrap_err();
517 assert!(err.to_string().contains("composefs="));
518 }
519
520 #[test]
521 fn test_composefs_cmdline_unsupported_blocksize_errors() {
522 let err = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&format!(
524 "composefs.digest=v1-sha256-8:{SHA256_HEX}"
525 ))
526 .unwrap_err();
527 let chain = format!("{err:#}");
529 assert!(
530 chain.contains("unsupported"),
531 "expected 'unsupported' in error chain, got: {chain}"
532 );
533
534 let err = ComposefsCmdline::<Sha512HashValue>::from_cmdline(&format!(
536 "composefs.digest=v1-sha512-99:{SHA512_HEX}"
537 ))
538 .unwrap_err();
539 let chain = format!("{err:#}");
540 assert!(
541 chain.contains("unsupported"),
542 "expected 'unsupported' in error chain, got: {chain}"
543 );
544
545 let err = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&format!(
547 "composefs.digest=v3-sha256-12:{SHA256_HEX}"
548 ))
549 .unwrap_err();
550 let chain = format!("{err:#}");
551 assert!(
552 chain.contains("unsupported format version"),
553 "expected version error, got: {chain}"
554 );
555 }
556
557 #[test]
558 fn test_composefs_digest_v2_parsed_as_v2() {
559 let cmdline = format!("composefs.digest=v2-sha256-12:{SHA256_HEX}");
561 let parsed = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&cmdline)
562 .unwrap()
563 .unwrap();
564 assert!(
565 matches!(parsed, ComposefsCmdline::V2 { .. }),
566 "expected V2 variant, got {parsed:?}"
567 );
568 assert_eq!(parsed.digest().to_hex(), SHA256_HEX);
569 assert!(!parsed.is_insecure());
570
571 let cmdline = format!("composefs.digest=?v2-sha512-12:{SHA512_HEX}");
573 let parsed = ComposefsCmdline::<Sha512HashValue>::from_cmdline(&cmdline)
574 .unwrap()
575 .unwrap();
576 assert!(matches!(
577 parsed,
578 ComposefsCmdline::V2 { insecure: true, .. }
579 ));
580 }
581
582 #[test]
583 fn test_digest_accessor() {
584 let digest = Sha256HashValue::from_hex(SHA256_HEX).unwrap();
585 let v1 = ComposefsCmdline::new_v1(digest.clone(), false);
586 let v2 = ComposefsCmdline::new_v2(digest.clone(), false);
587 assert_eq!(v1.digest(), &digest);
588 assert_eq!(v2.digest(), &digest);
589 }
590
591 #[test]
592 fn test_from_cmdline_v1() {
593 let cmdline = format!("root=UUID=abc composefs.digest=v1-sha256-12:{SHA256_HEX} rw");
594 let result = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&cmdline)
595 .unwrap()
596 .unwrap();
597 assert!(matches!(result, ComposefsCmdline::V1 { .. }));
598 assert_eq!(result.digest().to_hex(), SHA256_HEX);
599 assert!(!result.is_insecure());
600 }
601
602 #[test]
603 fn test_from_cmdline_v2_fallback() {
604 let cmdline = format!("root=UUID=abc composefs={SHA256_HEX} rw");
605 let result = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&cmdline)
606 .unwrap()
607 .unwrap();
608 assert!(matches!(result, ComposefsCmdline::V2 { .. }));
609 assert_eq!(result.digest().to_hex(), SHA256_HEX);
610 assert!(!result.is_insecure());
611 }
612
613 #[test]
614 fn test_from_cmdline_missing_returns_none() {
615 let result = ComposefsCmdline::<Sha256HashValue>::from_cmdline("root=UUID=abc rw").unwrap();
616 assert!(result.is_none());
617 }
618
619 #[test]
620 fn test_from_cmdline_insecure_prefix() {
621 let cmdline = format!("composefs=?{SHA256_HEX}");
622 let result = ComposefsCmdline::<Sha256HashValue>::from_cmdline(&cmdline)
623 .unwrap()
624 .unwrap();
625 assert!(result.is_insecure());
626 assert_eq!(result.digest().to_hex(), SHA256_HEX);
627 }
628
629 #[test]
630 fn test_make_cmdline_composefs_v1() {
631 use composefs::erofs::format::FormatVersion;
632 let result =
633 make_cmdline_composefs(SHA256_HEX, false, FormatVersion::V1, Algorithm::SHA256);
634 assert_eq!(
635 result,
636 format!("composefs.digest=v1-sha256-12:{SHA256_HEX}")
637 );
638 }
639
640 #[test]
641 fn test_make_cmdline_composefs_v1_sha512() {
642 use composefs::erofs::format::FormatVersion;
643 let result =
644 make_cmdline_composefs(SHA512_HEX, false, FormatVersion::V1, Algorithm::SHA512);
645 assert_eq!(
646 result,
647 format!("composefs.digest=v1-sha512-12:{SHA512_HEX}")
648 );
649 }
650
651 #[test]
652 fn test_validate_digest() {
653 let v1_digest = Sha256HashValue::from_hex(SHA256_HEX).unwrap();
654 let other = Sha256HashValue::from_hex(
655 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
656 )
657 .unwrap();
658 let karg = ComposefsCmdline::new_v2(v1_digest.clone(), false);
659
660 let acceptable = [&other, &v1_digest];
662 let matched = karg.validate_digest(acceptable.iter().copied()).unwrap();
663 assert_eq!(matched, &v1_digest);
664
665 let err = karg.validate_digest(std::iter::once(&other)).unwrap_err();
667 assert!(
668 err.to_string().contains("should be one of"),
669 "unexpected error message: {err}"
670 );
671 }
672
673 #[test]
674 fn test_make_cmdline_composefs_v2_insecure() {
675 use composefs::erofs::format::FormatVersion;
676 let result = make_cmdline_composefs(SHA256_HEX, true, FormatVersion::V2, Algorithm::SHA256);
677 assert_eq!(result, format!("composefs=?{SHA256_HEX}"));
678 }
679}