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

     1  // Copyright 2022 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 lib
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"crypto/ed25519"
    11  	"crypto/x509"
    12  	"encoding/base64"
    13  	"encoding/pem"
    14  	"errors"
    15  	"fmt"
    16  	"os"
    17  	"time"
    18  
    19  	"cloud.google.com/go/storage"
    20  
    21  	"go.chromium.org/luci/common/retry"
    22  	"go.chromium.org/luci/common/retry/transient"
    23  
    24  	"go.fuchsia.dev/infra/cmd/gcs-util/types"
    25  )
    26  
    27  const (
    28  	// The name of the public key file to be uploaded with release builds.
    29  	releasePubkeyFilename = "publickey.pem"
    30  
    31  	signatureKey = "signature"
    32  )
    33  
    34  // Sign signs the upload files and attaches the signatures as metadata.
    35  func Sign(uploads []types.Upload, privateKey ed25519.PrivateKey) ([]types.Upload, error) {
    36  	for i := range uploads {
    37  		if !uploads[i].Signed {
    38  			continue
    39  		}
    40  		data, err := os.ReadFile(uploads[i].Source)
    41  		if err != nil {
    42  			if os.IsNotExist(err) {
    43  				continue
    44  			}
    45  			return nil, fmt.Errorf("failed to read %s: %w", uploads[i].Source, err)
    46  		}
    47  		signature := base64.StdEncoding.EncodeToString(ed25519.Sign(privateKey, data))
    48  		if signature != "" {
    49  			uploads[i].Metadata = map[string]string{
    50  				signatureKey: signature,
    51  			}
    52  		}
    53  	}
    54  	return uploads, nil
    55  }
    56  
    57  // PrivateKey parses an ed25519 private key from a pem-formatted key file.
    58  // It expects the key to be in a PKCS#8, ASN.1 DER format as generated by
    59  // running `openssl genpkey -algorithm ed25519 -out <private key>.pem`.
    60  func PrivateKey(keyPath string) (ed25519.PrivateKey, error) {
    61  	if keyPath == "" {
    62  		return nil, nil
    63  	}
    64  
    65  	pemData, err := os.ReadFile(keyPath)
    66  	if err != nil {
    67  		return nil, fmt.Errorf("failed to read pkey: %w", err)
    68  	}
    69  	block, _ := pem.Decode(pemData)
    70  	if len(block.Bytes) == 0 {
    71  		return nil, fmt.Errorf("failed to decode private key")
    72  	}
    73  	// The decoded key should be in PKCS#8, ASN.1 DER form.
    74  	// We need to convert it to an ed25519.PrivateKey.
    75  	pkey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
    76  	if err != nil {
    77  		return nil, fmt.Errorf("failed to parse private key from DER bytes: %w", err)
    78  	}
    79  	if _, ok := pkey.(ed25519.PrivateKey); !ok {
    80  		return nil, fmt.Errorf("private key is not of type ed25519")
    81  	}
    82  	return pkey.(ed25519.PrivateKey), nil
    83  }
    84  
    85  // PublicKeyUpload returns an Upload representing the public key used for release builds.
    86  func PublicKeyUpload(publicKey ed25519.PublicKey) (*types.Upload, error) {
    87  	if len(publicKey) == 0 {
    88  		return nil, fmt.Errorf("nil public key provided")
    89  	}
    90  
    91  	// Convert to PKIX, ASN.1 DER form to match what is generated by openssl.
    92  	pubkeyDER, err := x509.MarshalPKIXPublicKey(publicKey)
    93  	if err != nil {
    94  		return nil, fmt.Errorf("failed to convert public key to DER form: %w", err)
    95  	}
    96  	block := &pem.Block{
    97  		Type:  "PUBLIC KEY",
    98  		Bytes: pubkeyDER,
    99  	}
   100  
   101  	var data bytes.Buffer
   102  	if err := pem.Encode(&data, block); err != nil {
   103  		return nil, fmt.Errorf("failed to encode public key: %w", err)
   104  	}
   105  	return &types.Upload{
   106  		Destination: releasePubkeyFilename,
   107  		Contents:    data.Bytes(),
   108  	}, nil
   109  }
   110  
   111  // Retry wraps a function that makes a GCS API call, adding retries for failures
   112  // that might be transient.
   113  func Retry(ctx context.Context, f func() error) error {
   114  	policy := transient.Only(func() retry.Iterator {
   115  		return &retry.ExponentialBackoff{
   116  			Limited: retry.Limited{
   117  				Delay:   1 * time.Second,
   118  				Retries: 4,
   119  			},
   120  			Multiplier: 2,
   121  		}
   122  	})
   123  	return retryWithPolicy(ctx, policy, f)
   124  }
   125  
   126  // Extracted to allow dependency injection for testing.
   127  func retryWithPolicy(ctx context.Context, policy retry.Factory, f func() error) error {
   128  	return retry.Retry(ctx, policy, func() error {
   129  		if err := f(); err != nil {
   130  			if errors.Is(err, storage.ErrBucketNotExist) || errors.Is(err, storage.ErrObjectNotExist) {
   131  				return err
   132  			}
   133  			return transient.Tag.Apply(err)
   134  		}
   135  		return nil
   136  	}, nil)
   137  }
   138  
   139  // ObjectAttrs gets the attributes for the given object, with retries.
   140  func ObjectAttrs(ctx context.Context, obj *storage.ObjectHandle) (*storage.ObjectAttrs, error) {
   141  	var objAttrs *storage.ObjectAttrs
   142  	err := Retry(ctx, func() error {
   143  		var err error
   144  		objAttrs, err = obj.Attrs(ctx)
   145  		return err
   146  	})
   147  	return objAttrs, err
   148  }
   149  
   150  // NewObjectReader gets a reader for the given object, with retries.
   151  func NewObjectReader(ctx context.Context, obj *storage.ObjectHandle) (*storage.Reader, error) {
   152  	var reader *storage.Reader
   153  	err := Retry(ctx, func() error {
   154  		var err error
   155  		reader, err = obj.NewReader(ctx)
   156  		return err
   157  	})
   158  	return reader, err
   159  }