post_policy_v4.go 15 KB

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