Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 46 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Creates VHD files (Linux) or CIM files (Windows) for each layer of a container i
#### Windows Examples

```bash
# From container registry (requires Windows host or WSL with interop)
# From container registry
./dmverity-vhd.exe create \
-i mcr.microsoft.com/windows/nanoserver:ltsc2025 \
-o ./output \
Expand All @@ -125,7 +125,7 @@ Creates VHD files (Linux) or CIM files (Windows) for each layer of a container i

### `roothash` - Compute Root Hashes

Computes the root hash for each layer without creating VHD/CIM files. Useful for verification or comparison.
Computes the root hash for each layer without creating VHD/CIM files. Useful for verification or comparison. Outputs JSON format for easy integration with other tools.

#### Linux Examples

Expand All @@ -147,6 +147,8 @@ Computes the root hash for each layer without creating VHD/CIM files. Useful for

#### Windows Examples

These work on both PowerShell and WSL2 provided you have `CimWriter.dll` in `C:\Windows\System32`. For deterministic hashes, use Windows Server 2025 or later.

```bash
# From container registry
./dmverity-vhd.exe roothash \
Expand All @@ -158,19 +160,50 @@ Computes the root hash for each layer without creating VHD/CIM files. Useful for
--platform windows/amd64
```

#### Output Example
#### Output Format

**Linux Output (JSON):**
```json
{
"layers": [
"75f76b2620207ef52a83803bb27b3243a51b13304950ee97fd4a2540cd2f465f",
"d7adf568bac4ee8b05efd56775ce1504a66834781b189a8067ab5b71f513d440",
"e3701df664d4fc1d5bd68ef5e5f82d66b67bd13038822b1adb8de22cc24915ac"
]
}
```
Layer 0 root hash: 75f76b2620207ef52a83803bb27b3243a51b13304950ee97fd4a2540cd2f465f
Layer 1 root hash: d7adf568bac4ee8b05efd56775ce1504a66834781b189a8067ab5b71f513d440
Layer 2 root hash: e3701df664d4fc1d5bd68ef5e5f82d66b67bd13038822b1adb8de22cc24915ac
```

For Windows multi-layer images, also shows:
**Windows Output (JSON):**

For single-layer images:
```json
{
"layers": [
"75f76b2620207ef52a83803bb27b3243a51b13304950ee97fd4a2540cd2f465f"
],
"mounted_cim": [
"75f76b2620207ef52a83803bb27b3243a51b13304950ee97fd4a2540cd2f465f"
]
}
```
Merged layer hash: 849f1104f01c006729222c65e014eb4070608e75b9860f3411ca4b5a77b658c5

For multi-layer images:
```json
{
"layers": [
"24af9c8b208189c4846926c47963835daecfbd71168d15da1af247c749b58873",
"24980812fb450c07daea8b3c72e78ade540b48cd59b649a181cac189fe93e22d"
],
"mounted_cim": [
"849f1104f01c006729222c65e014eb4070608e75b9860f3411ca4b5a77b658c5"
]
}
```

**Field Descriptions:**
- `layers`: Array of root hashes for each layer in the image (in order)
- `mounted_cim`: (Windows only) The hash of the mounted/merged CIM filesystem. For single-layer images, this is the same as the layer hash. For multi-layer images, this is the merged CIM hash.

#### Roothash Command Flags

