class S3::Signature

Class responsible for generating signatures to requests.

Implements algorithm defined by Amazon Web Services to sign request with secret private credentials

See

docs.amazonwebservices.com/AmazonS3/latest/index.html?RESTAuthentication.html

Public Class Methods

generate(options) click to toggle source

Generates signature for given parameters

Options

  • :host - Hostname

  • :request - Net::HTTPRequest object with correct headers

  • :access_key_id - Access key id

  • :secret_access_key - Secret access key

Returns

Generated signature string for given hostname and request

# File lib/s3/signature.rb, line 24
def self.generate(options)
  request = options[:request]
  access_key_id = options[:access_key_id]

  options.merge!(:headers => request,
                 :method => request.method,
                 :resource => request.path)

  signature = canonicalized_signature(options)

  "AWS #{access_key_id}:#{signature}"
end
generate_temporary_url(options) click to toggle source

Generates temporary URL for given resource

Options

  • :bucket - Bucket in which the resource resides

  • :resource - Path to the resouce you want to create a temporary link to

  • :access_key - Access key

  • :secret_access_key - Secret access key

  • :expires_at - Unix time stamp of when the resouce link will expire

  • :method - HTTP request method you want to use on the resource, defaults to GET

  • :headers - Any additional HTTP headers you intend to use when requesting the resource

  • :add_bucket_to_host - Use in case of virtual-host style, defaults to false

# File lib/s3/signature.rb, line 88
def self.generate_temporary_url(options)
  bucket = options[:bucket]
  resource = options[:resource]
  access_key = options[:access_key]
  expires = options[:expires_at].to_i
  host = options[:host]

  if options[:add_bucket_to_host]
    host = bucket + '.' + host
    url  = "http://#{host}/#{resource}"
  else
    url = "http://#{host}/#{bucket}/#{resource}"
  end

  options[:host] = host
  signature = generate_temporary_url_signature(options)

  url << "?AWSAccessKeyId=#{access_key}"
  url << "&Expires=#{expires}"
  url << "&Signature=#{signature}"
end
generate_temporary_url_signature(options) click to toggle source

Generates temporary URL signature for given resource

Options

  • :bucket - Bucket in which the resource resides

  • :resource - Path to the resouce you want to create a temporary link to

  • :secret_access_key - Secret access key

  • :expires_at - Unix time stamp of when the resouce link will expire

  • :method - HTTP request method you want to use on the resource, defaults to GET

  • :headers - Any additional HTTP headers you intend to use when requesting the resource

  • :add_bucket_to_host - Use in case of virtual-host style, defaults to false

# File lib/s3/signature.rb, line 52
def self.generate_temporary_url_signature(options)
  bucket = options[:bucket]
  resource = options[:resource]
  secret_access_key = options[:secret_access_key]
  expires = options[:expires_at]

  headers = options[:headers] || {}
  headers.merge!("date" => expires.to_i.to_s)

  resource = "/#{URI.escape(resource, /[^#{URI::REGEXP::PATTERN::UNRESERVED}\/]/)}"
  resource = "/#{bucket}" + resource unless options[:add_bucket_to_host]

  options.merge!(:resource => resource,
                 :method => options[:method] || :get,
                 :headers => headers)
  signature = canonicalized_signature(options)

  CGI.escape(signature)
end

Private Class Methods

canonicalized_amz_headers(request) click to toggle source

Helper method for extracting header fields from Net::HTTPRequest and preparing them for singing in generate method

Parameters

  • request - Net::HTTPRequest object with header fields filled in

Returns

String containing interesting header fields in suitable order and form

