Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
c7e5d14
Implement fsverity functionality
Jenkins-J Jan 9, 2024
5c9db94
Set proper fsverity block size
Jenkins-J Jan 9, 2024
7dc5829
Clean up fsverity implementation
Jenkins-J Jan 12, 2024
7644a95
Integrate fsveriy
Jenkins-J Jan 12, 2024
5438a73
Add fsverity debug messages
Jenkins-J Jan 19, 2024
d178237
Add fsverity package imports
Jenkins-J Jan 22, 2024
3d453ae
Fix errors in fsverity package
Jenkins-J Jan 23, 2024
bbe117e
Fix types of constants when assinging to structs
Jenkins-J Jan 23, 2024
3918790
Fix type of maxDigestSize constant
Jenkins-J Jan 23, 2024
8bf3c2a
Fix errors in variable names
Jenkins-J Jan 23, 2024
0c1023d
Add more details to fsverity logs for debugging
Jenkins-J Jan 23, 2024
5b13b9a
Set fsverity block size correctly
Jenkins-J Jan 23, 2024
c1e16fd
Fix block size field name
Jenkins-J Jan 23, 2024
333e54f
Fix nil map error
Jenkins-J Jan 23, 2024
4abca35
Add blob details to debug logs
Jenkins-J Jan 23, 2024
a1487ed
Check if labels are getting properly added
Jenkins-J Jan 23, 2024
91af4a2
Change position of log for more detail
Jenkins-J Jan 23, 2024
737649c
Add TODO comments for improvements
Jenkins-J Jan 26, 2024
f4eb728
Store fsverity digest values
Jenkins-J Jan 29, 2024
458b8a9
Fix errors
Jenkins-J Jan 29, 2024
bf5e0e5
Remove integrity digests when the blob is deleted
Jenkins-J Jan 30, 2024
8aec398
Fix typos
Jenkins-J Jan 30, 2024
4c2f56e
Add local function
Jenkins-J Jan 30, 2024
7f37729
Refactor ReaderAt
Jenkins-J Jan 30, 2024
15dc96e
Refactor content writer
Jenkins-J Jan 30, 2024
ddaa0f2
Move measure function
Jenkins-J Jan 31, 2024
a52deba
Create fsverity IsSupported function
Jenkins-J Feb 9, 2024
dbb0b8b
Use IsSupported function in content store
Jenkins-J Feb 9, 2024
d1157e7
Remove fsverity IsEnabled check
Jenkins-J Feb 12, 2024
ae4a24c
Separate integrity logic
Jenkins-J Feb 12, 2024
d6f9074
Separate integrity validation logic
Jenkins-J Feb 12, 2024
4c80710
Remove old logging messages
Jenkins-J Feb 13, 2024
d1e074c
Make fsverity IsSupported function more robust
Jenkins-J Feb 14, 2024
1017208
Close test verity file before enabling fsverity
Jenkins-J Feb 14, 2024
f6b8e5a
Modify IsSupported definition
Jenkins-J Feb 14, 2024
5b52535
Modify fsverity IsSupported
Jenkins-J Feb 15, 2024
6f6a3e6
Use sync once with fsverity IsSupported
Jenkins-J Feb 15, 2024
5644a40
Modify fsverity struct field names
Jenkins-J Feb 16, 2024
23a1e3d
Check if fsverity is enabled on the filesystem
Jenkins-J Feb 16, 2024
3c37f6e
Check if fsverity is enabled on the filesystem
Jenkins-J Feb 16, 2024
abe0e90
Merge branch 'fsverity-fs-check' into fsverity-builtin
Jenkins-J Feb 16, 2024
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
48 changes: 48 additions & 0 deletions content/local/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/containerd/containerd/v2/content"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/filters"
"github.com/containerd/containerd/v2/pkg/fsverity"
"github.com/containerd/log"

"github.com/opencontainers/go-digest"
Expand Down Expand Up @@ -128,6 +129,11 @@ func (s *store) ReaderAt(ctx context.Context, desc ocispec.Descriptor) (content.
return nil, fmt.Errorf("calculating blob path for ReaderAt: %w", err)
}

log.G(ctx).Debugf("Getting reader for blob %v", p)
if err = validateIntegrity(s.root, p, desc); err != nil {
log.G(ctx).Errorf("error validating integrity value of blob %v: %s", p, err.Error())
}

