presign_post.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419
  1. package s3
  2. import (
  3. "context"
  4. "crypto/hmac"
  5. "crypto/sha256"
  6. "encoding/base64"
  7. "encoding/hex"
  8. "encoding/json"
  9. "fmt"
  10. "strings"
  11. "time"
  12. "github.com/aws/aws-sdk-go-v2/aws"
  13. awsmiddleware "github.com/aws/aws-sdk-go-v2/aws/middleware"
  14. "github.com/aws/aws-sdk-go-v2/aws/retry"
  15. v4 "github.com/aws/aws-sdk-go-v2/aws/signer/v4"
  16. awshttp "github.com/aws/aws-sdk-go-v2/aws/transport/http"
  17. internalcontext "github.com/aws/aws-sdk-go-v2/internal/context"
  18. "github.com/aws/aws-sdk-go-v2/internal/sdk"
  19. acceptencodingcust "github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding"
  20. presignedurlcust "github.com/aws/aws-sdk-go-v2/service/internal/presigned-url"
  21. "github.com/aws/smithy-go/middleware"
  22. smithyhttp "github.com/aws/smithy-go/transport/http"
  23. )
  24. const (
  25. algorithmHeader = "X-Amz-Algorithm"
  26. credentialHeader = "X-Amz-Credential"
  27. dateHeader = "X-Amz-Date"
  28. tokenHeader = "X-Amz-Security-Token"
  29. signatureHeader = "X-Amz-Signature"
  30. algorithm = "AWS4-HMAC-SHA256"
  31. aws4Request = "aws4_request"
  32. bucketHeader = "bucket"
  33. defaultExpiresIn = 15 * time.Minute
  34. shortDateLayout = "20060102"
  35. )
  36. // PresignPostObject is a special kind of [presigned request] used to send a request using
  37. // form data, likely from an HTML form on a browser.
  38. // Unlike other presigned operations, the return values of this function are not meant to be used directly
  39. // to make an HTTP request but rather to be used as inputs to a form. See [the docs] for more information
  40. // on how to use these values
  41. //
  42. // [presigned request] https://docs.aws.amazon.com/AmazonS3/latest/userguide/ShareObjectPreSignedURL.html
  43. // [the docs] https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html
  44. func (c *PresignClient) PresignPostObject(ctx context.Context, params *PutObjectInput, optFns ...func(*PresignPostOptions)) (*PresignedPostRequest, error) {
  45. if params == nil {
  46. params = &PutObjectInput{}
  47. }
  48. clientOptions := c.options.copy()
  49. options := PresignPostOptions{
  50. Expires: clientOptions.Expires,
  51. PostPresigner: &postSignAdapter{},
  52. }
  53. for _, fn := range optFns {
  54. fn(&options)
  55. }
  56. clientOptFns := append(clientOptions.ClientOptions, withNopHTTPClientAPIOption)
  57. cvt := presignPostConverter(options)
  58. result, _, err := c.client.invokeOperation(ctx, "$type:L", params, clientOptFns,
  59. c.client.addOperationPutObjectMiddlewares,
  60. cvt.ConvertToPresignMiddleware,
  61. func(stack *middleware.Stack, options Options) error {
  62. return awshttp.RemoveContentTypeHeader(stack)
  63. },
  64. )
  65. if err != nil {
  66. return nil, err
  67. }
  68. out := result.(*PresignedPostRequest)
  69. return out, nil
  70. }
  71. // PresignedPostRequest represents a presigned request to be sent using HTTP verb POST and FormData
  72. type PresignedPostRequest struct {
  73. // Represents the Base URL to make a request to
  74. URL string
  75. // Values is a key-value map of values to be sent as FormData
  76. // these values are not encoded
  77. Values map[string]string
  78. }
  79. // postSignAdapter adapter to implement the presignPost interface
  80. type postSignAdapter struct{}
  81. // PresignPost creates a special kind of [presigned request]
  82. // to be used with HTTP verb POST.
  83. // It differs from PUT request mostly on
  84. // 1. It accepts a new set of parameters, `Conditions[]`, that are used to create a policy doc to limit where an object can be posted to
  85. // 2. The return value needs to have more processing since it's meant to be sent via a form and not stand on its own
  86. // 3. There's no body to be signed, since that will be attached when the actual request is made
  87. // 4. The signature is made based on the policy document, not the whole request
  88. // More information can be found at https://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectPOST.html
  89. //
  90. // [presigned request] https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html
  91. func (s *postSignAdapter) PresignPost(
  92. credentials aws.Credentials,
  93. bucket string, key string,
  94. region string, service string, signingTime time.Time, conditions []interface{}, expirationTime time.Time, optFns ...func(*v4.SignerOptions),
  95. ) (fields map[string]string, err error) {
  96. credentialScope := buildCredentialScope(signingTime, region, service)
  97. credentialStr := credentials.AccessKeyID + "/" + credentialScope
  98. policyDoc, err := createPolicyDocument(expirationTime, signingTime, bucket, key, credentialStr, &credentials.SessionToken, conditions)
  99. if err != nil {
  100. return nil, err
  101. }
  102. signature := buildSignature(policyDoc, credentials.SecretAccessKey, service, region, signingTime)
  103. fields = getPostSignRequiredFields(signingTime, credentialStr, credentials)
  104. fields[signatureHeader] = signature
  105. fields["key"] = key
  106. fields["policy"] = policyDoc
  107. return fields, nil
  108. }
  109. func getPostSignRequiredFields(t time.Time, credentialStr string, awsCredentials aws.Credentials) map[string]string {
  110. fields := map[string]string{
  111. algorithmHeader: algorithm,
  112. dateHeader: t.UTC().Format("20060102T150405Z"),
  113. credentialHeader: credentialStr,
  114. }
  115. sessionToken := awsCredentials.SessionToken
  116. if len(sessionToken) > 0 {
  117. fields[tokenHeader] = sessionToken
  118. }
  119. return fields
  120. }
  121. // PresignPost defines the interface to presign a POST request
  122. type PresignPost interface {
  123. PresignPost(
  124. credentials aws.Credentials,
  125. bucket string, key string,
  126. region string, service string, signingTime time.Time, conditions []interface{}, expirationTime time.Time,
  127. optFns ...func(*v4.SignerOptions),
  128. ) (fields map[string]string, err error)
  129. }
  130. // PresignPostOptions represent the options to be passed to a PresignPost sign request
  131. type PresignPostOptions struct {
  132. // ClientOptions are list of functional options to mutate client options used by
  133. // the presign client.
  134. ClientOptions []func(*Options)
  135. // PostPresigner to use. One will be created if none is provided
  136. PostPresigner PresignPost
  137. // Expires sets the expiration duration for the generated presign url. This should
  138. // be the duration in seconds the presigned URL should be considered valid for. If
  139. // not set or set to zero, presign url would default to expire after 900 seconds.
  140. Expires time.Duration
  141. // Conditions a list of extra conditions to pass to the policy document
  142. // Available conditions can be found [here]
  143. //
  144. // [here]https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-HTTPPOSTConstructPolicy.html#sigv4-PolicyConditions
  145. Conditions []interface{}
  146. }
  147. type presignPostConverter PresignPostOptions
  148. // presignPostRequestMiddlewareOptions is the options for the presignPostRequestMiddleware middleware.
  149. type presignPostRequestMiddlewareOptions struct {
  150. CredentialsProvider aws.CredentialsProvider
  151. Presigner PresignPost
  152. LogSigning bool
  153. ExpiresIn time.Duration
  154. Conditions []interface{}
  155. }
  156. type presignPostRequestMiddleware struct {
  157. credentialsProvider aws.CredentialsProvider
  158. presigner PresignPost
  159. logSigning bool
  160. expiresIn time.Duration
  161. conditions []interface{}
  162. }
  163. // newPresignPostRequestMiddleware returns a new presignPostRequestMiddleware
  164. // initialized with the presigner.
  165. func newPresignPostRequestMiddleware(options presignPostRequestMiddlewareOptions) *presignPostRequestMiddleware {
  166. return &presignPostRequestMiddleware{
  167. credentialsProvider: options.CredentialsProvider,
  168. presigner: options.Presigner,
  169. logSigning: options.LogSigning,
  170. expiresIn: options.ExpiresIn,
  171. conditions: options.Conditions,
  172. }
  173. }
  174. // ID provides the middleware ID.
  175. func (*presignPostRequestMiddleware) ID() string { return "PresignPostRequestMiddleware" }
  176. // HandleFinalize will take the provided input and create a presigned url for
  177. // the http request using the SigV4 presign authentication scheme.
  178. //
  179. // Since the signed request is not a valid HTTP request
  180. func (s *presignPostRequestMiddleware) HandleFinalize(
  181. ctx context.Context, in middleware.FinalizeInput, next middleware.FinalizeHandler,
  182. ) (
  183. out middleware.FinalizeOutput, metadata middleware.Metadata, err error,
  184. ) {
  185. input := getOperationInput(ctx)
  186. asS3Put, ok := input.(*PutObjectInput)
  187. if !ok {
  188. return out, metadata, fmt.Errorf("expected PutObjectInput")
  189. }
  190. bucketName, ok := asS3Put.bucket()
  191. if !ok {
  192. return out, metadata, fmt.Errorf("requested input bucketName not found on request")
  193. }
  194. uploadKey := asS3Put.Key
  195. if uploadKey == nil {
  196. return out, metadata, fmt.Errorf("PutObject input does not have a key input")
  197. }
  198. uri := getS3ResolvedURI(ctx)
  199. signingName := awsmiddleware.GetSigningName(ctx)
  200. signingRegion := awsmiddleware.GetSigningRegion(ctx)
  201. credentials, err := s.credentialsProvider.Retrieve(ctx)
  202. if err != nil {
  203. return out, metadata, &v4.SigningError{
  204. Err: fmt.Errorf("failed to retrieve credentials: %w", err),
  205. }
  206. }
  207. skew := internalcontext.GetAttemptSkewContext(ctx)
  208. signingTime := sdk.NowTime().Add(skew)
  209. expirationTime := signingTime.Add(s.expiresIn).UTC()
  210. fields, err := s.presigner.PresignPost(
  211. credentials,
  212. bucketName,
  213. *uploadKey,
  214. signingRegion,
  215. signingName,
  216. signingTime,
  217. s.conditions,
  218. expirationTime,
  219. func(o *v4.SignerOptions) {
  220. o.Logger = middleware.GetLogger(ctx)
  221. o.LogSigning = s.logSigning
  222. })
  223. if err != nil {
  224. return out, metadata, &v4.SigningError{
  225. Err: fmt.Errorf("failed to sign http request, %w", err),
  226. }
  227. }
  228. out.Result = &PresignedPostRequest{
  229. URL: uri,
  230. Values: fields,
  231. }
  232. return out, metadata, nil
  233. }
  234. // Adapted from existing PresignConverter middleware
  235. func (c presignPostConverter) ConvertToPresignMiddleware(stack *middleware.Stack, options Options) (err error) {
  236. stack.Build.Remove("UserAgent")
  237. stack.Finalize.Remove((*acceptencodingcust.DisableGzip)(nil).ID())
  238. stack.Finalize.Remove((*retry.Attempt)(nil).ID())
  239. stack.Finalize.Remove((*retry.MetricsHeader)(nil).ID())
  240. stack.Deserialize.Clear()
  241. if err := stack.Finalize.Insert(&presignContextPolyfillMiddleware{}, "Signing", middleware.Before); err != nil {
  242. return err
  243. }
  244. // if no expiration is set, set one
  245. expiresIn := c.Expires
  246. if expiresIn == 0 {
  247. expiresIn = defaultExpiresIn
  248. }
  249. pmw := newPresignPostRequestMiddleware(presignPostRequestMiddlewareOptions{
  250. CredentialsProvider: options.Credentials,
  251. Presigner: c.PostPresigner,
  252. LogSigning: options.ClientLogMode.IsSigning(),
  253. ExpiresIn: expiresIn,
  254. Conditions: c.Conditions,
  255. })
  256. if _, err := stack.Finalize.Swap("Signing", pmw); err != nil {
  257. return err
  258. }
  259. if err = smithyhttp.AddNoPayloadDefaultContentTypeRemover(stack); err != nil {
  260. return err
  261. }
  262. err = presignedurlcust.AddAsIsPresigningMiddleware(stack)
  263. if err != nil {
  264. return err
  265. }
  266. return nil
  267. }
  268. func createPolicyDocument(expirationTime time.Time, signingTime time.Time, bucket string, key string, credentialString string, securityToken *string, extraConditions []interface{}) (string, error) {
  269. initialConditions := []interface{}{
  270. map[string]string{
  271. algorithmHeader: algorithm,
  272. },
  273. map[string]string{
  274. bucketHeader: bucket,
  275. },
  276. map[string]string{
  277. credentialHeader: credentialString,
  278. },
  279. map[string]string{
  280. dateHeader: signingTime.UTC().Format("20060102T150405Z"),
  281. },
  282. }
  283. var conditions []interface{}
  284. for _, v := range initialConditions {
  285. conditions = append(conditions, v)
  286. }
  287. if securityToken != nil && *securityToken != "" {
  288. conditions = append(conditions, map[string]string{
  289. tokenHeader: *securityToken,
  290. })
  291. }
  292. // append user-defined conditions at the end
  293. conditions = append(conditions, extraConditions...)
  294. // The policy allows you to set a "key" value to specify what's the name of the
  295. // key to add. Customers can add one by specifying one in their conditions,
  296. // so we're checking if one has already been set.
  297. // If none is found, restrict this to just the key name passed on the request
  298. // This can be disabled by adding a condition that explicitly allows
  299. // everything
  300. if !isAlreadyCheckingForKey(conditions) {
  301. conditions = append(conditions, map[string]string{"key": key})
  302. }
  303. policyDoc := map[string]interface{}{
  304. "conditions": conditions,
  305. "expiration": expirationTime.Format(time.RFC3339),
  306. }
  307. jsonBytes, err := json.Marshal(policyDoc)
  308. if err != nil {
  309. return "", err
  310. }
  311. return base64.StdEncoding.EncodeToString(jsonBytes), nil
  312. }
  313. func isAlreadyCheckingForKey(conditions []interface{}) bool {
  314. // Need to check for two conditions:
  315. // 1. A condition of the form ["starts-with", "$key", "mykey"]
  316. // 2. A condition of the form {"key": "mykey"}
  317. for _, c := range conditions {
  318. slice, ok := c.([]interface{})
  319. if ok && len(slice) > 1 {
  320. if slice[0] == "starts-with" && slice[1] == "$key" {
  321. return true
  322. }
  323. }
  324. m, ok := c.(map[string]interface{})
  325. if ok && len(m) > 0 {
  326. for k := range m {
  327. if k == "key" {
  328. return true
  329. }
  330. }
  331. }
  332. // Repeat this but for map[string]string due to type constrains
  333. ms, ok := c.(map[string]string)
  334. if ok && len(ms) > 0 {
  335. for k := range ms {
  336. if k == "key" {
  337. return true
  338. }
  339. }
  340. }
  341. }
  342. return false
  343. }
  344. // these methods have been copied from v4 implementation since they are not exported for public use
  345. func hmacsha256(key []byte, data []byte) []byte {
  346. hash := hmac.New(sha256.New, key)
  347. hash.Write(data)
  348. return hash.Sum(nil)
  349. }
  350. func buildSignature(strToSign, secret, service, region string, t time.Time) string {
  351. key := deriveKey(secret, service, region, t)
  352. return hex.EncodeToString(hmacsha256(key, []byte(strToSign)))
  353. }
  354. func deriveKey(secret, service, region string, t time.Time) []byte {
  355. hmacDate := hmacsha256([]byte("AWS4"+secret), []byte(t.UTC().Format(shortDateLayout)))
  356. hmacRegion := hmacsha256(hmacDate, []byte(region))
  357. hmacService := hmacsha256(hmacRegion, []byte(service))
  358. return hmacsha256(hmacService, []byte(aws4Request))
  359. }
  360. func buildCredentialScope(signingTime time.Time, region, service string) string {
  361. return strings.Join([]string{
  362. signingTime.UTC().Format(shortDateLayout),
  363. region,
  364. service,
  365. aws4Request,
  366. }, "/")
  367. }