# File lib/s3/signature.rb, line 154
def self.canonicalized_amz_headers(request)
  headers = []

  # 1. Convert each HTTP header name to lower-case. For example,
  # "X-Amz-Date" becomes "x-amz-date".
  request.each { |key, value| headers << [key.downcase, value] if key =~ /\Ax-amz-/io }
  #=> [["c", 0], ["a", 1], ["a", 2], ["b", 3]]

  # 2. Sort the collection of headers lexicographically by header
  # name.
  headers.sort!
  #=> [["a", 1], ["a", 2], ["b", 3], ["c", 0]]

  # 3. Combine header fields with the same name into one
  # "header-name:comma-separated-value-list" pair as prescribed by
  # RFC 2616, section 4.2, without any white-space between
  # values. For example, the two metadata headers
  # "x-amz-meta-username: fred" and "x-amz-meta-username: barney"
  # would be combined into the single header "x-amz-meta-username:
  # fred,barney".
  combined_headers = headers.inject([]) do |new_headers, header|
    existing_header = new_headers.find { |h| h.first == header.first }
    if existing_header
      existing_header.last << ",#{header.last}"
    else
      new_headers << header
    end
  end
  #=> [["a", "1,2"], ["b", "3"], ["c", "0"]]

  # 4. "Un-fold" long headers that span multiple lines (as allowed
  # by RFC 2616, section 4.2) by replacing the folding white-space
  # (including new-line) by a single space.
  unfolded_headers = combined_headers.map do |header|
    key = header.first
    value = header.last
    value.gsub!(/\s+/, " ")
    [key, value]
  end

  # 5. Trim any white-space around the colon in the header. For
  # example, the header "x-amz-meta-username: fred,barney" would
  # become "x-amz-meta-username:fred,barney"
  joined_headers = unfolded_headers.map do |header|
    key = header.first.strip
    value = header.last.strip
    "#{key}:#{value}"
  end

  # 6. Finally, append a new-line (U+000A) to each canonicalized
  # header in the resulting list. Construct the
  # CanonicalizedResource element by concatenating all headers in
  # this list into a single string.
  joined_headers << "" unless joined_headers.empty?
  joined_headers.join("\n")
end
canonicalized_resource(host, resource) click to toggle source

Helper methods for extracting caninocalized resource address

Parameters

  • host - Hostname

  • request - Net::HTTPRequest object with header fields filled in

Returns

String containing extracted canonicalized resource

# File lib/s3/signature.rb, line 220
def self.canonicalized_resource(host, resource)
  # 1. Start with the empty string ("").
  string = ""

  # 2. If the request specifies a bucket using the HTTP Host
  # header (virtual hosted-style), append the bucket name preceded
  # by a "/" (e.g., "/bucketname"). For path-style requests and
  # requests that don't address a bucket, do nothing. For more
  # information on virtual hosted-style requests, see Virtual
  # Hosting of Buckets.
  bucket_name = host.sub(/\.?#{host}\Z/, "")
  string << "/#{bucket_name}" unless bucket_name.empty?

  # 3. Append the path part of the un-decoded HTTP Request-URI,
  # up-to but not including the query string.
  uri = URI.parse(resource)
  string << uri.path

  # 4. If the request addresses a sub-resource, like ?location,
  # ?acl, or ?torrent, append the sub-resource including question
  # mark.
  sub_resources = [
    "acl",
    "location",
    "logging",
    "notification",
    "partNumber",
    "policy",
    "requestPayment",
    "torrent",
    "uploadId",
    "uploads",
    "versionId",
    "versioning",
    "versions",
    "website"
  ]
  string << "?#{$1}" if uri.query =~ /&?(#{sub_resources.join("|")})(?:&|=|\Z)/
  string
end
canonicalized_signature(options) click to toggle source
# File lib/s3/signature.rb, line 112
def self.canonicalized_signature(options)
  headers = options[:headers] || {}
  host = options[:host] || ""
  resource = options[:resource]
  access_key_id = options[:access_key_id]
  secret_access_key = options[:secret_access_key]

  http_verb = options[:method].to_s.upcase
  content_md5 = headers["content-md5"] || ""
  content_type = headers["content-type"] || ""
  date = headers["x-amz-date"].nil? ? headers["date"] : ""
  canonicalized_resource = canonicalized_resource(host, resource)
  canonicalized_amz_headers = canonicalized_amz_headers(headers)

  string_to_sign = ""
  string_to_sign << http_verb
  string_to_sign << "\n"
  string_to_sign << content_md5
  string_to_sign << "\n"
  string_to_sign << content_type
  string_to_sign << "\n"
  string_to_sign << date
  string_to_sign << "\n"
  string_to_sign << canonicalized_amz_headers
  string_to_sign << canonicalized_resource

  digest = OpenSSL::Digest.new("sha1")
  hmac = OpenSSL::HMAC.digest(digest, secret_access_key, string_to_sign)
  base64 = Base64.encode64(hmac)
  base64.chomp
end