aws-sigv4 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/aws-sigv4.rb +4 -0
- data/lib/aws-sigv4/credentials.rb +59 -0
- data/lib/aws-sigv4/errors.rb +25 -0
- data/lib/aws-sigv4/request.rb +63 -0
- data/lib/aws-sigv4/signature.rb +35 -0
- data/lib/aws-sigv4/signer.rb +596 -0
- metadata +50 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: dc5dab371e96d81659b31a8f240fac3f0181e1b1
|
4
|
+
data.tar.gz: fa9c9cce2b176379585923c0345890756f930e78
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: cdad04342ef2a0e2e8d1016af4d7e50daf13bc892981f7629c7fb444bf86c599ad6c9657e377176c66ebddaea8a73b211c13e05c63ae38da9cf7f4cec86d70c7
|
7
|
+
data.tar.gz: af5088ab2930b07050ef3ab12c7de87671f54f3d8bcecc38a1fdde49e1df69f43cb11a80c402caf7257f5156b2eec935d35bcab427578f979ee8e6185d5bb1c0
|
data/lib/aws-sigv4.rb
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
module Aws
|
2
|
+
module Sigv4
|
3
|
+
# Users that wish to configure static credentials can use the
|
4
|
+
# `:access_key_id` and `:secret_access_key` constructor options.
|
5
|
+
# @api private
|
6
|
+
class Credentials
|
7
|
+
|
8
|
+
# @option options [required, String] :access_key_id
|
9
|
+
# @option options [required, String] :secret_access_key
|
10
|
+
# @option options [String, nil] :session_token (nil)
|
11
|
+
def initialize(options = {})
|
12
|
+
if options[:access_key_id] && options[:secret_access_key]
|
13
|
+
@access_key_id = options[:access_key_id]
|
14
|
+
@secret_access_key = options[:secret_access_key]
|
15
|
+
@session_token = options[:session_token]
|
16
|
+
else
|
17
|
+
msg = "expected both :access_key_id and :secret_access_key options"
|
18
|
+
raise ArgumentError, msg
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# @return [String]
|
23
|
+
attr_reader :access_key_id
|
24
|
+
|
25
|
+
# @return [String]
|
26
|
+
attr_reader :secret_access_key
|
27
|
+
|
28
|
+
# @return [String, nil]
|
29
|
+
attr_reader :session_token
|
30
|
+
|
31
|
+
# @return [Boolean]
|
32
|
+
def set?
|
33
|
+
!!(access_key_id && secret_access_key)
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
# Users that wish to configure static credentials can use the
|
39
|
+
# `:access_key_id` and `:secret_access_key` constructor options.
|
40
|
+
# @api private
|
41
|
+
class StaticCredentialsProvider
|
42
|
+
|
43
|
+
# @option options [Credentials] :credentials
|
44
|
+
# @option options [String] :access_key_id
|
45
|
+
# @option options [String] :secret_access_key
|
46
|
+
# @option options [String] :session_token (nil)
|
47
|
+
def initialize(options = {})
|
48
|
+
@credentials = options[:credentials] ?
|
49
|
+
options[:credentials] :
|
50
|
+
Credentials.new(options)
|
51
|
+
end
|
52
|
+
|
53
|
+
# @return [Credentials]
|
54
|
+
attr_reader :credentials
|
55
|
+
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Aws
|
2
|
+
module Sigv4
|
3
|
+
module Errors
|
4
|
+
|
5
|
+
class MissingCredentialsError < ArgumentError
|
6
|
+
def initialize(msg = nil)
|
7
|
+
super(msg || <<-MSG.strip)
|
8
|
+
missing credentials, provide credentials with one of the following options:
|
9
|
+
- :access_key_id and :secret_access_key
|
10
|
+
- :credentials
|
11
|
+
- :credentials_provider
|
12
|
+
MSG
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class MissingRegionError < ArgumentError
|
17
|
+
def initialize(*args)
|
18
|
+
super("missing required option :region")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Aws
|
4
|
+
module Sigv4
|
5
|
+
class Request
|
6
|
+
|
7
|
+
# @option options [required, String] :http_method
|
8
|
+
# @option options [required, HTTP::URI, HTTPS::URI, String] :endpoint
|
9
|
+
# @option options [Hash<String,String>] :headers ({})
|
10
|
+
# @option options [String, IO] :body ('')
|
11
|
+
def initialize(options = {})
|
12
|
+
@http_method = nil
|
13
|
+
@endpoint = nil
|
14
|
+
@headers = {}
|
15
|
+
@body = ''
|
16
|
+
options.each_pair do |attr_name, attr_value|
|
17
|
+
send("#{attr_name}=", attr_value)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# @param [String] http_method One of 'GET', 'PUT', 'POST', 'DELETE', 'HEAD', or 'PATCH'
|
22
|
+
def http_method=(http_method)
|
23
|
+
@http_method = http_method
|
24
|
+
end
|
25
|
+
|
26
|
+
# @return [String] One of 'GET', 'PUT', 'POST', 'DELETE', 'HEAD', or 'PATCH'
|
27
|
+
def http_method
|
28
|
+
@http_method
|
29
|
+
end
|
30
|
+
|
31
|
+
# @param [String, HTTP::URI, HTTPS::URI] endpoint
|
32
|
+
def endpoint=(endpoint)
|
33
|
+
@endpoint = URI.parse(endpoint.to_s)
|
34
|
+
end
|
35
|
+
|
36
|
+
# @return [HTTP::URI, HTTPS::URI]
|
37
|
+
def endpoint
|
38
|
+
@endpoint
|
39
|
+
end
|
40
|
+
|
41
|
+
# @param [Hash] headers
|
42
|
+
def headers=(headers)
|
43
|
+
@headers = headers
|
44
|
+
end
|
45
|
+
|
46
|
+
# @return [Hash<String,String>]
|
47
|
+
def headers
|
48
|
+
@headers
|
49
|
+
end
|
50
|
+
|
51
|
+
# @param [String, IO] body
|
52
|
+
def body=(body)
|
53
|
+
@body = body
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [String, IO]
|
57
|
+
def body
|
58
|
+
@body
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Aws
|
2
|
+
module Sigv4
|
3
|
+
class Signature
|
4
|
+
|
5
|
+
# @api private
|
6
|
+
def initialize(options)
|
7
|
+
options.each_pair do |attr_name, attr_value|
|
8
|
+
send("#{attr_name}=", attr_value)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# @return [Hash<String,String>] A hash of headers that should
|
13
|
+
# be applied to the HTTP request. Header keys are lower
|
14
|
+
# cased strings and may include the following:
|
15
|
+
#
|
16
|
+
# * 'host'
|
17
|
+
# * 'x-amz-date'
|
18
|
+
# * 'x-amz-security-token'
|
19
|
+
# * 'x-amz-content-sha256'
|
20
|
+
# * 'authorization'
|
21
|
+
#
|
22
|
+
attr_accessor :headers
|
23
|
+
|
24
|
+
# @return [String] For debugging purposes.
|
25
|
+
attr_accessor :canonical_request
|
26
|
+
|
27
|
+
# @return [String] For debugging purposes.
|
28
|
+
attr_accessor :string_to_sign
|
29
|
+
|
30
|
+
# @return [String] For debugging purposes.
|
31
|
+
attr_accessor :content_sha256
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,596 @@
|
|
1
|
+
require 'openssl'
|
2
|
+
require 'tempfile'
|
3
|
+
require 'time'
|
4
|
+
require 'uri'
|
5
|
+
require 'set'
|
6
|
+
require 'cgi'
|
7
|
+
|
8
|
+
module Aws
|
9
|
+
module Sigv4
|
10
|
+
|
11
|
+
# Utility class for creating AWS signature version 4 signature. This class
|
12
|
+
# provides two methods for generating signatures:
|
13
|
+
#
|
14
|
+
# * {#sign_request} - Computes a signature of the given request, returning
|
15
|
+
# the hash of headers that should be applied to the request.
|
16
|
+
#
|
17
|
+
# * {#presign_url} - Computes a presigned request with an expiration.
|
18
|
+
# By default, the body of this request is not signed and the request
|
19
|
+
# expires in 15 minutes.
|
20
|
+
#
|
21
|
+
# ## Configuration
|
22
|
+
#
|
23
|
+
# To use the signer, you need to specify the service, region, and credentials.
|
24
|
+
# The service name is normally the endpoint prefix to an AWS service. For
|
25
|
+
# example:
|
26
|
+
#
|
27
|
+
# ec2.us-west-1.amazonaws.com => ec2
|
28
|
+
#
|
29
|
+
# The region is normally the second portion of the endpoint, following
|
30
|
+
# the service name.
|
31
|
+
#
|
32
|
+
# ec2.us-west-1.amazonaws.com => us-west-1
|
33
|
+
#
|
34
|
+
# It is important to have the correct service and region name, or the
|
35
|
+
# signature will be invalid.
|
36
|
+
#
|
37
|
+
# ## Credentials
|
38
|
+
#
|
39
|
+
# The signer requires credentials. You can configure the signer
|
40
|
+
# with static credentials:
|
41
|
+
#
|
42
|
+
# signer = Aws::Sigv4::Signer.new(
|
43
|
+
# service: 's3',
|
44
|
+
# region: 'us-east-1',
|
45
|
+
# # static credentials
|
46
|
+
# access_key_id: 'akid',
|
47
|
+
# secret_access_key: 'secret'
|
48
|
+
# )
|
49
|
+
#
|
50
|
+
# You can also provide refreshing credentials via the `:credentials_provider`.
|
51
|
+
# If you are using the AWS SDK for Ruby, you can use any of the credential
|
52
|
+
# classes:
|
53
|
+
#
|
54
|
+
# signer = Aws::Sigv4::Signer.new(
|
55
|
+
# service: 's3',
|
56
|
+
# region: 'us-east-1',
|
57
|
+
# credentials_provider: Aws::InstanceProfileCredentials.new
|
58
|
+
# )
|
59
|
+
#
|
60
|
+
# Other AWS SDK for Ruby classes that can be provided via `:credentials_provider`:
|
61
|
+
#
|
62
|
+
# * `Aws::Credentials`
|
63
|
+
# * `Aws::SharedCredentials`
|
64
|
+
# * `Aws::InstanceProfileCredentials`
|
65
|
+
# * `Aws::AssumeRoleCredentials`
|
66
|
+
# * `Aws::ECSCredentials`
|
67
|
+
#
|
68
|
+
# A credential provider is any object that responds to `#credentials`
|
69
|
+
# returning another object that responds to `#access_key_id`, `#secret_access_key`,
|
70
|
+
# and `#session_token`.
|
71
|
+
#
|
72
|
+
class Signer
|
73
|
+
|
74
|
+
# @overload initialize(service:, region:, access_key_id:, secret_access_key:, session_token:nil, **options)
|
75
|
+
# @param [String] :service The service signing name, e.g. 's3'.
|
76
|
+
# @param [String] :region The region name, e.g. 'us-east-1'.
|
77
|
+
# @param [String] :access_key_id
|
78
|
+
# @param [String] :secret_access_key
|
79
|
+
# @param [String] :session_token (nil)
|
80
|
+
#
|
81
|
+
# @overload initialize(service:, region:, credentials:, **options)
|
82
|
+
# @param [String] :service The service signing name, e.g. 's3'.
|
83
|
+
# @param [String] :region The region name, e.g. 'us-east-1'.
|
84
|
+
# @param [Credentials] :credentials Any object that responds to the following
|
85
|
+
# methods:
|
86
|
+
#
|
87
|
+
# * `#access_key_id` => String
|
88
|
+
# * `#secret_access_key` => String
|
89
|
+
# * `#session_token` => String, nil
|
90
|
+
# * `#set?` => Boolean
|
91
|
+
#
|
92
|
+
# @overload initialize(service:, region:, credentials_provider:, **options)
|
93
|
+
# @param [String] :service The service signing name, e.g. 's3'.
|
94
|
+
# @param [String] :region The region name, e.g. 'us-east-1'.
|
95
|
+
# @param [#credentials] :credentials_provider An object that responds
|
96
|
+
# to `#credentials`, returning an object that responds to the following
|
97
|
+
# methods:
|
98
|
+
#
|
99
|
+
# * `#access_key_id` => String
|
100
|
+
# * `#secret_access_key` => String
|
101
|
+
# * `#session_token` => String, nil
|
102
|
+
# * `#set?` => Boolean
|
103
|
+
#
|
104
|
+
# @option options [Array<String>] :unsigned_headers ([]) A list of
|
105
|
+
# headers that should not be signed. This is useful when a proxy
|
106
|
+
# modifies headers, such as 'User-Agent', invalidating a signature.
|
107
|
+
#
|
108
|
+
# @option options [Boolean] :uri_escape_path (true) When `true`,
|
109
|
+
# the request URI path is uri-escaped as part of computing the canonical
|
110
|
+
# request string. This is required for every service, except Amazon S3,
|
111
|
+
# as of late 2016.
|
112
|
+
#
|
113
|
+
# @option options [Boolean] :apply_checksum_header (true) When `true`,
|
114
|
+
# the computed content checksum is returned in the hash of signature
|
115
|
+
# headers. This is required for AWS Glacier, and optional for
|
116
|
+
# every other AWS service as of late 2016.
|
117
|
+
#
|
118
|
+
def initialize(options = {})
|
119
|
+
@service = extract_service(options)
|
120
|
+
@region = extract_region(options)
|
121
|
+
@credentials_provider = extract_credentials_provider(options)
|
122
|
+
@unsigned_headers = Set.new((options.fetch(:unsigned_headers, [])).map(&:downcase))
|
123
|
+
@unsigned_headers << 'authorization'
|
124
|
+
[:uri_escape_path, :apply_checksum_header].each do |opt|
|
125
|
+
instance_variable_set("@#{opt}", options.key?(opt) ? !!options[:opt] : true)
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
# @return [String]
|
130
|
+
attr_reader :service
|
131
|
+
|
132
|
+
# @return [String]
|
133
|
+
attr_reader :region
|
134
|
+
|
135
|
+
# @return [#credentials] Returns an object that responds to
|
136
|
+
# `#credentials`, returning an object that responds to the following
|
137
|
+
# methods:
|
138
|
+
#
|
139
|
+
# * `#access_key_id` => String
|
140
|
+
# * `#secret_access_key` => String
|
141
|
+
# * `#session_token` => String, nil
|
142
|
+
# * `#set?` => Boolean
|
143
|
+
#
|
144
|
+
attr_reader :credentials_provider
|
145
|
+
|
146
|
+
# @return [Set<String>] Returns a set of header names that should not be signed.
|
147
|
+
# All header names have been downcased.
|
148
|
+
attr_reader :unsigned_headers
|
149
|
+
|
150
|
+
# @return [Boolean] When `true` the `x-amz-content-sha256` header will be signed and
|
151
|
+
# returned in the signature headers.
|
152
|
+
attr_reader :apply_checksum_header
|
153
|
+
|
154
|
+
# Computes a version 4 signature signature. Returns the resultant
|
155
|
+
# signature as a hash of headers to apply to your HTTP request. The given
|
156
|
+
# request is not modified.
|
157
|
+
#
|
158
|
+
# signature = signer.sign_request(
|
159
|
+
# http_method: 'PUT',
|
160
|
+
# url: 'https://domain.com',
|
161
|
+
# headers: {
|
162
|
+
# 'Abc' => 'xyz',
|
163
|
+
# },
|
164
|
+
# body: 'body' # String or IO object
|
165
|
+
# )
|
166
|
+
#
|
167
|
+
# # Apply the following hash of headers to your HTTP request
|
168
|
+
# signature.headers['Host']
|
169
|
+
# signature.headers['X-Amz-Date']
|
170
|
+
# signature.headers['X-Amz-Security-Token']
|
171
|
+
# signature.headers['X-Amz-Content-Sha256']
|
172
|
+
# signature.headers['Authorization']
|
173
|
+
#
|
174
|
+
# In addition to computing the signature headers, the canonicalized
|
175
|
+
# request, string to sign and content sha256 checksum are also available.
|
176
|
+
# These values are useful for debugging signature errors returned by AWS.
|
177
|
+
#
|
178
|
+
# signature.canonical_request #=> "..."
|
179
|
+
# signature.string_to_sign #=> "..."
|
180
|
+
# signature.content_sha256 #=> "..."
|
181
|
+
#
|
182
|
+
# @param [Hash] request
|
183
|
+
#
|
184
|
+
# @option request [required, String] :http_method One of
|
185
|
+
# 'GET', 'HEAD', 'PUT', 'POST', 'PATCH', or 'DELETE'
|
186
|
+
#
|
187
|
+
# @option request [required, String, URI::HTTPS, URI::HTTP] :url
|
188
|
+
# The request URI. Must be a valid HTTP or HTTPS URI.
|
189
|
+
#
|
190
|
+
# @option request [optional, Hash] :headers ({}) A hash of headers
|
191
|
+
# to sign. If the 'X-Amz-Content-Sha256' header is set, the `:body`
|
192
|
+
# is optional and will not be read.
|
193
|
+
#
|
194
|
+
# @option request [otpional, String, IO] :body ('') The HTTP request body.
|
195
|
+
# A sha256 checksum is computed of the body unless the
|
196
|
+
# 'X-Amz-Content-Sha256' header is set.
|
197
|
+
#
|
198
|
+
# @return [Signature] Return an instance of {Signature} that has
|
199
|
+
# a `#headers` method. The headers must be applied to your request.
|
200
|
+
#
|
201
|
+
def sign_request(request)
|
202
|
+
|
203
|
+
creds = get_credentials
|
204
|
+
|
205
|
+
http_method = extract_http_method(request)
|
206
|
+
url = extract_url(request)
|
207
|
+
headers = downcase_headers(request[:headers])
|
208
|
+
|
209
|
+
datetime = headers['x-amz-date']
|
210
|
+
datetime ||= Time.now.utc.strftime("%Y%m%dT%H%M%SZ")
|
211
|
+
date = datetime[0,8]
|
212
|
+
|
213
|
+
content_sha256 = headers['x-amz-content-sha256']
|
214
|
+
content_sha256 ||= sha256_hexdigest(request[:body] || '')
|
215
|
+
|
216
|
+
sigv4_headers = {}
|
217
|
+
sigv4_headers['host'] = host(url)
|
218
|
+
sigv4_headers['x-amz-date'] = datetime
|
219
|
+
sigv4_headers['x-amz-security-token'] = creds.session_token if creds.session_token
|
220
|
+
sigv4_headers['x-amz-content-sha256'] ||= content_sha256 if @apply_checksum_header
|
221
|
+
|
222
|
+
headers = headers.merge(sigv4_headers) # merge so we do not modify given headers hash
|
223
|
+
|
224
|
+
# compute signature parts
|
225
|
+
creq = canonical_request(http_method, url, headers, content_sha256)
|
226
|
+
sts = string_to_sign(datetime, creq)
|
227
|
+
sig = signature(creds.secret_access_key, date, sts)
|
228
|
+
|
229
|
+
# apply signature
|
230
|
+
sigv4_headers['authorization'] = [
|
231
|
+
"AWS4-HMAC-SHA256 Credential=#{credential(creds, date)}",
|
232
|
+
"SignedHeaders=#{signed_headers(headers)}",
|
233
|
+
"Signature=#{sig}",
|
234
|
+
].join(', ')
|
235
|
+
|
236
|
+
# Returning the signature components.
|
237
|
+
Signature.new(
|
238
|
+
headers: sigv4_headers,
|
239
|
+
string_to_sign: sts,
|
240
|
+
canonical_request: creq,
|
241
|
+
content_sha256: content_sha256
|
242
|
+
)
|
243
|
+
end
|
244
|
+
|
245
|
+
# Signs a URL with query authentication. Using query parameters
|
246
|
+
# to authenticate requests is useful when you want to express a
|
247
|
+
# request entirely in a URL. This method is also referred as
|
248
|
+
# presigning a URL.
|
249
|
+
#
|
250
|
+
# See [Authenticating Requests: Using Query Parameters (AWS Signature Version 4)](http://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html) for more information.
|
251
|
+
#
|
252
|
+
# To generate a presigned URL, you must provide a HTTP URI and
|
253
|
+
# the http method.
|
254
|
+
#
|
255
|
+
# url = signer.presigned_url(
|
256
|
+
# http_method: 'GET',
|
257
|
+
# url: 'https://my-bucket.s3-us-east-1.amazonaws.com/key',
|
258
|
+
# expires_in: 60
|
259
|
+
# )
|
260
|
+
#
|
261
|
+
# By default, signatures are valid for 15 minutes. You can specify
|
262
|
+
# the number of seconds for the URL to expire in.
|
263
|
+
#
|
264
|
+
# url = signer.presigned_url(
|
265
|
+
# http_method: 'GET',
|
266
|
+
# url: 'https://my-bucket.s3-us-east-1.amazonaws.com/key',
|
267
|
+
# expires_in: 3600 # one hour
|
268
|
+
# )
|
269
|
+
#
|
270
|
+
# You can provide a hash of headers that you plan to send with the
|
271
|
+
# request. Every 'X-Amz-*' header you plan to send with the request
|
272
|
+
# **must** be provided, or the signature is invalid. Other headers
|
273
|
+
# are optional, but should be provided for security reasons.
|
274
|
+
#
|
275
|
+
# url = signer.presigned_url(
|
276
|
+
# http_method: 'PUT',
|
277
|
+
# url: 'https://my-bucket.s3-us-east-1.amazonaws.com/key',
|
278
|
+
# headers: {
|
279
|
+
# 'X-Amz-Meta-Custom' => 'metadata'
|
280
|
+
# }
|
281
|
+
# )
|
282
|
+
#
|
283
|
+
# @option options [required, String] :http_method The HTTP request method,
|
284
|
+
# e.g. 'GET', 'HEAD', 'PUT', 'POST', 'PATCH', or 'DELETE'.
|
285
|
+
#
|
286
|
+
# @option options [required, String, HTTPS::URI, HTTP::URI] :url
|
287
|
+
# The URI to sign.
|
288
|
+
#
|
289
|
+
# @option options [Hash] :headers ({}) Headers that should
|
290
|
+
# be signed and sent along with the request. All x-amz-*
|
291
|
+
# headers must be present during signing. Other
|
292
|
+
# headers are optional.
|
293
|
+
#
|
294
|
+
# @option options [Integer<Seconds>] :expires_in (900)
|
295
|
+
# How long the presigned URL should be valid for. Defaults
|
296
|
+
# to 15 minutes (900 seconds).
|
297
|
+
#
|
298
|
+
# @option options [optional, String, IO] :body
|
299
|
+
# If the `:body` is set, then a SHA256 hexdigest will be computed of the body.
|
300
|
+
# If `:body_digest` is set, this option is ignored. If neither are set, then
|
301
|
+
# the `:body_digest` will be computed of the empty string.
|
302
|
+
#
|
303
|
+
# @option options [optional, String] :body_digest
|
304
|
+
# The SHA256 hexdigest of the request body. If you wish to send the presigned
|
305
|
+
# request without signing the body, you can pass 'UNSIGNED-PAYLOAD' as the
|
306
|
+
# `:body_digest` in place of passing `:body`.
|
307
|
+
#
|
308
|
+
# @option options [Time] :time (Time.now) Time of the signature.
|
309
|
+
# You should only set this value for testing.
|
310
|
+
#
|
311
|
+
# @return [HTTPS::URI, HTTP::URI]
|
312
|
+
#
|
313
|
+
def presign_url(options)
|
314
|
+
|
315
|
+
creds = get_credentials
|
316
|
+
|
317
|
+
http_method = extract_http_method(options)
|
318
|
+
url = extract_url(options)
|
319
|
+
|
320
|
+
headers = downcase_headers(options[:headers])
|
321
|
+
headers['host'] = host(url)
|
322
|
+
|
323
|
+
datetime = headers['x-amz-date']
|
324
|
+
datetime ||= (options[:time] || Time.now).utc.strftime("%Y%m%dT%H%M%SZ")
|
325
|
+
date = datetime[0,8]
|
326
|
+
|
327
|
+
content_sha256 = headers['x-amz-content-sha256']
|
328
|
+
content_sha256 ||= options[:body_digest]
|
329
|
+
content_sha256 ||= sha256_hexdigest(options[:body] || '')
|
330
|
+
|
331
|
+
params = {}
|
332
|
+
params['X-Amz-Algorithm'] = 'AWS4-HMAC-SHA256'
|
333
|
+
params['X-Amz-Credential'] = credential(creds, date)
|
334
|
+
params['X-Amz-Date'] = datetime
|
335
|
+
params['X-Amz-Expires'] = extract_expires_in(options)
|
336
|
+
params['X-Amz-SignedHeaders'] = signed_headers(headers)
|
337
|
+
params['X-Amz-Security-Token'] = creds.session_token if creds.session_token
|
338
|
+
|
339
|
+
params = params.map do |key, value|
|
340
|
+
"#{uri_escape(key)}=#{uri_escape(value)}"
|
341
|
+
end.join('&')
|
342
|
+
|
343
|
+
if url.query
|
344
|
+
url.query += '&' + params
|
345
|
+
else
|
346
|
+
url.query = params
|
347
|
+
end
|
348
|
+
|
349
|
+
creq = canonical_request(http_method, url, headers, content_sha256)
|
350
|
+
sts = string_to_sign(datetime, creq)
|
351
|
+
url.query += '&X-Amz-Signature=' + signature(creds.secret_access_key, date, sts)
|
352
|
+
url
|
353
|
+
end
|
354
|
+
|
355
|
+
private
|
356
|
+
|
357
|
+
def canonical_request(http_method, url, headers, content_sha256)
|
358
|
+
[
|
359
|
+
http_method,
|
360
|
+
path(url),
|
361
|
+
normalized_querystring(url.query || ''),
|
362
|
+
canonical_headers(headers) + "\n",
|
363
|
+
signed_headers(headers),
|
364
|
+
content_sha256,
|
365
|
+
].join("\n")
|
366
|
+
end
|
367
|
+
|
368
|
+
def string_to_sign(datetime, canonical_request)
|
369
|
+
[
|
370
|
+
'AWS4-HMAC-SHA256',
|
371
|
+
datetime,
|
372
|
+
credential_scope(datetime[0,8]),
|
373
|
+
sha256_hexdigest(canonical_request),
|
374
|
+
].join("\n")
|
375
|
+
end
|
376
|
+
|
377
|
+
def credential_scope(date)
|
378
|
+
[
|
379
|
+
date,
|
380
|
+
@region,
|
381
|
+
@service,
|
382
|
+
'aws4_request',
|
383
|
+
].join('/')
|
384
|
+
end
|
385
|
+
|
386
|
+
def credential(credentials, date)
|
387
|
+
"#{credentials.access_key_id}/#{credential_scope(date)}"
|
388
|
+
end
|
389
|
+
|
390
|
+
def signature(secret_access_key, date, string_to_sign)
|
391
|
+
k_date = hmac("AWS4" + secret_access_key, date)
|
392
|
+
k_region = hmac(k_date, @region)
|
393
|
+
k_service = hmac(k_region, @service)
|
394
|
+
k_credentials = hmac(k_service, 'aws4_request')
|
395
|
+
hexhmac(k_credentials, string_to_sign)
|
396
|
+
end
|
397
|
+
|
398
|
+
def path(url)
|
399
|
+
path = url.path
|
400
|
+
path = '/' if path == ''
|
401
|
+
if @uri_escape_path
|
402
|
+
uri_escape_path(path)
|
403
|
+
else
|
404
|
+
path
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
def normalized_querystring(querystring)
|
409
|
+
params = querystring.split('&')
|
410
|
+
params = params.map { |p| p.match(/=/) ? p : p + '=' }
|
411
|
+
# We have to sort by param name and preserve order of params that
|
412
|
+
# have the same name. Default sort <=> in JRuby will swap members
|
413
|
+
# occasionally when <=> is 0 (considered still sorted), but this
|
414
|
+
# causes our normalized query string to not match the sent querystring.
|
415
|
+
# When names match, we then sort by their original order
|
416
|
+
params = params.each.with_index.sort do |a, b|
|
417
|
+
a, a_offset = a
|
418
|
+
a_name = a.split('=')[0]
|
419
|
+
b, b_offset = b
|
420
|
+
b_name = b.split('=')[0]
|
421
|
+
if a_name == b_name
|
422
|
+
a_offset <=> b_offset
|
423
|
+
else
|
424
|
+
a_name <=> b_name
|
425
|
+
end
|
426
|
+
end.map(&:first).join('&')
|
427
|
+
end
|
428
|
+
|
429
|
+
def signed_headers(headers)
|
430
|
+
headers.inject([]) do |signed_headers, (header, _)|
|
431
|
+
if @unsigned_headers.include?(header)
|
432
|
+
signed_headers
|
433
|
+
else
|
434
|
+
signed_headers << header
|
435
|
+
end
|
436
|
+
end.sort.join(';')
|
437
|
+
end
|
438
|
+
|
439
|
+
def canonical_headers(headers)
|
440
|
+
headers = headers.inject([]) do |headers, (k,v)|
|
441
|
+
if @unsigned_headers.include?(k)
|
442
|
+
headers
|
443
|
+
else
|
444
|
+
headers << [k,v]
|
445
|
+
end
|
446
|
+
end
|
447
|
+
headers = headers.sort_by(&:first)
|
448
|
+
headers.map{|k,v| "#{k}:#{canonical_header_value(v.to_s)}" }.join("\n")
|
449
|
+
end
|
450
|
+
|
451
|
+
def canonical_header_value(value)
|
452
|
+
value.match(/^".*"$/) ? value : value.gsub(/\s+/, ' ').strip
|
453
|
+
end
|
454
|
+
|
455
|
+
def host(uri)
|
456
|
+
if standard_port?(uri)
|
457
|
+
uri.host
|
458
|
+
else
|
459
|
+
"#{uri.host}:#{uri.port}"
|
460
|
+
end
|
461
|
+
end
|
462
|
+
|
463
|
+
def standard_port?(uri)
|
464
|
+
(uri.scheme == 'http' && uri.port == 80) ||
|
465
|
+
(uri.scheme == 'https' && uri.port == 443)
|
466
|
+
end
|
467
|
+
|
468
|
+
# @param [File, Tempfile, IO#read, String] value
|
469
|
+
# @return [String<SHA256 Hexdigest>]
|
470
|
+
def sha256_hexdigest(value)
|
471
|
+
if (File === value || Tempfile === value) && !value.path.nil? && File.exist?(value.path)
|
472
|
+
OpenSSL::Digest::SHA256.file(value).hexdigest
|
473
|
+
elsif value.respond_to?(:read)
|
474
|
+
sha256 = OpenSSL::Digest::SHA256.new
|
475
|
+
while chunk = value.read(1024 * 1024) # 1MB
|
476
|
+
sha256.update(chunk)
|
477
|
+
end
|
478
|
+
value.rewind
|
479
|
+
sha256.hexdigest
|
480
|
+
else
|
481
|
+
OpenSSL::Digest::SHA256.hexdigest(value)
|
482
|
+
end
|
483
|
+
end
|
484
|
+
|
485
|
+
def hmac(key, value)
|
486
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, value)
|
487
|
+
end
|
488
|
+
|
489
|
+
def hexhmac(key, value)
|
490
|
+
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), key, value)
|
491
|
+
end
|
492
|
+
|
493
|
+
def extract_service(options)
|
494
|
+
if options[:service]
|
495
|
+
options[:service]
|
496
|
+
else
|
497
|
+
msg = "missing required option :service"
|
498
|
+
raise ArgumentError, msg
|
499
|
+
end
|
500
|
+
end
|
501
|
+
|
502
|
+
def extract_region(options)
|
503
|
+
if options[:region]
|
504
|
+
options[:region]
|
505
|
+
else
|
506
|
+
raise Errors::MissingRegionError
|
507
|
+
end
|
508
|
+
end
|
509
|
+
|
510
|
+
def extract_credentials_provider(options)
|
511
|
+
if options[:credentials_provider]
|
512
|
+
options[:credentials_provider]
|
513
|
+
elsif options.key?(:credentials) || options.key?(:access_key_id)
|
514
|
+
StaticCredentialsProvider.new(options)
|
515
|
+
else
|
516
|
+
raise Errors::MissingCredentialsError
|
517
|
+
end
|
518
|
+
end
|
519
|
+
|
520
|
+
def extract_http_method(request)
|
521
|
+
if request[:http_method]
|
522
|
+
request[:http_method].upcase
|
523
|
+
else
|
524
|
+
msg = "missing required option :http_method"
|
525
|
+
raise ArgumentError, msg
|
526
|
+
end
|
527
|
+
end
|
528
|
+
|
529
|
+
def extract_url(request)
|
530
|
+
if request[:url]
|
531
|
+
URI.parse(request[:url].to_s)
|
532
|
+
else
|
533
|
+
msg = "missing required option :url"
|
534
|
+
raise ArgumentError, msg
|
535
|
+
end
|
536
|
+
end
|
537
|
+
|
538
|
+
def downcase_headers(headers)
|
539
|
+
(headers || {}).to_hash.inject({}) do |hash, (key, value)|
|
540
|
+
hash[key.downcase] = value
|
541
|
+
hash
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
def extract_expires_in(options)
|
546
|
+
case options[:expires_in]
|
547
|
+
when nil then 900.to_s
|
548
|
+
when Integer then options[:expires_in].to_s
|
549
|
+
else
|
550
|
+
msg = "expected :expires_in to be a number of seconds"
|
551
|
+
raise ArgumentError, msg
|
552
|
+
end
|
553
|
+
end
|
554
|
+
|
555
|
+
def uri_escape(string)
|
556
|
+
self.class.uri_escape(string)
|
557
|
+
end
|
558
|
+
|
559
|
+
def uri_escape_path(string)
|
560
|
+
self.class.uri_escape_path(string)
|
561
|
+
end
|
562
|
+
|
563
|
+
def get_credentials
|
564
|
+
credentials = @credentials_provider.credentials
|
565
|
+
if credentials_set?(credentials)
|
566
|
+
credentials
|
567
|
+
else
|
568
|
+
msg = 'unable to sign request without credentials set'
|
569
|
+
raise Errors::MissingCredentialsError.new(msg)
|
570
|
+
end
|
571
|
+
end
|
572
|
+
|
573
|
+
def credentials_set?(credentials)
|
574
|
+
credentials.access_key_id && credentials.secret_access_key
|
575
|
+
end
|
576
|
+
|
577
|
+
class << self
|
578
|
+
|
579
|
+
# @api private
|
580
|
+
def uri_escape_path(path)
|
581
|
+
path.gsub(/[^\/]+/) { |part| uri_escape(part) }
|
582
|
+
end
|
583
|
+
|
584
|
+
# @api private
|
585
|
+
def uri_escape(string)
|
586
|
+
if string.nil?
|
587
|
+
nil
|
588
|
+
else
|
589
|
+
CGI.escape(string.encode('UTF-8')).gsub('+', '%20').gsub('%7E', '~')
|
590
|
+
end
|
591
|
+
end
|
592
|
+
|
593
|
+
end
|
594
|
+
end
|
595
|
+
end
|
596
|
+
end
|
metadata
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: aws-sigv4
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Amazon Web Services
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-11-08 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Amazon Web Services Signature Version 4 signing ligrary. Generates sigv4
|
14
|
+
signature for HTTP requests.
|
15
|
+
email:
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- lib/aws-sigv4.rb
|
21
|
+
- lib/aws-sigv4/credentials.rb
|
22
|
+
- lib/aws-sigv4/errors.rb
|
23
|
+
- lib/aws-sigv4/request.rb
|
24
|
+
- lib/aws-sigv4/signature.rb
|
25
|
+
- lib/aws-sigv4/signer.rb
|
26
|
+
homepage: http://github.com/aws/aws-sdk-ruby
|
27
|
+
licenses:
|
28
|
+
- Apache-2.0
|
29
|
+
metadata: {}
|
30
|
+
post_install_message:
|
31
|
+
rdoc_options: []
|
32
|
+
require_paths:
|
33
|
+
- lib
|
34
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
35
|
+
requirements:
|
36
|
+
- - ">="
|
37
|
+
- !ruby/object:Gem::Version
|
38
|
+
version: '0'
|
39
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
40
|
+
requirements:
|
41
|
+
- - ">="
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
requirements: []
|
45
|
+
rubyforge_project:
|
46
|
+
rubygems_version: 2.6.4
|
47
|
+
signing_key:
|
48
|
+
specification_version: 4
|
49
|
+
summary: AWS Signature Version 4 library.
|
50
|
+
test_files: []
|