| // Copyright 2023 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package main |
| |
| import ( |
| "bytes" |
| "context" |
| "encoding/json" |
| "errors" |
| "fmt" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strings" |
| "sync" |
| |
| "github.com/maruel/subcommands" |
| "go.chromium.org/luci/common/logging" |
| "go.chromium.org/luci/common/logging/gologger" |
| |
| "go.fuchsia.dev/infra/cmd/gcs-util/types" |
| ) |
| |
| // Canonical GCS namespace pattern for versioned blobs. |
| const blobNamespacePattern = "blobs/[0-9]*/*" |
| |
| // This command exists to verify delivery blobs per security considerations |
| // noted in RFC-0207. See fxbug.dev/124944 for more details. |
| func cmdVerifyBlobs() *subcommands.Command { |
| return &subcommands.Command{ |
| UsageLine: "verify-blobs -blobfs-compression-path <blobfs-compression-path> -manifest-path <manifest-path>", |
| ShortDesc: "Verify blobs in an upload manifest.", |
| LongDesc: "Verify blobs in an upload manifest.", |
| CommandRun: func() subcommands.CommandRun { |
| c := &verifyBlobsCmd{} |
| c.Init() |
| return c |
| }, |
| } |
| } |
| |
| type verifyBlobsCmd struct { |
| subcommands.CommandRunBase |
| blobfsCompressionPath string |
| manifestPath string |
| j int |
| logLevel logging.Level |
| } |
| |
| func (c *verifyBlobsCmd) Init() { |
| c.Flags.StringVar(&c.blobfsCompressionPath, "blobfs-compression-path", "", "Path to blobfs-compression tool.") |
| c.Flags.StringVar(&c.manifestPath, "manifest-path", "", "Path to upload manifest.") |
| c.Flags.IntVar(&c.j, "j", 32, "Maximum number of concurrent uploading processes.") |
| c.Flags.Var(&c.logLevel, "log-level", "Logging level. Can be debug, info, warning, or error.") |
| } |
| |
| func (c *verifyBlobsCmd) parseArgs() error { |
| if c.blobfsCompressionPath == "" { |
| return errors.New("-blobfs-compression-path is required") |
| } |
| if c.manifestPath == "" { |
| return errors.New("-manifest-path is required") |
| } |
| return nil |
| } |
| |
| func (c *verifyBlobsCmd) Run(a subcommands.Application, _ []string, _ subcommands.Env) int { |
| if err := c.parseArgs(); err != nil { |
| fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err) |
| return 1 |
| } |
| if err := c.main(); err != nil { |
| fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err) |
| return 1 |
| } |
| return 0 |
| } |
| |
| // getBlobs returns any Uploads in the input manifest whose destination lies in |
| // the canonical namespaces for delivery blobs. |
| func getBlobs(ctx context.Context, manifest []types.Upload) ([]types.Upload, error) { |
| var blobs []types.Upload |
| for _, upload := range manifest { |
| matched, err := filepath.Match(blobNamespacePattern, upload.Destination) |
| if err != nil { |
| return nil, err |
| } |
| if matched { |
| blobs = append(blobs, upload) |
| } |
| } |
| logging.Debugf(ctx, "got %d blobs", len(blobs)) |
| return blobs, nil |
| } |
| |
| // merkleRoot returns the merkle root of the given blob. |
| type merkleRoot func(blob types.Upload, tool string) (string, error) |
| |
| // runBlobfsCompression runs the blobfs-compression tool to get the merkle root |
| // of the given blob. |
| func runBlobfsCompression(blob types.Upload, tool string) (string, error) { |
| var stdout, stderr bytes.Buffer |
| cmd := exec.Command(tool, fmt.Sprintf("--calculate_digest=%s", blob.Source)) |
| cmd.Stdout = &stdout |
| cmd.Stderr = &stderr |
| if err := cmd.Run(); err != nil { |
| return "", fmt.Errorf("could not compute merkle root of blob %s: %w\n%s", blob.Source, err, stderr.String()) |
| } |
| return strings.TrimSuffix(stdout.String(), "\n"), nil |
| } |
| |
| // verifyBlobs checks that the merkle root of each blob is equal to its |
| // filename. |
| func verifyBlobs(ctx context.Context, blobs []types.Upload, tool string, f merkleRoot, j int) error { |
| if j <= 0 { |
| return fmt.Errorf("concurrency factor j must be a positive number") |
| } |
| toVerify := make(chan types.Upload, j) |
| errs := make(chan error, j) |
| |
| queueBlobs := func() { |
| defer close(toVerify) |
| for _, blob := range blobs { |
| toVerify <- blob |
| } |
| } |
| var wg sync.WaitGroup |
| wg.Add(j) |
| verify := func() { |
| defer wg.Done() |
| for blob := range toVerify { |
| err := verifyBlob(ctx, blob, tool, f) |
| if err != nil { |
| errs <- err |
| } |
| } |
| } |
| |
| go queueBlobs() |
| for i := 0; i < j; i++ { |
| go verify() |
| } |
| go func() { |
| wg.Wait() |
| close(errs) |
| }() |
| |
| if err := <-errs; err != nil { |
| return err |
| } |
| return nil |
| } |
| |
| // verifyBlob checks that the merkle root of the blob is equal to the blob's |
| // filename. |
| func verifyBlob(ctx context.Context, blob types.Upload, tool string, f merkleRoot) error { |
| logging.Debugf(ctx, "verifying blob %s", blob.Source) |
| m, err := f(blob, tool) |
| if err != nil { |
| return err |
| } |
| if m != filepath.Base(blob.Source) { |
| return fmt.Errorf("blob source %s does not match merkle root %s", blob.Source, m) |
| } |
| if m != filepath.Base(blob.Destination) { |
| return fmt.Errorf("blob destination %s does not match merkle root %s", blob.Destination, m) |
| } |
| return nil |
| } |
| |
| func (c *verifyBlobsCmd) main() error { |
| ctx := context.Background() |
| ctx = logging.SetLevel(ctx, c.logLevel) |
| ctx = gologger.StdConfig.Use(ctx) |
| |
| jsonInput, err := os.ReadFile(c.manifestPath) |
| if err != nil { |
| return err |
| } |
| var manifest []types.Upload |
| if err := json.Unmarshal(jsonInput, &manifest); err != nil { |
| return err |
| } |
| blobs, err := getBlobs(ctx, manifest) |
| if err != nil { |
| return err |
| } |
| return verifyBlobs(ctx, blobs, c.blobfsCompressionPath, runBlobfsCompression, c.j) |
| } |