- Implement gpgVerify function that creates a temporary keyring to verify detached signatures against embedded public keys - Signature verification happens during deserialization after hash validation but before decompression - Extract signatureString() as a method on manifest for generating the canonical signature string (MAGIC-UUID-MULTIHASH) - Add --require-signature flag to check command to mandate signature from a specific GPG key ID - Expose IsSigned() and Signer() methods on Checker for signature status
177 lines
4.8 KiB
Go
177 lines
4.8 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/dustin/go-humanize"
|
|
"github.com/spf13/afero"
|
|
"github.com/urfave/cli/v2"
|
|
"sneak.berlin/go/mfer/internal/log"
|
|
"sneak.berlin/go/mfer/mfer"
|
|
)
|
|
|
|
// findManifest looks for a manifest file in the given directory.
|
|
// It checks for index.mf and .index.mf, returning the first one found.
|
|
func findManifest(fs afero.Fs, dir string) (string, error) {
|
|
candidates := []string{"index.mf", ".index.mf"}
|
|
for _, name := range candidates {
|
|
path := filepath.Join(dir, name)
|
|
exists, err := afero.Exists(fs, path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if exists {
|
|
return path, nil
|
|
}
|
|
}
|
|
return "", fmt.Errorf("no manifest found in %s (looked for index.mf and .index.mf)", dir)
|
|
}
|
|
|
|
func (mfa *CLIApp) checkManifestOperation(ctx *cli.Context) error {
|
|
log.Debug("checkManifestOperation()")
|
|
|
|
var manifestPath string
|
|
var err error
|
|
|
|
if ctx.Args().Len() > 0 {
|
|
arg := ctx.Args().Get(0)
|
|
// Check if arg is a directory or a file
|
|
info, statErr := mfa.Fs.Stat(arg)
|
|
if statErr == nil && info.IsDir() {
|
|
// It's a directory, look for manifest inside
|
|
manifestPath, err = findManifest(mfa.Fs, arg)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
// Treat as a file path
|
|
manifestPath = arg
|
|
}
|
|
} else {
|
|
// No argument, look in current directory
|
|
manifestPath, err = findManifest(mfa.Fs, ".")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
basePath := ctx.String("base")
|
|
showProgress := ctx.Bool("progress")
|
|
|
|
log.Infof("checking manifest %s with base %s", manifestPath, basePath)
|
|
|
|
// Create checker
|
|
chk, err := mfer.NewChecker(manifestPath, basePath, mfa.Fs)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to load manifest: %w", err)
|
|
}
|
|
|
|
// Check signature requirement
|
|
requiredSigner := ctx.String("require-signature")
|
|
if requiredSigner != "" {
|
|
if !chk.IsSigned() {
|
|
return fmt.Errorf("manifest is not signed, but signature from %s is required", requiredSigner)
|
|
}
|
|
signer := chk.Signer()
|
|
if signer == nil {
|
|
return fmt.Errorf("manifest signature has no signer fingerprint")
|
|
}
|
|
// Compare signer - the required key ID might be a suffix of the full fingerprint
|
|
signerStr := string(signer)
|
|
if !strings.EqualFold(signerStr, requiredSigner) && !strings.HasSuffix(strings.ToUpper(signerStr), strings.ToUpper(requiredSigner)) {
|
|
return fmt.Errorf("manifest signed by %s, but %s is required", signerStr, requiredSigner)
|
|
}
|
|
log.Infof("manifest signature verified (signer: %s)", signerStr)
|
|
}
|
|
|
|
log.Infof("manifest contains %d files, %s", chk.FileCount(), humanize.IBytes(uint64(chk.TotalBytes())))
|
|
|
|
// Set up results channel
|
|
results := make(chan mfer.Result, 1)
|
|
|
|
// Set up progress channel
|
|
var progress chan mfer.CheckStatus
|
|
if showProgress {
|
|
progress = make(chan mfer.CheckStatus, 1)
|
|
go func() {
|
|
for status := range progress {
|
|
if status.ETA > 0 {
|
|
log.Progressf("Checking: %d/%d files, %s/s, ETA %s, %d failures",
|
|
status.CheckedFiles,
|
|
status.TotalFiles,
|
|
humanize.IBytes(uint64(status.BytesPerSec)),
|
|
status.ETA.Round(time.Second),
|
|
status.Failures)
|
|
} else {
|
|
log.Progressf("Checking: %d/%d files, %s/s, %d failures",
|
|
status.CheckedFiles,
|
|
status.TotalFiles,
|
|
humanize.IBytes(uint64(status.BytesPerSec)),
|
|
status.Failures)
|
|
}
|
|
}
|
|
log.ProgressDone()
|
|
}()
|
|
}
|
|
|
|
// Process results in a goroutine
|
|
var failures int64
|
|
done := make(chan struct{})
|
|
go func() {
|
|
for result := range results {
|
|
if result.Status != mfer.StatusOK {
|
|
failures++
|
|
log.Infof("%s: %s (%s)", result.Status, result.Path, result.Message)
|
|
} else {
|
|
log.Verbosef("%s: %s", result.Status, result.Path)
|
|
}
|
|
}
|
|
close(done)
|
|
}()
|
|
|
|
// Run check
|
|
err = chk.Check(ctx.Context, results, progress)
|
|
if err != nil {
|
|
return fmt.Errorf("check failed: %w", err)
|
|
}
|
|
|
|
// Wait for results processing to complete
|
|
<-done
|
|
|
|
// Check for extra files if requested
|
|
if ctx.Bool("no-extra-files") {
|
|
extraResults := make(chan mfer.Result, 1)
|
|
extraDone := make(chan struct{})
|
|
go func() {
|
|
for result := range extraResults {
|
|
failures++
|
|
log.Infof("%s: %s (%s)", result.Status, result.Path, result.Message)
|
|
}
|
|
close(extraDone)
|
|
}()
|
|
|
|
err = chk.FindExtraFiles(ctx.Context, extraResults)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check for extra files: %w", err)
|
|
}
|
|
<-extraDone
|
|
}
|
|
|
|
elapsed := time.Since(mfa.startupTime).Seconds()
|
|
rate := float64(chk.TotalBytes()) / elapsed
|
|
if failures == 0 {
|
|
log.Infof("checked %d files (%s) in %.1fs (%s/s): all OK", chk.FileCount(), humanize.IBytes(uint64(chk.TotalBytes())), elapsed, humanize.IBytes(uint64(rate)))
|
|
} else {
|
|
log.Infof("checked %d files (%s) in %.1fs (%s/s): %d failed", chk.FileCount(), humanize.IBytes(uint64(chk.TotalBytes())), elapsed, humanize.IBytes(uint64(rate)), failures)
|
|
}
|
|
|
|
if failures > 0 {
|
|
mfa.exitCode = 1
|
|
}
|
|
|
|
return nil
|
|
}
|