Skip to content

Commit 913c04f

Browse files
committed
Added initial logic to detect dm-verity
1 parent d8d0e84 commit 913c04f

3 files changed

Lines changed: 296 additions & 1 deletion

File tree

internal/image/imageinspect/compare.go

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,26 @@ type ImageDiff struct {
3939
PartitionTable PartitionTableDiff `json:"partitionTable,omitempty"`
4040
Partitions PartitionDiff `json:"partitions,omitempty"`
4141
EFIBinaries EFIBinaryDiff `json:"efiBinaries,omitempty"`
42+
Verity *VerityDiff `json:"verity,omitempty" yaml:"verity,omitempty"`
4243
}
4344

4445
// MetaDiff represents differences in image-level metadata.
4546
type MetaDiff struct {
4647
SizeBytes *ValueDiff[int64] `json:"sizeBytes,omitempty"`
4748
}
4849

50+
// VerityDiff represents differences in dm-verity configuration.
51+
type VerityDiff struct {
52+
Added *VerityInfo `json:"added,omitempty" yaml:"added,omitempty"`
53+
Removed *VerityInfo `json:"removed,omitempty" yaml:"removed,omitempty"`
54+
Changed bool `json:"changed,omitempty" yaml:"changed,omitempty"`
55+
56+
Enabled *ValueDiff[bool] `json:"enabled,omitempty" yaml:"enabled,omitempty"`
57+
Method *ValueDiff[string] `json:"method,omitempty" yaml:"method,omitempty"`
58+
RootDevice *ValueDiff[string] `json:"rootDevice,omitempty" yaml:"rootDevice,omitempty"`
59+
HashPartition *ValueDiff[int] `json:"hashPartition,omitempty" yaml:"hashPartition,omitempty"`
60+
}
61+
4962
// PartitionTableDiff represents differences in partition table-level fields.
5063
type PartitionTableDiff struct {
5164
DiskGUID *ValueDiff[string] `json:"diskGuid,omitempty"`
@@ -234,6 +247,12 @@ func CompareImages(from, to *ImageSummary) ImageCompareResult {
234247
res.Summary.Changed = true
235248
}
236249

250+
// --- dm-verity ---
251+
res.Diff.Verity = compareVerity(from.Verity, to.Verity)
252+
if res.Diff.Verity != nil && res.Diff.Verity.Changed {
253+
res.Summary.Changed = true
254+
}
255+
237256
// Deterministic ordering for stable JSON
238257
normalizeCompareResult(&res)
239258

@@ -284,6 +303,56 @@ func compareMeta(from, to ImageSummary) MetaDiff {
284303
return out
285304
}
286305

306+
func compareVerity(from, to *VerityInfo) *VerityDiff {
307+
// Both nil = no difference
308+
if from == nil && to == nil {
309+
return nil
310+
}
311+
312+
diff := &VerityDiff{}
313+
314+
// Added (to has verity, from doesn't)
315+
if from == nil && to != nil {
316+
diff.Added = to
317+
diff.Changed = true
318+
return diff
319+
}
320+
321+
// Removed (from has verity, to doesn't)
322+
if from != nil && to == nil {
323+
diff.Removed = from
324+
diff.Changed = true
325+
return diff
326+
}
327+
328+
// Both present
329+
if from.Enabled != to.Enabled {
330+
diff.Enabled = &ValueDiff[bool]{From: from.Enabled, To: to.Enabled}
331+
diff.Changed = true
332+
}
333+
334+
if from.Method != to.Method {
335+
diff.Method = &ValueDiff[string]{From: from.Method, To: to.Method}
336+
diff.Changed = true
337+
}
338+
339+
if from.RootDevice != to.RootDevice {
340+
diff.RootDevice = &ValueDiff[string]{From: from.RootDevice, To: to.RootDevice}
341+
diff.Changed = true
342+
}
343+
344+
if from.HashPartition != to.HashPartition {
345+
diff.HashPartition = &ValueDiff[int]{From: from.HashPartition, To: to.HashPartition}
346+
diff.Changed = true
347+
}
348+
349+
if !diff.Changed {
350+
return nil
351+
}
352+
353+
return diff
354+
}
355+
287356
// comparePartitionTable compares two PartitionTableSummary objects and returns a PartitionTableDiff.
288357
func comparePartitionTable(from, to PartitionTableSummary) PartitionTableDiff {
289358
var d PartitionTableDiff
@@ -751,6 +820,29 @@ func tallyDiffs(d ImageDiff) diffTally {
751820

752821
tallyEFIBinaryDiff(&t, d.EFIBinaries)
753822

823+
// dm-verity changes are meaningful (security-critical)
824+
if d.Verity != nil && d.Verity.Changed {
825+
if d.Verity.Added != nil {
826+
t.addMeaningful(1, "dm-verity enabled")
827+
} else if d.Verity.Removed != nil {
828+
t.addMeaningful(1, "dm-verity disabled")
829+
} else {
830+
// Field changes
831+
if d.Verity.Enabled != nil {
832+
t.addMeaningful(1, "dm-verity enabled status changed")
833+
}
834+
if d.Verity.Method != nil {
835+
t.addMeaningful(1, "dm-verity method changed")
836+
}
837+
if d.Verity.RootDevice != nil {
838+
t.addMeaningful(1, "dm-verity root device changed")
839+
}
840+
if d.Verity.HashPartition != nil {
841+
t.addMeaningful(1, "dm-verity hash partition changed")
842+
}
843+
}
844+
}
845+
754846
return t
755847
}
756848

internal/image/imageinspect/imageinspect.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,19 @@ type ImageSummary struct {
2727
SHA256 string `json:"sha256,omitempty"`
2828
SizeBytes int64 `json:"sizeBytes,omitempty"`
2929
PartitionTable PartitionTableSummary `json:"partitionTable,omitempty"`
30+
Verity *VerityInfo `json:"verity,omitempty" yaml:"verity,omitempty"`
3031
// SBOM SBOMSummary `json:"sbom,omitempty"`
3132
}
3233

34+
// VerityInfo holds dm-verity detection information.
35+
type VerityInfo struct {
36+
Enabled bool `json:"enabled" yaml:"enabled"`
37+
Method string `json:"method,omitempty" yaml:"method,omitempty"` // "systemd-verity", "custom-initramfs", "unknown"
38+
RootDevice string `json:"rootDevice,omitempty" yaml:"rootDevice,omitempty"`
39+
HashPartition int `json:"hashPartition,omitempty" yaml:"hashPartition,omitempty"` // partition index, 0 if none
40+
Notes []string `json:"notes,omitempty" yaml:"notes,omitempty"`
41+
}
42+
3343
// PartitionTableSummary holds information about the partition table of the disk image.
3444
type PartitionTableSummary struct {
3545
Type string
@@ -299,11 +309,15 @@ func (d *DiskfsInspector) inspectCore(
299309
}
300310
ptSummary.Partitions = partitionsWithFS
301311

312+
// Detect dm-verity configuration
313+
verityInfo := detectVerity(ptSummary)
314+
302315
return &ImageSummary{
303316
File: imagePath,
304317
SizeBytes: sizeBytes,
305318
PartitionTable: ptSummary,
306319
SHA256: sha256sum,
320+
Verity: verityInfo,
307321
}, nil
308322
}
309323

@@ -554,3 +568,119 @@ func computeFileSHA256(f *os.File) (string, error) {
554568

555569
return hex.EncodeToString(h.Sum(nil)), nil
556570
}
571+
572+
// detectVerity inspects the partition table and UKI cmdline to detect dm-verity configuration.
573+
func detectVerity(pt PartitionTableSummary) *VerityInfo {
574+
info := &VerityInfo{}
575+
576+
// Look for hash partition (common names/types)
577+
hashPartIdx := -1
578+
for i, p := range pt.Partitions {
579+
name := strings.ToLower(p.Name)
580+
// Check for common hash partition names
581+
if strings.Contains(name, "hash") || name == "roothashmap" {
582+
hashPartIdx = i
583+
info.HashPartition = p.Index
584+
info.Notes = append(info.Notes, fmt.Sprintf("Hash partition found: %s (partition %d)", p.Name, p.Index))
585+
break
586+
}
587+
}
588+
589+
// Extract cmdline from UKI if present
590+
var cmdline string
591+
for _, p := range pt.Partitions {
592+
if p.Filesystem != nil && p.Filesystem.HasUKI {
593+
for _, efi := range p.Filesystem.EFIBinaries {
594+
if efi.IsUKI && efi.Cmdline != "" {
595+
cmdline = efi.Cmdline
596+
break
597+
}
598+
}
599+
}
600+
if cmdline != "" {
601+
break
602+
}
603+
}
604+
605+
if cmdline == "" {
606+
// No cmdline found, dm-verity not detected
607+
if hashPartIdx >= 0 {
608+
info.Notes = append(info.Notes, "Hash partition exists but no UKI cmdline found")
609+
}
610+
return nil
611+
}
612+
613+
// Check for dm-verity indicators in cmdline
614+
// 1. systemd.verity_* parameters (standard systemd-verity)
615+
if strings.Contains(cmdline, "systemd.verity_name=") ||
616+
strings.Contains(cmdline, "systemd.verity_root_data=") ||
617+
strings.Contains(cmdline, "systemd.verity_root_hash=") {
618+
info.Enabled = true
619+
info.Method = "systemd-verity"
620+
info.Notes = append(info.Notes, "systemd.verity_* parameters found in cmdline")
621+
622+
// Extract root device from cmdline
623+
if strings.Contains(cmdline, "root=") {
624+
for _, part := range strings.Fields(cmdline) {
625+
if strings.HasPrefix(part, "root=") {
626+
info.RootDevice = strings.TrimPrefix(part, "root=")
627+
break
628+
}
629+
}
630+
}
631+
632+
if hashPartIdx >= 0 {
633+
info.Notes = append(info.Notes, fmt.Sprintf("Hash partition present at index %d", hashPartIdx))
634+
} else {
635+
info.Notes = append(info.Notes, "WARNING: systemd.verity_* found but no hash partition detected")
636+
}
637+
return info
638+
}
639+
640+
// 2. root=/dev/mapper/*verity* pattern (custom initramfs, e.g., EMT/EMF tpm-cryptsetup)
641+
if strings.Contains(cmdline, "root=/dev/mapper/") && strings.Contains(cmdline, "verity") {
642+
info.Enabled = true
643+
info.Method = "custom-initramfs"
644+
info.Notes = append(info.Notes, "root=/dev/mapper/*verity* pattern found in cmdline")
645+
646+
// Extract the exact root device
647+
for _, part := range strings.Fields(cmdline) {
648+
if strings.HasPrefix(part, "root=") {
649+
info.RootDevice = strings.TrimPrefix(part, "root=")
650+
break
651+
}
652+
}
653+
654+
if hashPartIdx >= 0 {
655+
info.Notes = append(info.Notes, fmt.Sprintf("Hash partition present at index %d", hashPartIdx))
656+
info.Notes = append(info.Notes, "Likely using separate hash partition for dm-verity")
657+
} else {
658+
info.Notes = append(info.Notes, "No separate hash partition detected")
659+
info.Notes = append(info.Notes, "Likely using custom initramfs (e.g., dracut tpm-cryptsetup module)")
660+
info.Notes = append(info.Notes, "Hash data may be: appended to rootfs, embedded in FDE, or managed by initramfs")
661+
}
662+
return info
663+
}
664+
665+
// 3. Check for roothash= parameter (direct hash specification)
666+
if strings.Contains(cmdline, "roothash=") {
667+
info.Enabled = true
668+
info.Method = "roothash-parameter"
669+
info.Notes = append(info.Notes, "roothash= parameter found in cmdline")
670+
671+
for _, part := range strings.Fields(cmdline) {
672+
if strings.HasPrefix(part, "root=") {
673+
info.RootDevice = strings.TrimPrefix(part, "root=")
674+
break
675+
}
676+
}
677+
678+
if hashPartIdx >= 0 {
679+
info.Notes = append(info.Notes, fmt.Sprintf("Hash partition present at index %d", hashPartIdx))
680+
}
681+
return info
682+
}
683+
684+
// No dm-verity detected
685+
return nil
686+
}

internal/image/imageinspect/renderer_text.go

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,12 @@ func RenderCompareText(w io.Writer, r *ImageCompareResult, opts CompareTextOptio
141141
renderEFIBinaryDiffText(w, r.Diff.EFIBinaries, " ")
142142
}
143143

144+
// dm-verity diff
145+
if r.Diff.Verity != nil && r.Diff.Verity.Changed {
146+
fmt.Fprintln(w)
147+
renderVerityDiffText(w, r.Diff.Verity)
148+
}
149+
144150
// Full mode: image metadata & volatile / meaningful remove reasons
145151
if mode == "full" {
146152
renderImagesBlock(w, r.From, r.To)
@@ -168,7 +174,11 @@ func RenderSummaryText(w io.Writer, summary *ImageSummary, opts TextOptions) err
168174

169175
// Partitions table (includes the “Partitions” header)
170176
renderPartitionTable(w, summary.PartitionTable)
171-
177+
// dm-verity section
178+
if summary.Verity != nil && summary.Verity.Enabled {
179+
fmt.Fprintln(w)
180+
renderVerityInfo(w, summary.Verity)
181+
}
172182
// Detailed per-partition filesystem blocks (ONLY ONCE)
173183
for _, p := range summary.PartitionTable.Partitions {
174184
if p.Filesystem == nil || isFilesystemEmpty(p.Filesystem) {
@@ -546,6 +556,69 @@ func renderPartitionTableHeader(w io.Writer, pt PartitionTableSummary) {
546556
_ = tw.Flush()
547557
}
548558

559+
func renderVerityInfo(w io.Writer, v *VerityInfo) {
560+
fmt.Fprintln(w, "dm-verity Configuration")
561+
fmt.Fprintln(w, "-----------------------")
562+
563+
tw := tabwriter.NewWriter(w, 0, 0, 3, ' ', 0)
564+
fmt.Fprintf(tw, "Enabled:\t%t\n", v.Enabled)
565+
if v.Method != "" {
566+
fmt.Fprintf(tw, "Method:\t%s\n", v.Method)
567+
}
568+
if v.RootDevice != "" {
569+
fmt.Fprintf(tw, "Root device:\t%s\n", v.RootDevice)
570+
}
571+
if v.HashPartition > 0 {
572+
fmt.Fprintf(tw, "Hash partition:\t%d\n", v.HashPartition)
573+
}
574+
_ = tw.Flush()
575+
576+
if len(v.Notes) > 0 {
577+
fmt.Fprintln(w, "Notes:")
578+
for _, note := range v.Notes {
579+
fmt.Fprintf(w, " - %s\n", note)
580+
}
581+
}
582+
}
583+
584+
func renderVerityDiffText(w io.Writer, d *VerityDiff) {
585+
fmt.Fprintln(w, "dm-verity:")
586+
587+
if d.Added != nil {
588+
fmt.Fprintln(w, " dm-verity ENABLED:")
589+
tw := tabwriter.NewWriter(w, 0, 0, 3, ' ', 0)
590+
fmt.Fprintf(tw, " Method:\t%s\n", d.Added.Method)
591+
if d.Added.RootDevice != "" {
592+
fmt.Fprintf(tw, " Root device:\t%s\n", d.Added.RootDevice)
593+
}
594+
if d.Added.HashPartition > 0 {
595+
fmt.Fprintf(tw, " Hash partition:\t%d\n", d.Added.HashPartition)
596+
}
597+
_ = tw.Flush()
598+
return
599+
}
600+
601+
if d.Removed != nil {
602+
fmt.Fprintln(w, " dm-verity DISABLED")
603+
fmt.Fprintf(w, " Previous method: %s\n", d.Removed.Method)
604+
return
605+
}
606+
607+
// Field changes
608+
if d.Enabled != nil {
609+
fmt.Fprintf(w, " Enabled: %t -> %t\n", d.Enabled.From, d.Enabled.To)
610+
}
611+
if d.Method != nil {
612+
fmt.Fprintf(w, " Method: %s -> %s\n", d.Method.From, d.Method.To)
613+
}
614+
if d.RootDevice != nil {
615+
fmt.Fprintf(w, " Root device: %s -> %s\n", d.RootDevice.From, d.RootDevice.To)
616+
}
617+
if d.HashPartition != nil {
618+
fmt.Fprintf(w, " Hash partition: %d -> %d\n", d.HashPartition.From, d.HashPartition.To)
619+
}
620+
}
621+
549622
func renderEFIArtifactsTable(w io.Writer, arts []EFIBinaryEvidence) {
550623

551624
fmt.Fprintln(w)

0 commit comments

Comments
 (0)