go.fuchsia.dev/infra@v0.0.0-20240507153436-9b593402251b/cmd/gcs-util/verify_blobs.go (about)

     1  // Copyright 2023 The Fuchsia Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license that can be
     3  // found in the LICENSE file.
     4  
     5  package main
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"encoding/json"
    11  	"errors"
    12  	"fmt"
    13  	"os"
    14  	"os/exec"
    15  	"path/filepath"
    16  	"strings"
    17  	"sync"
    18  
    19  	"github.com/maruel/subcommands"
    20  	"go.chromium.org/luci/common/logging"
    21  	"go.chromium.org/luci/common/logging/gologger"
    22  
    23  	"go.fuchsia.dev/infra/cmd/gcs-util/types"
    24  )
    25  
    26  // Canonical GCS namespace pattern for versioned blobs.
    27  const blobNamespacePattern = "blobs/[0-9]*/*"
    28  
    29  // This command exists to verify delivery blobs per security considerations
    30  // noted in RFC-0207. See fxbug.dev/124944 for more details.
    31  func cmdVerifyBlobs() *subcommands.Command {
    32  	return &subcommands.Command{
    33  		UsageLine: "verify-blobs -blobfs-compression-path <blobfs-compression-path> -manifest-path <manifest-path>",
    34  		ShortDesc: "Verify blobs in an upload manifest.",
    35  		LongDesc:  "Verify blobs in an upload manifest.",
    36  		CommandRun: func() subcommands.CommandRun {
    37  			c := &verifyBlobsCmd{}
    38  			c.Init()
    39  			return c
    40  		},
    41  	}
    42  }
    43  
    44  type verifyBlobsCmd struct {
    45  	subcommands.CommandRunBase
    46  	blobfsCompressionPath string
    47  	manifestPath          string
    48  	j                     int
    49  	logLevel              logging.Level
    50  }
    51  
    52  func (c *verifyBlobsCmd) Init() {
    53  	c.Flags.StringVar(&c.blobfsCompressionPath, "blobfs-compression-path", "", "Path to blobfs-compression tool.")
    54  	c.Flags.StringVar(&c.manifestPath, "manifest-path", "", "Path to upload manifest.")
    55  	c.Flags.IntVar(&c.j, "j", 32, "Maximum number of concurrent uploading processes.")
    56  	c.Flags.Var(&c.logLevel, "log-level", "Logging level. Can be debug, info, warning, or error.")
    57  }
    58  
    59  func (c *verifyBlobsCmd) parseArgs() error {
    60  	if c.blobfsCompressionPath == "" {
    61  		return errors.New("-blobfs-compression-path is required")
    62  	}
    63  	if c.manifestPath == "" {
    64  		return errors.New("-manifest-path is required")
    65  	}
    66  	return nil
    67  }
    68  
    69  func (c *verifyBlobsCmd) Run(a subcommands.Application, _ []string, _ subcommands.Env) int {
    70  	if err := c.parseArgs(); err != nil {
    71  		fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
    72  		return 1
    73  	}
    74  	if err := c.main(); err != nil {
    75  		fmt.Fprintf(a.GetErr(), "%s: %s\n", a.GetName(), err)
    76  		return 1
    77  	}
    78  	return 0
    79  }
    80  
    81  // getBlobs returns any Uploads in the input manifest whose destination lies in
    82  // the canonical namespaces for delivery blobs.
    83  func getBlobs(ctx context.Context, manifest []types.Upload) ([]types.Upload, error) {
    84  	var blobs []types.Upload
    85  	for _, upload := range manifest {
    86  		matched, err := filepath.Match(blobNamespacePattern, upload.Destination)
    87  		if err != nil {
    88  			return nil, err
    89  		}
    90  		if matched {
    91  			blobs = append(blobs, upload)
    92  		}
    93  	}
    94  	logging.Debugf(ctx, "got %d blobs", len(blobs))
    95  	return blobs, nil
    96  }
    97  
    98  // merkleRoot returns the merkle root of the given blob.
    99  type merkleRoot func(blob types.Upload, tool string) (string, error)
   100  
   101  // runBlobfsCompression runs the blobfs-compression tool to get the merkle root
   102  // of the given blob.
   103  func runBlobfsCompression(blob types.Upload, tool string) (string, error) {
   104  	var stdout, stderr bytes.Buffer
   105  	cmd := exec.Command(tool, fmt.Sprintf("--calculate_digest=%s", blob.Source))
   106  	cmd.Stdout = &stdout
   107  	cmd.Stderr = &stderr
   108  	if err := cmd.Run(); err != nil {
   109  		return "", fmt.Errorf("could not compute merkle root of blob %s: %w\n%s", blob.Source, err, stderr.String())
   110  	}
   111  	return strings.TrimSuffix(stdout.String(), "\n"), nil
   112  }
   113  
   114  // verifyBlobs checks that the merkle root of each blob is equal to its
   115  // filename.
   116  func verifyBlobs(ctx context.Context, blobs []types.Upload, tool string, f merkleRoot, j int) error {
   117  	if j <= 0 {
   118  		return fmt.Errorf("concurrency factor j must be a positive number")
   119  	}
   120  	toVerify := make(chan types.Upload, j)
   121  	errs := make(chan error, j)
   122  
   123  	queueBlobs := func() {
   124  		defer close(toVerify)
   125  		for _, blob := range blobs {
   126  			toVerify <- blob
   127  		}
   128  	}
   129  	var wg sync.WaitGroup
   130  	wg.Add(j)
   131  	verify := func() {
   132  		defer wg.Done()
   133  		for blob := range toVerify {
   134  			err := verifyBlob(ctx, blob, tool, f)
   135  			if err != nil {
   136  				errs <- err
   137  			}
   138  		}
   139  	}
   140  
   141  	go queueBlobs()
   142  	for range j {
   143  		go verify()
   144  	}
   145  	go func() {
   146  		wg.Wait()
   147  		close(errs)
   148  	}()
   149  
   150  	if err := <-errs; err != nil {
   151  		return err
   152  	}
   153  	return nil
   154  }
   155  
   156  // verifyBlob checks that the merkle root of the blob is equal to the blob's
   157  // filename.
   158  func verifyBlob(ctx context.Context, blob types.Upload, tool string, f merkleRoot) error {
   159  	logging.Debugf(ctx, "verifying blob %s", blob.Source)
   160  	m, err := f(blob, tool)
   161  	if err != nil {
   162  		return err
   163  	}
   164  	if m != filepath.Base(blob.Source) {
   165  		return fmt.Errorf("blob source %s does not match merkle root %s", blob.Source, m)
   166  	}
   167  	if m != filepath.Base(blob.Destination) {
   168  		return fmt.Errorf("blob destination %s does not match merkle root %s", blob.Destination, m)
   169  	}
   170  	return nil
   171  }
   172  
   173  func (c *verifyBlobsCmd) main() error {
   174  	ctx := context.Background()
   175  	ctx = logging.SetLevel(ctx, c.logLevel)
   176  	ctx = gologger.StdConfig.Use(ctx)
   177  
   178  	jsonInput, err := os.ReadFile(c.manifestPath)
   179  	if err != nil {
   180  		return err
   181  	}
   182  	var manifest []types.Upload
   183  	if err := json.Unmarshal(jsonInput, &manifest); err != nil {
   184  		return err
   185  	}
   186  	blobs, err := getBlobs(ctx, manifest)
   187  	if err != nil {
   188  		return err
   189  	}
   190  	return verifyBlobs(ctx, blobs, c.blobfsCompressionPath, runBlobfsCompression, c.j)
   191  }