```bash
Expand Down Expand Up @@ -250,13 +283,15 @@ Converts a single tar file to either ext4 (Linux) or CIM (Windows) with integrit
- Creates `.vhd` files with dm-verity integrity protection
- Merkle tree and superblock embedded in VHD
- Can use `--hash-dev-vhd` to separate hash device
- `roothash` command outputs JSON with `layers` array

### Windows (`--platform windows/amd64`)
- Creates `.bcim` files (Block CIM format)
- Integrity checksums embedded in CIM structure
- Multi-layer images also generate merged hash
- Multi-layer images generate merged CIM hash for the actual mounted filesystem
- `roothash` command outputs JSON with `layers` array and `mounted_cim` field

**Note:** Windows CIM creation requires the Windows cimwriter.dll. On Linux, you can use WSL2 with Windows interop enabled to run the `.exe` binary. But to get deterministic hashes on image repull, you need to use this tool on a WS2025 OS.
**Note:** Windows CIM creation requires the Windows CimWriter.dll. On Linux, you can use WSL2 to run the `.exe` binary. For deterministic hashes on image repull, you need to use this tool on Windows Server 2025 or later.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to specify the particular minimum WS2025


## Output Files

Expand Down
59 changes: 59 additions & 0 deletions cmd/dmverity-vhd/cim.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,71 @@ import (
"os"
"path/filepath"
"strings"
"sync"

"github.com/Microsoft/hcsshim/pkg/cimfs"
cimimport "github.com/Microsoft/hcsshim/pkg/ociwclayer/cim"
log "github.com/sirupsen/logrus"
"golang.org/x/sys/windows"
)

type ParentLayers []*cimfs.BlockCIM

var (
checkWindowsVersionOnce sync.Once
windowsVersionError error
)

// checkWindowsVersion verifies that we're running on Windows Server 2025 (build 26100) or newer.
// CIM file generation on older versions will not produce deterministic hashes.
// Returns an error if the version requirement is not met, unless debug mode is enabled.
func checkWindowsVersion() error {
checkWindowsVersionOnce.Do(func() {
ver := windows.RtlGetVersion()
if ver == nil {
windowsVersionError = fmt.Errorf("failed to get Windows version information")
return
}

build := ver.BuildNumber
// Windows Server 2025 is build 26100
// Deterministic CIM file generation requires WS2025+
if build < 26100 {
if debugSkipVersionCheck {
// Debug mode: warn but allow execution
log.WithFields(log.Fields{
"current_build": build,
"required_build": 26100,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need the more than this. 26100 => Windows 2025. There is a version number of WS2025 (today's one) that we require. Note that my Windows 11 25H2 is 26200.7985 and it is not suitable. I guess you need to check xxxxx.yyyy where xxxxx == 26100 and yyyy >= today's build.

"debug_mode": true,
}).Warn("DEBUG MODE: Running on pre-WS2025. Hashes may not be deterministic. This is for development/testing only.")
windowsVersionError = nil
} else {
// Production mode: fail with error
windowsVersionError = fmt.Errorf(
"Windows Server 2025 (build 26100) or newer is required for deterministic Windows container hashes. "+
"Current build: %d. Windows platform processing cannot continue on this version.",
build,
)
log.WithFields(log.Fields{
"current_build": build,
"required_build": 26100,
}).Error("Insufficient Windows version for deterministic CIM generation")
}
} else {
log.WithField("build", build).Debug("Windows version check passed")
}
})
return windowsVersionError
}

func tarToCim(tarReader io.Reader, parentLayers ParentLayers, out string, layerName string) (string, ParentLayers, error) {
log.Trace("tarToCim called")

// Check Windows version for deterministic hash support
if err := checkWindowsVersion(); err != nil {
return "", parentLayers, err
}

// If no out path is given, use a temp directory
var err error
if out == "" {
Expand Down Expand Up @@ -70,6 +124,11 @@ func tarToCim(tarReader io.Reader, parentLayers ParentLayers, out string, layerN
func generateMergedCim(parentLayers ParentLayers, out string, mergedName string) (string, error) {
log.Trace("generateMergedCim called")

// Check Windows version for deterministic hash support
if err := checkWindowsVersion(); err != nil {
return "", err
}

if mergedName == "" {
mergedName = "merged"
}
Expand Down
7 changes: 7 additions & 0 deletions cmd/dmverity-vhd/instrumentation.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,13 @@ func setLoggingLevel(ctx *cli.Context) {
}
}

func setDebugSkipVersionCheck(ctx *cli.Context) {
debugSkipVersionCheck = ctx.GlobalBool(debugSkipVersionFlag)
if debugSkipVersionCheck {
log.Warn("DEBUG MODE: Windows version check will be skipped. This is for development only and may produce non-deterministic hashes.")
}
}

var profilerEnabled bool = false

func setupProfiler(ctx *cli.Context) {
Expand Down
47 changes: 30 additions & 17 deletions cmd/dmverity-vhd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,24 +12,28 @@ import (
)

const (
usernameFlag = "username"
passwordFlag = "password"
platformFlag = "platform"
inputFlag = "input"
outputFlag = "output"
typeFlag = "type"
verboseFlag = "verbose"
traceFlag = "trace"
profilerFlag = "profiler" // enable profiling
outputDirFlag = "out-dir"
dockerFlag = "docker"
bufferedReaderFlag = "buffered-reader"
tarballFlag = "tarball"
hashDeviceVhdFlag = "hash-dev-vhd"
dataVhdFlag = "data-vhd"
maxVHDSize = dmverity.RecommendedVHDSizeGB
usernameFlag = "username"
passwordFlag = "password"
platformFlag = "platform"
inputFlag = "input"
outputFlag = "output"
typeFlag = "type"
verboseFlag = "verbose"
traceFlag = "trace"
profilerFlag = "profiler" // enable profiling
outputDirFlag = "out-dir"
dockerFlag = "docker"
bufferedReaderFlag = "buffered-reader"
tarballFlag = "tarball"
hashDeviceVhdFlag = "hash-dev-vhd"
dataVhdFlag = "data-vhd"
debugSkipVersionFlag = "debug-skip-version-check"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not entirely debug. We'll need it when using base2-hyp. Can I suggest "ignore-windows-version". There might also be a check in the hcsshim code BTW.

maxVHDSize = dmverity.RecommendedVHDSizeGB
)

// Global variable to control Windows version check strictness
var debugSkipVersionCheck bool = false

func init() {
log.SetFormatter(&log.TextFormatter{
DisableTimestamp: false,
Expand Down Expand Up @@ -82,6 +86,10 @@ func main() {
Name: profilerFlag,
Usage: "Optional: profile and put the results in this file",
},
cli.BoolFlag{
Name: debugSkipVersionFlag,
Usage: "Optional: DEBUG ONLY - skip Windows version check (allows running on pre-WS2025 with warnings)",
},
}

if err := app.Run(os.Args); err != nil {
Expand Down Expand Up @@ -128,6 +136,7 @@ var createVHDCommand = cli.Command{
Action: func(ctx *cli.Context) error {
setupProfiler(ctx)
setLoggingLevel(ctx)
setDebugSkipVersionCheck(ctx)
log.Trace("createVHDCommand called")

imageName, outDir, platform, verityHashDev, verityData, imageFetcher, imageParser, manifestParser, err := parseCreateVhdArgs(ctx)
Expand Down Expand Up @@ -165,13 +174,15 @@ var rootHashVHDCommand = cli.Command{
Action: func(ctx *cli.Context) error {
setupProfiler(ctx)
setLoggingLevel(ctx)
setDebugSkipVersionCheck(ctx)
log.Trace("rootHashVHDCommand called")

imageFetcher, imageParser, manifestParser, layerParser, mergedHashGenerator, err := parseRoothashArgs(ctx)
if err != nil {
return err
}
err = roothash(imageFetcher, imageParser, manifestParser, layerParser, mergedHashGenerator)
platform := ctx.String(platformFlag)
err = roothash(imageFetcher, imageParser, manifestParser, layerParser, mergedHashGenerator, platform)
stopProfiler(ctx)
return err
},
Expand All @@ -195,6 +206,7 @@ var hashLayerCommand = cli.Command{
Action: func(ctx *cli.Context) error {
setupProfiler(ctx)
setLoggingLevel(ctx)
setDebugSkipVersionCheck(ctx)
log.Trace("hashLayerCommand called")

tarPath, platform, err := parseHashLayerArgs(ctx)
Expand Down Expand Up @@ -233,6 +245,7 @@ var tar2hashedCommand = cli.Command{
Action: func(ctx *cli.Context) error {
setupProfiler(ctx)
setLoggingLevel(ctx)
setDebugSkipVersionCheck(ctx)
log.Trace("tar2hashedCommand called")

srcTarPath := ctx.String(inputFlag)
Expand Down
36 changes: 31 additions & 5 deletions cmd/dmverity-vhd/roothash.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package main

import (
"encoding/json"
"fmt"
"io"
"os"
Expand All @@ -12,6 +13,11 @@ import (
"github.com/urfave/cli"
)

type RootHashOutput struct {
Layers []string `json:"layers"`
MountedCim []string `json:"mounted_cim,omitempty"`
}

type MergedHashGenerator func(layerCount int) (string, error)

func parseRoothashArgs(ctx *cli.Context) (
Expand Down Expand Up @@ -78,6 +84,7 @@ func roothash(
manifestParser ManifestParser,
layerParser LayerParser,
mergedHashGenerator MergedHashGenerator,
platform string,
) error {
log.Trace("roothash called")

Expand All @@ -96,30 +103,49 @@ func roothash(
return err
}

// Print the layer number to layer hash
// Collect layer hashes in order
var layerHashes []string
var missingLayers []int
for layerNumber := 0; layerNumber < len(layerDigests); layerNumber++ {
hash, ok := layerDigestToHash[layerDigests[layerNumber]]
if !ok {
missingLayers = append(missingLayers, layerNumber)
continue
}
fmt.Fprintf(os.Stdout, "Layer %d root hash: %s\n", layerNumber, hash)
layerHashes = append(layerHashes, hash)
}
if len(missingLayers) > 0 {
return fmt.Errorf("missing root hashes for layers: %v", missingLayers)
}

// Generate and print merged hash if applicable
// Generate merged hash if applicable
var mergedHash string
if mergedHashGenerator != nil {
mergedHash, err := mergedHashGenerator(len(layerDigests))
mergedHash, err = mergedHashGenerator(len(layerDigests))
if err != nil {
return fmt.Errorf("failed to generate merged hash: %w", err)
}
}

// Output format depends on platform
output := RootHashOutput{
Layers: layerHashes,
}

if strings.HasPrefix(platform, "windows") {
// Add mounted_cim for Windows: either merged hash or single layer hash
if mergedHash != "" {
fmt.Fprintf(os.Stdout, "Merged layer hash: %s\n", mergedHash)
output.MountedCim = []string{mergedHash}
} else if len(layerHashes) == 1 {
output.MountedCim = []string{layerHashes[0]}
}
}
// Both Linux and Windows output JSON
jsonData, err := json.MarshalIndent(output, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal JSON output: %w", err)
}
fmt.Fprintf(os.Stdout, "%s\n", jsonData)

return nil
}
Loading