diff --git a/internal/winapi/cimfs.go b/internal/winapi/cimfs.go index 6c026d9822..cc3d254120 100644 --- a/internal/winapi/cimfs.go +++ b/internal/winapi/cimfs.go @@ -56,3 +56,6 @@ type CimFsImagePath struct { //sys CimMergeMountImage(numCimPaths uint32, backingImagePaths *CimFsImagePath, flags uint32, volumeID *g) (hr error) = cimfs.CimMergeMountImage? //sys CimTombstoneFile(cimFSHandle FsHandle, path string) (hr error) = cimfs.CimTombstoneFile? //sys CimCreateMergeLink(cimFSHandle FsHandle, newPath string, oldPath string) (hr error) = cimfs.CimCreateMergeLink? +//sys CimSealImage(blockCimPath string, hashSize *uint64, fixedHeaderSize *uint64, hash *byte) (hr error) = cimfs.CimSealImage? +//sys CimGetVerificationInformation(blockCimPath string, isSealed *uint32, hashSize *uint64, signatureSize *uint64, fixedHeaderSize *uint64, hash *byte, signature *byte) (hr error) = cimfs.CimGetVerificationInformation? +//sys CimMountVerifiedImage(imagePath string, fsName string, flags uint32, volumeID *g, hashSize uint16, hash *byte) (hr error) = cimfs.CimMountVerifiedImage? diff --git a/internal/winapi/zsyscall_windows.go b/internal/winapi/zsyscall_windows.go index db4fc1c961..a7eea44ec7 100644 --- a/internal/winapi/zsyscall_windows.go +++ b/internal/winapi/zsyscall_windows.go @@ -68,8 +68,11 @@ var ( procCimCreateMergeLink = modcimfs.NewProc("CimCreateMergeLink") procCimDeletePath = modcimfs.NewProc("CimDeletePath") procCimDismountImage = modcimfs.NewProc("CimDismountImage") + procCimGetVerificationInformation = modcimfs.NewProc("CimGetVerificationInformation") procCimMergeMountImage = modcimfs.NewProc("CimMergeMountImage") procCimMountImage = modcimfs.NewProc("CimMountImage") + procCimMountVerifiedImage = modcimfs.NewProc("CimMountVerifiedImage") + procCimSealImage = modcimfs.NewProc("CimSealImage") procCimTombstoneFile = modcimfs.NewProc("CimTombstoneFile") procCimWriteStream = modcimfs.NewProc("CimWriteStream") procSetJobCompartmentId = modiphlpapi.NewProc("SetJobCompartmentId") @@ -491,6 +494,30 @@ func CimDismountImage(volumeID *g) (hr error) { return } +func CimGetVerificationInformation(blockCimPath string, isSealed *uint32, hashSize *uint64, signatureSize *uint64, fixedHeaderSize *uint64, hash *byte, signature *byte) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(blockCimPath) + if hr != nil { + return + } + return _CimGetVerificationInformation(_p0, isSealed, hashSize, signatureSize, fixedHeaderSize, hash, signature) +} + +func _CimGetVerificationInformation(blockCimPath *uint16, isSealed *uint32, hashSize *uint64, signatureSize *uint64, fixedHeaderSize *uint64, hash *byte, signature *byte) (hr error) { + hr = procCimGetVerificationInformation.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimGetVerificationInformation.Addr(), uintptr(unsafe.Pointer(blockCimPath)), uintptr(unsafe.Pointer(isSealed)), uintptr(unsafe.Pointer(hashSize)), uintptr(unsafe.Pointer(signatureSize)), uintptr(unsafe.Pointer(fixedHeaderSize)), uintptr(unsafe.Pointer(hash)), uintptr(unsafe.Pointer(signature))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + func CimMergeMountImage(numCimPaths uint32, backingImagePaths *CimFsImagePath, flags uint32, volumeID *g) (hr error) { hr = procCimMergeMountImage.Find() if hr != nil { @@ -535,6 +562,59 @@ func _CimMountImage(imagePath *uint16, fsName *uint16, flags uint32, volumeID *g return } +func CimMountVerifiedImage(imagePath string, fsName string, flags uint32, volumeID *g, hashSize uint16, hash *byte) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(imagePath) + if hr != nil { + return + } + var _p1 *uint16 + _p1, hr = syscall.UTF16PtrFromString(fsName) + if hr != nil { + return + } + return _CimMountVerifiedImage(_p0, _p1, flags, volumeID, hashSize, hash) +} + +func _CimMountVerifiedImage(imagePath *uint16, fsName *uint16, flags uint32, volumeID *g, hashSize uint16, hash *byte) (hr error) { + hr = procCimMountVerifiedImage.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimMountVerifiedImage.Addr(), uintptr(unsafe.Pointer(imagePath)), uintptr(unsafe.Pointer(fsName)), uintptr(flags), uintptr(unsafe.Pointer(volumeID)), uintptr(hashSize), uintptr(unsafe.Pointer(hash))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + +func CimSealImage(blockCimPath string, hashSize *uint64, fixedHeaderSize *uint64, hash *byte) (hr error) { + var _p0 *uint16 + _p0, hr = syscall.UTF16PtrFromString(blockCimPath) + if hr != nil { + return + } + return _CimSealImage(_p0, hashSize, fixedHeaderSize, hash) +} + +func _CimSealImage(blockCimPath *uint16, hashSize *uint64, fixedHeaderSize *uint64, hash *byte) (hr error) { + hr = procCimSealImage.Find() + if hr != nil { + return + } + r0, _, _ := syscall.SyscallN(procCimSealImage.Addr(), uintptr(unsafe.Pointer(blockCimPath)), uintptr(unsafe.Pointer(hashSize)), uintptr(unsafe.Pointer(fixedHeaderSize)), uintptr(unsafe.Pointer(hash))) + if int32(r0) < 0 { + if r0&0x1fff0000 == 0x00070000 { + r0 &= 0xffff + } + hr = syscall.Errno(r0) + } + return +} + func CimTombstoneFile(cimFSHandle FsHandle, path string) (hr error) { var _p0 *uint16 _p0, hr = syscall.UTF16PtrFromString(path) diff --git a/pkg/cimfs/cim_test.go b/pkg/cimfs/cim_test.go index 7e194421c8..bd744a4452 100644 --- a/pkg/cimfs/cim_test.go +++ b/pkg/cimfs/cim_test.go @@ -5,9 +5,11 @@ package cimfs import ( "bytes" + "context" "errors" "fmt" "io" + "strings" "syscall" "os" @@ -53,6 +55,14 @@ func (t *testBlockCIM) cimPath() string { return filepath.Join(t.BlockPath, t.CimName) } +type testVerifiedBlockCIM struct { + BlockCIM +} + +func (t *testVerifiedBlockCIM) cimPath() string { + return filepath.Join(t.BlockPath, t.CimName) +} + // A utility function to create a file/directory and write data to it in the given cim. func createCimFileUtil(c *CimFsWriter, fileTuple tuple) error { // create files inside the cim @@ -99,6 +109,8 @@ func openNewCIM(t *testing.T, newCIM testCIM) *CimFsWriter { writer, err = Create(val.imageDir, val.parentName, val.imageName) case *testBlockCIM: writer, err = CreateBlockCIM(val.BlockPath, val.CimName, val.Type) + case *testVerifiedBlockCIM: + writer, err = CreateBlockCIMWithOptions(context.Background(), &val.BlockCIM, WithDataIntegrity()) } if err != nil { t.Fatalf("failed while creating a cim: %s", err) @@ -666,3 +678,119 @@ func TestMergedLinksInMergedBlockCIMs(rootT *testing.T) { rootT.Logf("file contents don't match!") } } + +func TestVerifiedSingleFileBlockCIM(t *testing.T) { + if !IsVerifiedCimSupported() { + t.Skipf("verified CIMs are not supported") + } + + // contents to write to the CIM + testContents := []tuple{ + {"foo.txt", []byte("foo1"), false}, + {"bar.txt", []byte("bar"), false}, + } + + root := t.TempDir() + blockPath := filepath.Join(root, "layer.bcim") + tc := &testVerifiedBlockCIM{ + BlockCIM: BlockCIM{ + Type: BlockCIMTypeSingleFile, + BlockPath: blockPath, + CimName: "layer.cim", + }} + writer := openNewCIM(t, tc) + writeCIM(t, writer, testContents) + + mountvol := mountCIM(t, tc, CimMountVerifiedCim|CimMountSingleFileCim) + + compareContent(t, mountvol, testContents) +} + +func TestVerifiedSingleFileBlockCIMMount(t *testing.T) { + if !IsVerifiedCimSupported() { + t.Skipf("verified CIMs are not supported") + } + + // contents to write to the CIM + testContents := []tuple{ + {"foo.txt", []byte("foo1"), false}, + {"bar.txt", []byte("bar"), false}, + } + + root := t.TempDir() + blockPath := filepath.Join(root, "layer.bcim") + tc := &testVerifiedBlockCIM{ + BlockCIM: BlockCIM{ + Type: BlockCIMTypeSingleFile, + BlockPath: blockPath, + CimName: "layer.cim", + }} + writer := openNewCIM(t, tc) + writeCIM(t, writer, testContents) + + rootHash, err := GetVerificationInfo(blockPath) + if err != nil { + t.Fatalf("failed to get verification info: %s", err) + } + + // mount and read the contents of the cim + volumeGUID, err := guid.NewV4() + if err != nil { + t.Fatalf("generate cim mount GUID: %s", err) + } + + mountvol, err := MountVerifiedBlockCIM(&tc.BlockCIM, CimMountSingleFileCim, volumeGUID, rootHash) + if err != nil { + t.Fatalf("mount verified cim : %s", err) + } + t.Cleanup(func() { + if err := Unmount(mountvol); err != nil { + t.Logf("CIM unmount failed: %s", err) + } + }) + compareContent(t, mountvol, testContents) +} + +func TestVerifiedSingleFileBlockCIMMountReadFailure(t *testing.T) { + if !IsVerifiedCimSupported() { + t.Skipf("verified CIMs are not supported") + } + + // contents to write to the CIM + testContents := []tuple{ + {"foo.txt", []byte("foo1"), false}, + {"bar.txt", []byte("bar"), false}, + } + + root := t.TempDir() + blockPath := filepath.Join(root, "layer.bcim") + tc := &testVerifiedBlockCIM{ + BlockCIM: BlockCIM{ + Type: BlockCIMTypeSingleFile, + BlockPath: blockPath, + CimName: "layer.cim", + }} + writer := openNewCIM(t, tc) + writeCIM(t, writer, testContents) + + rootHash, err := GetVerificationInfo(blockPath) + if err != nil { + t.Fatalf("failed to get verification info: %s", err) + } + + // mount and read the contents of the cim + volumeGUID, err := guid.NewV4() + if err != nil { + t.Fatalf("generate cim mount GUID: %s", err) + } + + // change the rootHash slightly, this may cause the mount to fail due to integrity check. + rootHash[0] = rootHash[0] + 1 + + _, err = MountVerifiedBlockCIM(&tc.BlockCIM, CimMountSingleFileCim, volumeGUID, rootHash) + if err == nil { + t.Fatalf("mount verified cim should fail with integrity error") + } else if !strings.Contains(err.Error(), "integrity violation") { + t.Fatalf("expected integrity violation error") + } +} diff --git a/pkg/cimfs/cim_writer_windows.go b/pkg/cimfs/cim_writer_windows.go index affcac4ea7..62e45841f7 100644 --- a/pkg/cimfs/cim_writer_windows.go +++ b/pkg/cimfs/cim_writer_windows.go @@ -32,6 +32,8 @@ type CimFsWriter struct { activeStream winapi.StreamHandle // amount of bytes that can be written to the activeStream. activeLeft uint64 + // if true the CIM will be sealed after the writer is closed. + sealOnClose bool } // Create creates a new cim image. The CimFsWriter returned can then be used to do @@ -63,39 +65,110 @@ func Create(imagePath string, oldFSName string, newFSName string) (_ *CimFsWrite return &CimFsWriter{handle: handle, name: filepath.Join(imagePath, fsName)}, nil } -// Create creates a new block CIM and opens it for writing. The CimFsWriter -// returned can then be used to add/remove files to/from this CIM. -func CreateBlockCIM(blockPath, name string, blockType BlockCIMType) (_ *CimFsWriter, err error) { +// blockCIMConfig represents options for creating or merging block CIMs +type blockCIMConfig struct { + // ensures that the generted CIM is identical every time when created from the same source data. + // This is mostly required for image layers. Dissabled by default. + consistentCIM bool + // enables data integrity checking, which means the CIM will be verified and sealed on close. + // This is useful for ensuring that the CIM is tamper-proof. Disabled by default. + dataIntegrity bool +} + +// BlockCIMOpt is a function type for configuring block CIM creation options +type BlockCIMOpt func(*blockCIMConfig) error + +// enabled consistent CIM creation, this ensures that CIMs created from identical source data will always be identical (i.e. SHA256 digest of the CIM will remain same) +func WithConsistentCIM() BlockCIMOpt { + return func(opts *blockCIMConfig) error { + opts.consistentCIM = true + return nil + } +} + +// WithDataIntegrity enables data integrity checking (verified CIM with sealing on close) +func WithDataIntegrity() BlockCIMOpt { + return func(opts *blockCIMConfig) error { + opts.dataIntegrity = true + return nil + } +} + +// CreateBlockCIMWithOptions creates a new block CIM with the specified options and opens it for writing. +// The CimFsWriter returned can then be used to add/remove files to/from this CIM. +func CreateBlockCIMWithOptions(ctx context.Context, bCIM *BlockCIM, options ...BlockCIMOpt) (_ *CimFsWriter, err error) { + // Apply default options + config := &blockCIMConfig{} + + // Apply provided options + for _, option := range options { + if err = option(config); err != nil { + return nil, err + } + } + + // Validate options + if bCIM.BlockPath == "" || bCIM.CimName == "" { + return nil, fmt.Errorf("both blockPath & name must be non empty: %w", os.ErrInvalid) + } + + if bCIM.Type == BlockCIMTypeNone { + return nil, fmt.Errorf("invalid block CIM type `%d`: %w", bCIM.Type, os.ErrInvalid) + } + + // Check OS support if !IsBlockCimSupported() { return nil, fmt.Errorf("block CIM not supported on this OS version") } - if blockPath == "" || name == "" { - return nil, fmt.Errorf("both blockPath & name must be non empty: %w", os.ErrInvalid) + + if config.dataIntegrity && !IsVerifiedCimSupported() { + return nil, fmt.Errorf("verified CIMs are not supported on this OS version") } - // When creating block CIMs we always want them to be consistent CIMs i.e a CIMs - // created from the same layer tar will always be identical. - var createFlags = CimCreateFlagConsistentCim - switch blockType { + // Build create flags based on options + var createFlags uint32 + if config.consistentCIM { + createFlags |= CimCreateFlagConsistentCim + } + if config.dataIntegrity { + createFlags |= CimCreateFlagVerifiedCim + } + + switch bCIM.Type { case BlockCIMTypeDevice: createFlags |= CimCreateFlagBlockDeviceCim case BlockCIMTypeSingleFile: createFlags |= CimCreateFlagSingleFileCim default: - return nil, fmt.Errorf("invalid block CIM type `%d`: %w", blockType, os.ErrInvalid) + return nil, fmt.Errorf("invalid block CIM type `%d`: %w", bCIM.Type, os.ErrInvalid) } var newNameUTF16 *uint16 - newNameUTF16, err = windows.UTF16PtrFromString(name) + newNameUTF16, err = windows.UTF16PtrFromString(bCIM.CimName) if err != nil { return nil, err } var handle winapi.FsHandle - if err := winapi.CimCreateImage2(blockPath, createFlags, nil, newNameUTF16, &handle); err != nil { - return nil, fmt.Errorf("failed to create block CIM at path %s,%s: %w", blockPath, name, err) + if err := winapi.CimCreateImage2(bCIM.BlockPath, createFlags, nil, newNameUTF16, &handle); err != nil { + return nil, fmt.Errorf("failed to create block CIM at path %s,%s: %w", bCIM.BlockPath, bCIM.CimName, err) } - return &CimFsWriter{handle: handle, name: name}, nil + + return &CimFsWriter{ + handle: handle, + name: filepath.Join(bCIM.BlockPath, bCIM.CimName), + sealOnClose: config.dataIntegrity, // Seal on close if data integrity is enabled + }, nil +} + +// Create creates a new block CIM and opens it for writing. The CimFsWriter +// returned can then be used to add/remove files to/from this CIM. +func CreateBlockCIM(blockPath, name string, blockType BlockCIMType) (_ *CimFsWriter, err error) { + return CreateBlockCIMWithOptions(context.Background(), &BlockCIM{ + Type: blockType, + BlockPath: blockPath, + CimName: name, + }, WithConsistentCIM()) } // CreateAlternateStream creates alternate stream of given size at the given path inside the cim. This will @@ -268,7 +341,15 @@ func (c *CimFsWriter) Close() (err error) { } err = winapi.CimCloseImage(c.handle) c.handle = 0 - return err + if err != nil { + return &OpError{Cim: c.name, Op: "close", Err: err} + } + if c.sealOnClose { + if err = sealBlockCIM(filepath.Dir(c.name)); err != nil { + return &OpError{Cim: c.name, Op: "seal", Err: err} + } + } + return nil } // DestroyCim finds out the region files, object files of this cim and then delete the @@ -351,30 +432,44 @@ func GetCimUsage(ctx context.Context, cimPath string) (uint64, error) { // considered the base CIM. (i.e file with the same path in CIM at index 0 will shadow // files with the same path at all other CIMs) When mounting this merged CIM the source // CIMs MUST be provided in the exact same order. -func MergeBlockCIMs(mergedCIM *BlockCIM, sourceCIMs []*BlockCIM) (err error) { +func MergeBlockCIMsWithOpts(ctx context.Context, mergedCIM *BlockCIM, sourceCIMs []*BlockCIM, opts ...BlockCIMOpt) (err error) { if !IsMergedCimSupported() { return fmt.Errorf("merged CIMs aren't supported on this OS version") } else if len(sourceCIMs) < 2 { return fmt.Errorf("need at least 2 source CIMs, got %d: %w", len(sourceCIMs), os.ErrInvalid) } - var mergeFlag uint32 + // Apply default options + config := &blockCIMConfig{} + + // Apply provided options + for _, opt := range opts { + if err = opt(config); err != nil { + return err + } + } + + for _, sCIM := range sourceCIMs { + if sCIM.Type != mergedCIM.Type { + return fmt.Errorf("source CIM (%s) type MUST match with merged CIM type: %w", sCIM.String(), os.ErrInvalid) + } + } + + var mergeFlags uint32 switch mergedCIM.Type { case BlockCIMTypeDevice: - mergeFlag = CimMergeFlagBlockDevice + mergeFlags = CimMergeFlagBlockDevice case BlockCIMTypeSingleFile: - mergeFlag = CimMergeFlagSingleFile + mergeFlags = CimMergeFlagSingleFile default: return fmt.Errorf("invalid block CIM type `%d`: %w", mergedCIM.Type, os.ErrInvalid) } - for _, sCIM := range sourceCIMs { - if sCIM.Type != mergedCIM.Type { - return fmt.Errorf("source CIM (%s) type doesn't match with merged CIM type: %w", sCIM.String(), os.ErrInvalid) - } + if config.dataIntegrity { + mergeFlags |= CimMergeFlagVerifiedCim } - cim, err := CreateBlockCIM(mergedCIM.BlockPath, mergedCIM.CimName, mergedCIM.Type) + cim, err := CreateBlockCIMWithOptions(ctx, mergedCIM, opts...) if err != nil { return fmt.Errorf("create merged CIM: %w", err) } @@ -389,9 +484,52 @@ func MergeBlockCIMs(mergedCIM *BlockCIM, sourceCIMs []*BlockCIM) (err error) { // most CIM is added last. for _, sCIM := range sourceCIMs { fullPath := filepath.Join(sCIM.BlockPath, sCIM.CimName) - if err := winapi.CimAddFsToMergedImage2(cim.handle, fullPath, mergeFlag); err != nil { + if err := winapi.CimAddFsToMergedImage2(cim.handle, fullPath, mergeFlags); err != nil { return fmt.Errorf("add cim to merged image: %w", err) } } return nil } + +// MergeBlockCIMs creates a new merged BlockCIM from the provided source BlockCIMs. CIM +// at index 0 is considered to be topmost CIM and the CIM at index `length-1` is +// considered the base CIM. (i.e file with the same path in CIM at index 0 will shadow +// files with the same path at all other CIMs) When mounting this merged CIM the source +// CIMs MUST be provided in the exact same order. +func MergeBlockCIMs(mergedCIM *BlockCIM, sourceCIMs []*BlockCIM) (err error) { + return MergeBlockCIMsWithOpts(context.Background(), mergedCIM, sourceCIMs, WithConsistentCIM()) +} + +// sealBlockCIM seals a blockCIM at the given path so that no further modifications are allowed on it. This also writes a +// root hash in the block header so that in future any reads happening on the CIM can be easily verified against this root hash +// to detect tampering. +func sealBlockCIM(blockPath string) error { + var hashSize, fixedHeaderSize uint64 + hashBuf := make([]byte, cimHashSize) + if err := winapi.CimSealImage(blockPath, &hashSize, &fixedHeaderSize, &hashBuf[0]); err != nil { + return fmt.Errorf("failed to seal block CIM: %w", err) + } else if hashSize != cimHashSize { + return fmt.Errorf("unexpected cim hash size %d", hashSize) + } + return nil +} + +// GetVerificationInfo returns the root digest of the given block CIM. This is only +// applicable for CIMs that are sealed after writing. +func GetVerificationInfo(blockPath string) ([]byte, error) { + var ( + isSealed uint32 + hashSize uint64 + signatureSize uint64 + fixedHeaderSize uint64 + hash = make([]byte, cimHashSize) + ) + if err := winapi.CimGetVerificationInformation(blockPath, &isSealed, &hashSize, &signatureSize, &fixedHeaderSize, &hash[0], nil); err != nil { + return nil, fmt.Errorf("failed to get verification info from the CIM: %w", err) + } else if hashSize != cimHashSize { + return nil, fmt.Errorf("unexpected cim hash size %d", hashSize) + } else if isSealed == 0 { + return nil, fmt.Errorf("cim is not sealed") + } + return hash, nil +} diff --git a/pkg/cimfs/cimfs.go b/pkg/cimfs/cimfs.go index f301764387..3fb95df011 100644 --- a/pkg/cimfs/cimfs.go +++ b/pkg/cimfs/cimfs.go @@ -31,6 +31,15 @@ func IsBlockCimSupported() bool { return build >= 27766 } +// IsVerifiedCimSupported returns true if block CIM format supports also writing verification information in the CIM. +func IsVerifiedCimSupported() bool { + build := osversion.Build() + // TODO(ambarve): Currently we are checking against a higher build number since there is no + // official build with block CIM support yet. Once we have that build, we should + // update the build number here. + return build >= 27800 +} + func IsMergedCimSupported() bool { // The merged CIM support was originally added before block CIM support. However, // some of the merged CIM features that we use (e.g. merged hard links) were added @@ -49,6 +58,7 @@ const ( CimMountFlagEnableDax uint32 = 0x2 CimMountBlockDeviceCim uint32 = 0x10 CimMountSingleFileCim uint32 = 0x20 + CimMountVerifiedCim uint32 = 0x80 CimCreateFlagNone uint32 = 0x0 CimCreateFlagDoNotExpandPEImages uint32 = 0x1 @@ -56,10 +66,12 @@ const ( CimCreateFlagBlockDeviceCim uint32 = 0x4 CimCreateFlagSingleFileCim uint32 = 0x8 CimCreateFlagConsistentCim uint32 = 0x10 + CimCreateFlagVerifiedCim uint32 = 0x40 CimMergeFlagNone uint32 = 0x0 CimMergeFlagSingleFile uint32 = 0x1 CimMergeFlagBlockDevice uint32 = 0x2 + CimMergeFlagVerifiedCim uint32 = 0x4 ) // BlockCIM represents a CIM stored in a block formatted way. diff --git a/pkg/cimfs/common.go b/pkg/cimfs/common.go index 0a05f5a9d2..ab988aff06 100644 --- a/pkg/cimfs/common.go +++ b/pkg/cimfs/common.go @@ -15,6 +15,10 @@ import ( "github.com/Microsoft/hcsshim/pkg/cimfs/format" ) +const ( + cimHashSize = 32 // size of a hash of a verified CIM in bytes +) + var ( // Equivalent to SDDL of "D:NO_ACCESS_CONTROL". nullSd = []byte{1, 0, 4, 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0} diff --git a/pkg/cimfs/doc.go b/pkg/cimfs/doc.go index bb9ce57717..c40f38cc69 100644 --- a/pkg/cimfs/doc.go +++ b/pkg/cimfs/doc.go @@ -24,6 +24,16 @@ newFSName string) (_ *CimFsWriter, err error)` function defined in this package, block CIMs can be created with the `func CreateBlockCIM(blockPath, oldName, newName string, blockType BlockCIMType) (_ *CimFsWriter, err error)` function. +Verified CIMs: +A block CIM can also provide integrity checking (via a hash/Merkel tree, +similar to dm-verity on Linux). If a CIM is written and sealed, it generates a +root hash of all of its contents and shares it back with the client. Any +verified CIM can be mounted by passing a hash that we expect to be its root +hash. All read operations on such a mounted CIM will then validate that the +generated root hash matches with the one that was provided at mount time. If it +doesn't match the read fails. This allows us to guarantee that the CIM based +layered aren't being modified underneath us. + Forking & Merging CIMs: In container world, CIMs are used for storing container image layers. Usually, one layer is stored in one CIM. This means we need a way to combine multiple CIMs to create the diff --git a/pkg/cimfs/mount_cim.go b/pkg/cimfs/mount_cim.go index c9dacffd68..e9c305f604 100644 --- a/pkg/cimfs/mount_cim.go +++ b/pkg/cimfs/mount_cim.go @@ -118,3 +118,32 @@ func MountMergedBlockCIMs(mergedCIM *BlockCIM, sourceCIMs []*BlockCIM, mountFlag } return fmt.Sprintf("\\\\?\\Volume{%s}\\", volumeGUID.String()), nil } + +// Mounts a verified block CIM with the provided root hash. The root hash is usually +// returned when the CIM is sealed or the root hash can be queried from a block CIM. +// Every read on the mounted volume will be verified to match against the provided root +// hash if it doesn't, the read will fail. The CIM MUST have been created with the +// verified creation flag. +func MountVerifiedBlockCIM(bCIM *BlockCIM, mountFlags uint32, volumeGUID guid.GUID, rootHash []byte) (string, error) { + if len(rootHash) != cimHashSize { + return "", fmt.Errorf("unexpected root hash size %d, expected size is %d", len(rootHash), cimHashSize) + } + + // The CimMountVerifiedCim flag should only be used when using the regular mount + // CIM API. That flag is required to tell that API that this is a verified + // CIM. This API doesn't need that flag as it is already assumed that the CIM is + // verified. + switch bCIM.Type { + case BlockCIMTypeDevice: + mountFlags |= CimMountBlockDeviceCim + case BlockCIMTypeSingleFile: + mountFlags |= CimMountSingleFileCim + default: + return "", fmt.Errorf("invalid block CIM type `%d`: %w", bCIM.Type, os.ErrInvalid) + } + + if err := winapi.CimMountVerifiedImage(bCIM.BlockPath, bCIM.CimName, mountFlags, &volumeGUID, cimHashSize, &rootHash[0]); err != nil { + return "", &MountError{Cim: bCIM.String(), Op: "MountVerifiedCIM", Err: err} + } + return fmt.Sprintf("\\\\?\\Volume{%s}\\", volumeGUID.String()), nil +}