Module: Everywhere::S3::Signer
- Defined in:
- lib/everywhere/s3.rb
Overview
Pure SigV4 header computation, separated for direct testing against the published AWS test vectors.
Class Method Summary collapse
-
.canonical_query(query) ⇒ Object
Canonical form: every param as key=value (empty value keeps the "="), sorted.
- .headers(method:, uri:, headers:, payload_sha256:, access_key_id:, secret_access_key:, session_token: nil, region:, time: Time.now.utc) ⇒ Object
- .host_header(uri) ⇒ Object
-
.uri_encode(str) ⇒ Object
RFC 3986 unreserved-only encoding (AWS canonical form).
Class Method Details
.canonical_query(query) ⇒ Object
Canonical form: every param as key=value (empty value keeps the "="), sorted. Callers pass unencoded simple queries; we never need decoding.
212 213 214 215 216 217 218 219 |
# File 'lib/everywhere/s3.rb', line 212 def canonical_query(query) return "" if query.nil? || query.empty? query.split("&").map { |pair| k, v = pair.split("=", 2) "#{uri_encode(k)}=#{uri_encode(v || "")}" }.sort.join("&") end |
.headers(method:, uri:, headers:, payload_sha256:, access_key_id:, secret_access_key:, session_token: nil, region:, time: Time.now.utc) ⇒ Object
168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 |
# File 'lib/everywhere/s3.rb', line 168 def headers(method:, uri:, headers:, payload_sha256:, access_key_id:, secret_access_key:, session_token: nil, region:, time: Time.now.utc) amz_date = time.strftime("%Y%m%dT%H%M%SZ") date = time.strftime("%Y%m%d") signed = { "host" => host_header(uri), "x-amz-content-sha256" => payload_sha256, "x-amz-date" => amz_date } signed["x-amz-security-token"] = session_token if session_token # Sign every x-amz-* header we send (S3 requires it) plus the core set. canonical_headers = headers.select { |k, _| k.downcase.start_with?("x-amz-") || %w[range content-type content-md5 cache-control date expires].include?(k.downcase) } .transform_keys(&:downcase).merge(signed) sorted = canonical_headers.sort.to_h header_list = sorted.keys.join(";") canonical_request = [ method.to_s.upcase, uri.path.empty? ? "/" : uri.path, canonical_query(uri.query), sorted.map { |k, v| "#{k}:#{v.to_s.strip}\n" }.join, header_list, payload_sha256 ].join("\n") scope = "#{date}/#{region}/s3/aws4_request" string_to_sign = [ ALGORITHM, amz_date, scope, OpenSSL::Digest::SHA256.hexdigest(canonical_request) ].join("\n") k = %W[AWS4#{secret_access_key} #{date} #{region} s3 aws4_request] .reduce { |key, data| OpenSSL::HMAC.digest("SHA256", key, data) } signature = OpenSSL::HMAC.hexdigest("SHA256", k, string_to_sign) signed.merge( "authorization" => "#{ALGORITHM} Credential=#{access_key_id}/#{scope}, " \ "SignedHeaders=#{header_list}, Signature=#{signature}" ) end |
.host_header(uri) ⇒ Object
221 222 223 224 |
# File 'lib/everywhere/s3.rb', line 221 def host_header(uri) default_port = uri.scheme == "https" ? 443 : 80 uri.port == default_port ? uri.host : "#{uri.host}:#{uri.port}" end |
.uri_encode(str) ⇒ Object
RFC 3986 unreserved-only encoding (AWS canonical form).
227 228 229 |
# File 'lib/everywhere/s3.rb', line 227 def uri_encode(str) str.to_s.gsub(/[^A-Za-z0-9\-._~]/) { |c| c.bytes.map { |b| format("%%%02X", b) }.join } end |