reader, err := OpenReader(p)
if err != nil {
return nil, fmt.Errorf("blob %s expected at %s: %w", desc.Digest, p, err)
Expand All @@ -154,6 +160,15 @@ func (s *store) Delete(ctx context.Context, dgst digest.Digest) error {
return fmt.Errorf("content %v: %w", dgst, errdefs.ErrNotFound)
}

integrityFile := filepath.Join(s.root, "integrity", dgst.Encoded())
if err := os.RemoveAll(integrityFile); err != nil {
if !os.IsNotExist(err) {
return err
}

return fmt.Errorf("integrity file %v: %w", dgst, errdefs.ErrNotFound)
}

return nil
}

Expand Down Expand Up @@ -684,3 +699,36 @@ func writeToCompletion(path string, data []byte, mode os.FileMode) error {
}
return nil
}

func validateIntegrity(rootPath string, p string, desc ocispec.Descriptor) error {
// validate the integrity of the blob if integrity validation is supported
if supported := fsverity.IsSupported(rootPath); !supported {
return fmt.Errorf("integrity validation is not supported")
}

var verityDigest string
verityDigest, merr := fsverity.Measure(p)
if merr != nil {
return fmt.Errorf("failed to take fsverity measurement of blob: %s", merr.Error())
}

var expectedDigest string
integrityFile := filepath.Join(rootPath, "integrity", desc.Digest.Encoded())
ifd, err := os.Open(integrityFile)
if err != nil {
return fmt.Errorf("could not read expected integrity value of %s", p)
}
b, err := io.ReadAll(ifd)
if err != nil {
return fmt.Errorf("could not read fsverity digest from integrity file: %s", err.Error())
} else {
expectedDigest = string(b)
}

// compare the digest to the "good" value stored in the blob label
if verityDigest != expectedDigest {
return fmt.Errorf("blob not trusted: fsverity digest does not match the expected digest value")
}

return nil
}
51 changes: 51 additions & 0 deletions content/local/writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (

"github.com/containerd/containerd/v2/content"
"github.com/containerd/containerd/v2/errdefs"
"github.com/containerd/containerd/v2/pkg/fsverity"
"github.com/containerd/log"
"github.com/opencontainers/go-digest"
)
Expand Down Expand Up @@ -137,6 +138,10 @@ func (w *writer) Commit(ctx context.Context, size int64, expected digest.Digest,
return err
}

if err = storeIntegrity(w.s.root, target, dgst); err != nil {
log.G(ctx).Errorf("failed to store integrity of blob %v: %s", target, err.Error())
}

// Ingest has now been made available in the content store, attempt to complete
// setting metadata but errors should only be logged and not returned since
// the content store cannot be cleanly rolled back.
Expand All @@ -151,6 +156,7 @@ func (w *writer) Commit(ctx context.Context, size int64, expected digest.Digest,
log.G(ctx).WithField("ref", w.ref).WithField("path", w.path).Error("failed to remove ingest directory")
}

log.G(ctx).Debugf("content labels: %v", base.Labels)
if w.s.ls != nil && base.Labels != nil {
if err := w.s.ls.Set(dgst, base.Labels); err != nil {
log.G(ctx).WithField("digest", dgst).Error("failed to set labels")
Expand Down Expand Up @@ -206,3 +212,48 @@ func (w *writer) Truncate(size int64) error {
}
return w.fp.Truncate(0)
}

func storeIntegrity(rootPath string, target string, dgst digest.Digest) error {
if supported := fsverity.IsSupported(rootPath); !supported {
return fmt.Errorf("integrity validation is not supported")
}

var verityDigest string
if err := fsverity.Enable(target); err != nil {
return fmt.Errorf("failed to enable fsverity verification: %s", err.Error())
}

verityDigest, merr := fsverity.Measure(target)
if merr != nil {
return fmt.Errorf("failed to take fsverity measurement of blob: %s", merr.Error())
}

integrityStore := filepath.Join(rootPath, "integrity")
if err := os.MkdirAll(integrityStore, 0755); err != nil {
return fmt.Errorf("error creating integrity digest directory: %s", err)
}

digestPath := filepath.Join(integrityStore, dgst.Encoded())
_, err := os.Stat(digestPath)
if err != nil {
if os.IsExist(err) {
return fmt.Errorf("integrity digest for blob already exists: %s", err)
}
}

digestFile, err := os.Create(digestPath)
if err != nil {
if os.IsExist(err) {
return fmt.Errorf("error creating integrity digest file: %s", err)
}

return fmt.Errorf("Error creating integrity digest file for blob: %s", dgst.Encoded())
}

_, err = digestFile.WriteString(verityDigest)
if err != nil {
return fmt.Errorf("error writing vsverity digest to file: %s", err)
}

return nil
}
170 changes: 170 additions & 0 deletions pkg/fsverity/fsverity_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
//go:build linux

/*
Copyright The containerd Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package fsverity

import (
"fmt"
"os"
"path/filepath"
"sync"
"syscall"
"unsafe"

"github.com/containerd/containerd/v2/contrib/seccomp/kernelversion"
"golang.org/x/sys/unix"
)

type fsverityEnableArg struct {
version uint32
hashAlgorithm uint32
blockSize uint32
saltSize uint32
saltPtr uint64
sigSize uint32
reserved1 uint32
sigPtr uint64
reserved2 [11]uint64
}

type fsverityDigest struct {
digestAlgorithm uint16
digestSize uint16
digest [64]uint8
}

const (
defaultBlockSize int = 4096
maxDigestSize uint16 = 64
)

var (
once sync.Once
supported bool
)

func IsSupported(rootPath string) bool {
once.Do(func () {
minKernelVersion := kernelversion.KernelVersion{Kernel: 5, Major: 4}
s, err := kernelversion.GreaterEqualThan(minKernelVersion)
if err != nil {
supported = s
return
}

integrityStore := filepath.Join(rootPath, "integrity")
if err = os.MkdirAll(integrityStore, 0755); err != nil {
supported = false
return
}

digestPath := filepath.Join(integrityStore, "supported")
digestFile, err := os.Create(digestPath)
if err != nil {
supported = false
return
}
digestFile.Close()
defer os.Remove(digestPath)

eerr := Enable(digestPath)
if eerr != nil {
supported = false
return
}

supported = true
})
return supported
}

func IsEnabled(path string) (bool, error) {
f, err := os.Open(path)
if err != nil {
return false, fmt.Errorf("Error opening file: %s", err)
}

var attr int

_, _, flagErr := unix.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(unix.FS_IOC_GETFLAGS), uintptr(unsafe.Pointer(&attr)))
if flagErr != 0 {
return false, fmt.Errorf("Error getting inode flags: %s", flagErr)
}

if attr&unix.FS_VERITY_FL == unix.FS_VERITY_FL {
return true, nil
}

return false, nil
}

func Enable(path string) error {
f, err := os.Open(path)
if err != nil {
return fmt.Errorf("Error opening file: %s\n", err.Error())
}

var args *fsverityEnableArg = &fsverityEnableArg{}
args.version = 1
args.hashAlgorithm = 1

// fsverity block size should be the minimum between the page size
// and the file system block size
// If neither value is retrieved successfully, set fsverity block size to the default value
blockSize := unix.Getpagesize()

s := unix.Stat_t{}
serr := unix.Stat(path, &s)
if serr == nil && int(s.Blksize) < blockSize {
blockSize = int(s.Blksize)
}

if blockSize <= 0 {
blockSize = defaultBlockSize
}

args.blockSize = uint32(blockSize)

_, _, errno := unix.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(unix.FS_IOC_ENABLE_VERITY), uintptr(unsafe.Pointer(args)))
if errno != 0 {
return fmt.Errorf("Enable fsverity failed: %d\n", errno)
}

return nil
}

func Measure(path string) (string, error) {
var verityDigest string
f, err := os.Open(path)
if err != nil {
return verityDigest, fmt.Errorf("Error opening file: %s\n", err.Error())
}

var d *fsverityDigest = &fsverityDigest{digestSize: maxDigestSize}
_, _, errno := unix.Syscall(syscall.SYS_IOCTL, f.Fd(), uintptr(unix.FS_IOC_MEASURE_VERITY), uintptr(unsafe.Pointer(d)))
if errno != 0 {
return verityDigest, fmt.Errorf("Measure fsverity failed: %d\n", errno)
}

var i uint16
for i = 0; i < (*d).digestSize; i++ {
verityDigest = fmt.Sprintf("%s%x", verityDigest, (*d).digest[i])
}

return verityDigest, nil
}
7 changes: 7 additions & 0 deletions pkg/fsverity/fsverity_other.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
//go:build !linux

package fsverity

func IsSupported(rootPath string) bool {
return false
}