Skip to content

Instantly share code, notes, and snippets.

@conorh
Last active April 26, 2016 02:03
Show Gist options
  • Save conorh/d135ae05da757a4c369953d744e99d97 to your computer and use it in GitHub Desktop.
Save conorh/d135ae05da757a4c369953d744e99d97 to your computer and use it in GitHub Desktop.
Calculate AWS v4 signatures
require "uri"
require "time"
require "openssl"
# Calculate v4 AWS sig
class AWSV4Sig
def initialize(access_key, secret_key)
@access_key = access_key
@secret_key = secret_key
end
def auth_header(method, uri, headers, request_payload, opts = {})
@uri = uri.kind_of?(URI) ? uri : URI.parse(uri)
@headers = headers
@method = method
@request_payload = request_payload
@date = headers["x-amz-date"] || headers["Date"]
@short_date = Time.parse(@date).strftime("%Y%m%d")
split_host = @uri.host.split(".")
@region = opts[:region] || split_host[2]
@service = opts[:service] || split_host[1]
auth_header = "AWS4-HMAC-SHA256 Credential=#{@access_key}/#{credentials}, SignedHeaders=#{signed_headers}, Signature=#{signature}"
end
def string_to_sign
"AWS4-HMAC-SHA256\n#{@date}\n#{credentials}\n#{digest(canonical_request)}"
end
def credentials
"#{@short_date}/#{@region}/#{@service}/aws4_request"
end
def signature
kSecret = @secret_key
kDate = hmac("AWS4" + kSecret, @short_date)
kRegion = hmac(kDate, @region)
kService = hmac(kRegion, @service)
kSigning = hmac(kService, "aws4_request")
hexdigest_hmac(kSigning, string_to_sign)
end
# http://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
def canonical_request
[
@method,
canonical_uri,
normalized_query_string,
canonical_headers,
signed_headers,
digest(@request_payload || '')
].join("\n")
end
def normalized_query_string
if @uri.query
@uri.query.split('&').map {|v| [v.split("=")[0], v] }.sort_by {|v| v[0] }.map {|v| [v[1]]}.join("&")
else
""
end
end
def canonical_headers
@headers.sort_by {|k,v| k.downcase} .map do |k, v|
[k.downcase.strip, v.to_s.strip.gsub(/\s+/,' ')].join(":")
end.join("\n") + "\n"
end
def signed_headers
# We sign all the headers, so the set of signed headers is all of them :)
@headers.sort_by {|k,v| k.downcase }.map {|k, v| k.downcase}.join(";")
end
def canonical_uri
path = @uri.path
path = "/" if path.nil? || path == ""
path
end
def digest(value)
Digest::SHA256.new.update(value).hexdigest
end
def hmac(key, value)
OpenSSL::HMAC.digest(OpenSSL::Digest.new('sha256'), key, value)
end
def hexdigest_hmac(key, value)
OpenSSL::HMAC.hexdigest(OpenSSL::Digest.new('sha256'), key, value)
end
end
# Sample
req = AWSV4Sig.new("AKIDEXAMPLE","wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY")
headers = {
"Host" => "example.amazonaws.com",
"X-Amz-Date" => "20150830T123600Z"
}
# From a sample in AWS docs - http://docs.aws.amazon.com/general/latest/gr/signature-v4-test-suite.html
puts req.auth_header("GET", "http://example.amazonaws.com/?Param2=value2&Param1=value1", headers, nil, region: "us-east-1", service: "service")
puts "AWS4-HMAC-SHA256 Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request, SignedHeaders=host;x-amz-date, Signature=b97d918cfa904a5beff61c982a1b6f458b799221646efd99d3219ec94cdf2500"
@conorh
Copy link
Author

conorh commented Apr 26, 2016

Sample usage for an upload to s3

signer = AWSV4Sig.new(access_key, secret_key)
uri = URI("http://#{bucket_name}.s3.amazonaws.com/new_file.txt")
date = Time.now.utc + 60

content = "this is a test"

headers = {
  "Host" => uri.host,
  "X-Amz-Date" => date.strftime("%Y%m%dT%H%M%SZ"),
  "Content-Type" => "text/plain; charset=utf8",
  "x-amz-content-sha256" => signer.digest(content)
}

http = Net::HTTP.new(uri.host, uri.port)
req = Net::HTTP::Put.new(uri.path, headers)
req.body = content

all_headers = {}
req.each_header {|n, v| all_headers[n] = v}
req["Authorization"] = signer.auth_header("PUT", uri, all_headers, content, region: "us-east-1", service: "s3")

res = http.request(req)
puts "Response #{res.code} #{res.message}: #{res.body}"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment