...

Source file src/cloud.google.com/go/storage/post_policy_v4.go

Documentation: cloud.google.com/go/storage

     1  // Copyright 2020 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package storage
    16  
    17  import (
    18  	"crypto"
    19  	"crypto/rand"
    20  	"crypto/rsa"
    21  	"crypto/sha256"
    22  	"encoding/base64"
    23  	"encoding/json"
    24  	"errors"
    25  	"fmt"
    26  	"net/url"
    27  	"strings"
    28  	"time"
    29  )
    30  
    31  // PostPolicyV4Options are used to construct a signed post policy.
    32  // Please see https://cloud.google.com/storage/docs/xml-api/post-object
    33  // for reference about the fields.
    34  type PostPolicyV4Options struct {
    35  	// GoogleAccessID represents the authorizer of the signed post policy generation.
    36  	// It is typically the Google service account client email address from
    37  	// the Google Developers Console in the form of "xxx@developer.gserviceaccount.com".
    38  	// Required.
    39  	GoogleAccessID string
    40  
    41  	// PrivateKey is the Google service account private key. It is obtainable
    42  	// from the Google Developers Console.
    43  	// At https://console.developers.google.com/project/<your-project-id>/apiui/credential,
    44  	// create a service account client ID or reuse one of your existing service account
    45  	// credentials. Click on the "Generate new P12 key" to generate and download
    46  	// a new private key. Once you download the P12 file, use the following command
    47  	// to convert it into a PEM file.
    48  	//
    49  	//    $ openssl pkcs12 -in key.p12 -passin pass:notasecret -out key.pem -nodes
    50  	//
    51  	// Provide the contents of the PEM file as a byte slice.
    52  	// Exactly one of PrivateKey or SignBytes must be non-nil.
    53  	PrivateKey []byte
    54  
    55  	// SignBytes is a function for implementing custom signing.
    56  	//
    57  	// Deprecated: Use SignRawBytes. If both SignBytes and SignRawBytes are defined,
    58  	// SignBytes will be ignored.
    59  	// This SignBytes function expects the bytes it receives to be hashed, while
    60  	// SignRawBytes accepts the raw bytes without hashing, allowing more flexibility.
    61  	// Add the following to the top of your signing function to hash the bytes
    62  	// to use SignRawBytes instead:
    63  	//		shaSum := sha256.Sum256(bytes)
    64  	//		bytes = shaSum[:]
    65  	//
    66  	SignBytes func(hashBytes []byte) (signature []byte, err error)
    67  
    68  	// SignRawBytes is a function for implementing custom signing. For example, if
    69  	// your application is running on Google App Engine, you can use
    70  	// appengine's internal signing function:
    71  	//		ctx := appengine.NewContext(request)
    72  	//     	acc, _ := appengine.ServiceAccount(ctx)
    73  	//     	&PostPolicyV4Options{
    74  	//     		GoogleAccessID: acc,
    75  	//     		SignRawBytes: func(b []byte) ([]byte, error) {
    76  	//     			_, signedBytes, err := appengine.SignBytes(ctx, b)
    77  	//     			return signedBytes, err
    78  	//     		},
    79  	//     		// etc.
    80  	//     	})
    81  	//
    82  	// SignRawBytes is equivalent to the SignBytes field on SignedURLOptions;
    83  	// that is, you may use the same signing function for the two.
    84  	//
    85  	// Exactly one of PrivateKey or SignRawBytes must be non-nil.
    86  	SignRawBytes func(bytes []byte) (signature []byte, err error)
    87  
    88  	// Expires is the expiration time on the signed post policy.
    89  	// It must be a time in the future.
    90  	// Required.
    91  	Expires time.Time
    92  
    93  	// Style provides options for the type of URL to use. Options are
    94  	// PathStyle (default), BucketBoundHostname, and VirtualHostedStyle. See
    95  	// https://cloud.google.com/storage/docs/request-endpoints for details.
    96  	// Optional.
    97  	Style URLStyle
    98  
    99  	// Insecure when set indicates that the generated URL's scheme
   100  	// will use "http" instead of "https" (default).
   101  	// Optional.
   102  	Insecure bool
   103  
   104  	// Fields specifies the attributes of a PostPolicyV4 request.
   105  	// When Fields is non-nil, its attributes must match those that will
   106  	// passed into field Conditions.
   107  	// Optional.
   108  	Fields *PolicyV4Fields
   109  
   110  	// The conditions that the uploaded file will be expected to conform to.
   111  	// When used, the failure of an upload to satisfy a condition will result in
   112  	// a 4XX status code, back with the message describing the problem.
   113  	// Optional.
   114  	Conditions []PostPolicyV4Condition
   115  
   116  	// Hostname sets the host of the signed post policy. This field overrides
   117  	// any endpoint set on a storage Client or through STORAGE_EMULATOR_HOST.
   118  	// Only compatible with PathStyle URLStyle.
   119  	// Optional.
   120  	Hostname string
   121  
   122  	shouldHashSignBytes bool
   123  }
   124  
   125  func (opts *PostPolicyV4Options) clone() *PostPolicyV4Options {
   126  	return &PostPolicyV4Options{
   127  		GoogleAccessID:      opts.GoogleAccessID,
   128  		PrivateKey:          opts.PrivateKey,
   129  		SignBytes:           opts.SignBytes,
   130  		SignRawBytes:        opts.SignRawBytes,
   131  		Expires:             opts.Expires,
   132  		Style:               opts.Style,
   133  		Insecure:            opts.Insecure,
   134  		Fields:              opts.Fields,
   135  		Conditions:          opts.Conditions,
   136  		shouldHashSignBytes: opts.shouldHashSignBytes,
   137  		Hostname:            opts.Hostname,
   138  	}
   139  }
   140  
   141  // PolicyV4Fields describes the attributes for a PostPolicyV4 request.
   142  type PolicyV4Fields struct {
   143  	// ACL specifies the access control permissions for the object.
   144  	// Optional.
   145  	ACL string
   146  	// CacheControl specifies the caching directives for the object.
   147  	// Optional.
   148  	CacheControl string
   149  	// ContentType specifies the media type of the object.
   150  	// Optional.
   151  	ContentType string
   152  	// ContentDisposition specifies how the file will be served back to requesters.
   153  	// Optional.
   154  	ContentDisposition string
   155  	// ContentEncoding specifies the decompressive transcoding that the object.
   156  	// This field is complementary to ContentType in that the file could be
   157  	// compressed but ContentType specifies the file's original media type.
   158  	// Optional.
   159  	ContentEncoding string
   160  	// Metadata specifies custom metadata for the object.
   161  	// If any key doesn't begin with "x-goog-meta-", an error will be returned.
   162  	// Optional.
   163  	Metadata map[string]string
   164  	// StatusCodeOnSuccess when set, specifies the status code that Cloud Storage
   165  	// will serve back on successful upload of the object.
   166  	// Optional.
   167  	StatusCodeOnSuccess int
   168  	// RedirectToURLOnSuccess when set, specifies the URL that Cloud Storage
   169  	// will serve back on successful upload of the object.
   170  	// Optional.
   171  	RedirectToURLOnSuccess string
   172  }
   173  
   174  // PostPolicyV4 describes the URL and respective form fields for a generated PostPolicyV4 request.
   175  type PostPolicyV4 struct {
   176  	// URL is the generated URL that the file upload will be made to.
   177  	URL string
   178  	// Fields specifies the generated key-values that the file uploader
   179  	// must include in their multipart upload form.
   180  	Fields map[string]string
   181  }
   182  
   183  // PostPolicyV4Condition describes the constraints that the subsequent
   184  // object upload's multipart form fields will be expected to conform to.
   185  type PostPolicyV4Condition interface {
   186  	isEmpty() bool
   187  	json.Marshaler
   188  }
   189  
   190  type startsWith struct {
   191  	key, value string
   192  }
   193  
   194  func (sw *startsWith) MarshalJSON() ([]byte, error) {
   195  	return json.Marshal([]string{"starts-with", sw.key, sw.value})
   196  }
   197  func (sw *startsWith) isEmpty() bool {
   198  	return sw.value == ""
   199  }
   200  
   201  // ConditionStartsWith checks that an attributes starts with value.
   202  // An empty value will cause this condition to be ignored.
   203  func ConditionStartsWith(key, value string) PostPolicyV4Condition {
   204  	return &startsWith{key, value}
   205  }
   206  
   207  type contentLengthRangeCondition struct {
   208  	start, end uint64
   209  }
   210  
   211  func (clr *contentLengthRangeCondition) MarshalJSON() ([]byte, error) {
   212  	return json.Marshal([]interface{}{"content-length-range", clr.start, clr.end})
   213  }
   214  func (clr *contentLengthRangeCondition) isEmpty() bool {
   215  	return clr.start == 0 && clr.end == 0
   216  }
   217  
   218  type singleValueCondition struct {
   219  	name, value string
   220  }
   221  
   222  func (svc *singleValueCondition) MarshalJSON() ([]byte, error) {
   223  	return json.Marshal(map[string]string{svc.name: svc.value})
   224  }
   225  func (svc *singleValueCondition) isEmpty() bool {
   226  	return svc.value == ""
   227  }
   228  
   229  // ConditionContentLengthRange constraints the limits that the
   230  // multipart upload's range header will be expected to be within.
   231  func ConditionContentLengthRange(start, end uint64) PostPolicyV4Condition {
   232  	return &contentLengthRangeCondition{start, end}
   233  }
   234  
   235  func conditionRedirectToURLOnSuccess(redirectURL string) PostPolicyV4Condition {
   236  	return &singleValueCondition{"success_action_redirect", redirectURL}
   237  }
   238  
   239  func conditionStatusCodeOnSuccess(statusCode int) PostPolicyV4Condition {
   240  	svc := &singleValueCondition{name: "success_action_status"}
   241  	if statusCode > 0 {
   242  		svc.value = fmt.Sprintf("%d", statusCode)
   243  	}
   244  	return svc
   245  }
   246  
   247  // GenerateSignedPostPolicyV4 generates a PostPolicyV4 value from bucket, object and opts.
   248  // The generated URL and fields will then allow an unauthenticated client to perform multipart uploads.
   249  // If initializing a Storage Client, instead use the Bucket.GenerateSignedPostPolicyV4
   250  // method which uses the Client's credentials to handle authentication.
   251  func GenerateSignedPostPolicyV4(bucket, object string, opts *PostPolicyV4Options) (*PostPolicyV4, error) {
   252  	if bucket == "" {
   253  		return nil, errors.New("storage: bucket must be non-empty")
   254  	}
   255  	if object == "" {
   256  		return nil, errors.New("storage: object must be non-empty")
   257  	}
   258  	now := utcNow()
   259  	if err := validatePostPolicyV4Options(opts, now); err != nil {
   260  		return nil, err
   261  	}
   262  
   263  	var signingFn func(hashedBytes []byte) ([]byte, error)
   264  	switch {
   265  	case opts.SignRawBytes != nil:
   266  		signingFn = opts.SignRawBytes
   267  	case opts.shouldHashSignBytes:
   268  		signingFn = opts.SignBytes
   269  	case len(opts.PrivateKey) != 0:
   270  		parsedRSAPrivKey, err := parseKey(opts.PrivateKey)
   271  		if err != nil {
   272  			return nil, err
   273  		}
   274  		signingFn = func(b []byte) ([]byte, error) {
   275  			sum := sha256.Sum256(b)
   276  			return rsa.SignPKCS1v15(rand.Reader, parsedRSAPrivKey, crypto.SHA256, sum[:])
   277  		}
   278  
   279  	default:
   280  		return nil, errors.New("storage: exactly one of PrivateKey or SignRawBytes must be set")
   281  	}
   282  
   283  	var descFields PolicyV4Fields
   284  	if opts.Fields != nil {
   285  		descFields = *opts.Fields
   286  	}
   287  
   288  	if err := validateMetadata(descFields.Metadata); err != nil {
   289  		return nil, err
   290  	}
   291  
   292  	// Build the policy.
   293  	conds := make([]PostPolicyV4Condition, len(opts.Conditions))
   294  	copy(conds, opts.Conditions)
   295  	conds = append(conds,
   296  		// These are ordered lexicographically. Technically the order doesn't matter
   297  		// for creating the policy, but we use this order to match the
   298  		// cross-language conformance tests for this feature.
   299  		&singleValueCondition{"acl", descFields.ACL},
   300  		&singleValueCondition{"cache-control", descFields.CacheControl},
   301  		&singleValueCondition{"content-disposition", descFields.ContentDisposition},
   302  		&singleValueCondition{"content-encoding", descFields.ContentEncoding},
   303  		&singleValueCondition{"content-type", descFields.ContentType},
   304  		conditionRedirectToURLOnSuccess(descFields.RedirectToURLOnSuccess),
   305  		conditionStatusCodeOnSuccess(descFields.StatusCodeOnSuccess),
   306  	)
   307  
   308  	YYYYMMDD := now.Format(yearMonthDay)
   309  	policyFields := map[string]string{
   310  		"key":                     object,
   311  		"x-goog-date":             now.Format(iso8601),
   312  		"x-goog-credential":       opts.GoogleAccessID + "/" + YYYYMMDD + "/auto/storage/goog4_request",
   313  		"x-goog-algorithm":        "GOOG4-RSA-SHA256",
   314  		"acl":                     descFields.ACL,
   315  		"cache-control":           descFields.CacheControl,
   316  		"content-disposition":     descFields.ContentDisposition,
   317  		"content-encoding":        descFields.ContentEncoding,
   318  		"content-type":            descFields.ContentType,
   319  		"success_action_redirect": descFields.RedirectToURLOnSuccess,
   320  	}
   321  	for key, value := range descFields.Metadata {
   322  		conds = append(conds, &singleValueCondition{key, value})
   323  		policyFields[key] = value
   324  	}
   325  
   326  	// Following from the order expected by the conformance test cases,
   327  	// hence manually inserting these fields in a specific order.
   328  	conds = append(conds,
   329  		&singleValueCondition{"bucket", bucket},
   330  		&singleValueCondition{"key", object},
   331  		&singleValueCondition{"x-goog-date", now.Format(iso8601)},
   332  		&singleValueCondition{
   333  			name:  "x-goog-credential",
   334  			value: opts.GoogleAccessID + "/" + YYYYMMDD + "/auto/storage/goog4_request",
   335  		},
   336  		&singleValueCondition{"x-goog-algorithm", "GOOG4-RSA-SHA256"},
   337  	)
   338  
   339  	nonEmptyConds := make([]PostPolicyV4Condition, 0, len(opts.Conditions))
   340  	for _, cond := range conds {
   341  		if cond == nil || !cond.isEmpty() {
   342  			nonEmptyConds = append(nonEmptyConds, cond)
   343  		}
   344  	}
   345  	condsAsJSON, err := json.Marshal(map[string]interface{}{
   346  		"conditions": nonEmptyConds,
   347  		"expiration": opts.Expires.Format(time.RFC3339),
   348  	})
   349  	if err != nil {
   350  		return nil, fmt.Errorf("storage: PostPolicyV4 JSON serialization failed: %w", err)
   351  	}
   352  
   353  	b64Policy := base64.StdEncoding.EncodeToString(condsAsJSON)
   354  	var signature []byte
   355  	var signErr error
   356  
   357  	if opts.shouldHashSignBytes {
   358  		// SignBytes expects hashed bytes as input instead of raw bytes, so we hash them
   359  		shaSum := sha256.Sum256([]byte(b64Policy))
   360  		signature, signErr = signingFn(shaSum[:])
   361  	} else {
   362  		signature, signErr = signingFn([]byte(b64Policy))
   363  	}
   364  	if signErr != nil {
   365  		return nil, signErr
   366  	}
   367  
   368  	policyFields["policy"] = b64Policy
   369  	policyFields["x-goog-signature"] = fmt.Sprintf("%x", signature)
   370  
   371  	// Construct the URL.
   372  	scheme := "https"
   373  	if opts.Insecure {
   374  		scheme = "http"
   375  	}
   376  	path := opts.Style.path(bucket, "") + "/"
   377  	u := &url.URL{
   378  		Path:    path,
   379  		RawPath: pathEncodeV4(path),
   380  		Host:    opts.Style.host(opts.Hostname, bucket),
   381  		Scheme:  scheme,
   382  	}
   383  
   384  	if descFields.StatusCodeOnSuccess > 0 {
   385  		policyFields["success_action_status"] = fmt.Sprintf("%d", descFields.StatusCodeOnSuccess)
   386  	}
   387  
   388  	// Clear out fields with blanks values.
   389  	for key, value := range policyFields {
   390  		if value == "" {
   391  			delete(policyFields, key)
   392  		}
   393  	}
   394  	pp4 := &PostPolicyV4{
   395  		Fields: policyFields,
   396  		URL:    u.String(),
   397  	}
   398  	return pp4, nil
   399  }
   400  
   401  // validatePostPolicyV4Options checks that:
   402  // * GoogleAccessID is set
   403  // * either PrivateKey or SignRawBytes/SignBytes is set, but not both
   404  // * the deadline set in Expires is not in the past
   405  // * if Style is not set, it'll use PathStyle
   406  // * sets shouldHashSignBytes to true if opts.SignBytes should be used
   407  func validatePostPolicyV4Options(opts *PostPolicyV4Options, now time.Time) error {
   408  	if opts == nil || opts.GoogleAccessID == "" {
   409  		return errors.New("storage: missing required GoogleAccessID")
   410  	}
   411  	if privBlank, signBlank := len(opts.PrivateKey) == 0, opts.SignBytes == nil && opts.SignRawBytes == nil; privBlank == signBlank {
   412  		return errors.New("storage: exactly one of PrivateKey or SignRawBytes must be set")
   413  	}
   414  	if opts.Expires.Before(now) {
   415  		return errors.New("storage: expecting Expires to be in the future")
   416  	}
   417  	if opts.Style == nil {
   418  		opts.Style = PathStyle()
   419  	}
   420  	if opts.SignRawBytes == nil && opts.SignBytes != nil {
   421  		opts.shouldHashSignBytes = true
   422  	}
   423  	return nil
   424  }
   425  
   426  // validateMetadata ensures that all keys passed in have a prefix of "x-goog-meta-",
   427  // otherwise it will return an error.
   428  func validateMetadata(hdrs map[string]string) (err error) {
   429  	if len(hdrs) == 0 {
   430  		return nil
   431  	}
   432  
   433  	badKeys := make([]string, 0, len(hdrs))
   434  	for key := range hdrs {
   435  		if !strings.HasPrefix(key, "x-goog-meta-") {
   436  			badKeys = append(badKeys, key)
   437  		}
   438  	}
   439  	if len(badKeys) != 0 {
   440  		err = errors.New("storage: expected metadata to begin with x-goog-meta-, got " + strings.Join(badKeys, ", "))
   441  	}
   442  	return
   443  }
   444  

View as plain text