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 }