Files
server-deploy/internal/aptrepo/artifacts.go

118 lines
3.2 KiB
Go

package aptrepo
import (
"crypto/sha256"
"encoding/hex"
"errors"
"io"
"os"
"path"
"path/filepath"
"time"
"server-deploy/internal/installplan"
)
// VerifyArtifacts authenticates repository metadata anew before checking the
// selected five deb files. It never accepts a caller-asserted authenticated lock.
// The staging directories and ancestors must be trusted and not concurrently
// modified. This is observation evidence, not a capability to later execute paths.
func VerifyArtifacts(metadataDirectory, artifactDirectory, suite, arch string, versions map[string]string, now time.Time) (Result, error) {
result, err := Verify(metadataDirectory, suite, arch, versions, now)
if err != nil {
return Result{}, err
}
if err := verifyArtifacts(artifactDirectory, result.Lock); err != nil {
return Result{}, err
}
result.PackageBytesVerified = true
return result, nil
}
func verifyArtifacts(directory string, lock installplan.Lock) error {
reject := errors.New("invalid staged Docker artifacts")
if _, err := installplan.Validate(lock, lock.Suite, lock.Architecture); err != nil {
return reject
}
if !filepath.IsAbs(directory) {
return reject
}
directory = filepath.Clean(directory)
before, err := os.Lstat(directory)
if err != nil || !before.IsDir() || before.Mode()&os.ModeSymlink != 0 {
return reject
}
root, err := os.OpenRoot(directory)
if err != nil {
return reject
}
defer root.Close()
opened, err := root.Stat(".")
if err != nil || !os.SameFile(before, opened) {
return reject
}
expected := make(map[string]installplan.Package, len(lock.Packages))
for _, p := range lock.Packages {
expected[path.Base(p.Filename)] = p
}
dir, err := root.Open(".")
if err != nil {
return reject
}
defer dir.Close()
// Enumerate at most the expected count plus one, never an unbounded directory.
seen := make(map[string]bool)
for {
entries, err := dir.ReadDir(1)
if err != nil && err != io.EOF {
return reject
}
if len(entries) == 0 {
if err == io.EOF {
break
}
return reject
}
name := entries[0].Name()
p, ok := expected[name]
if !ok || seen[name] {
return reject
}
seen[name] = true
if err := verifyArtifact(root, name, p); err != nil {
return reject
}
}
if len(seen) != len(expected) {
return reject
}
return nil
}
func verifyArtifact(root *os.Root, name string, p installplan.Package) error {
reject := errors.New("artifact size or digest mismatch")
before, err := root.Lstat(name)
if err != nil || !before.Mode().IsRegular() || before.Size() != int64(p.Size) {
return reject
}
file, err := root.Open(name)
if err != nil {
return reject
}
defer file.Close()
opened, err := file.Stat()
if err != nil || !opened.Mode().IsRegular() || !os.SameFile(before, opened) || opened.Size() != before.Size() {
return reject
}
digest := sha256.New()
n, err := io.CopyBuffer(digest, io.LimitReader(file, int64(p.Size)+1), make([]byte, 64<<10))
if err != nil || n != int64(p.Size) || "sha256:"+hex.EncodeToString(digest.Sum(nil)) != p.Digest {
return reject
}
after, err := file.Stat()
if err != nil || after.Size() != opened.Size() || !after.ModTime().Equal(opened.ModTime()) {
return reject
}
return nil
}