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 }