1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175
|
require_relative 'openssl'
##
# S3URISigner implements AWS SigV4 for S3 Source to avoid a dependency on the aws-sdk-* gems
# More on AWS SigV4: https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html
class Gem::S3URISigner
class ConfigurationError < Gem::Exception
def initialize(message)
super message
end
def to_s # :nodoc:
"#{super}"
end
end
class InstanceProfileError < Gem::Exception
def initialize(message)
super message
end
def to_s # :nodoc:
"#{super}"
end
end
attr_accessor :uri
def initialize(uri)
@uri = uri
end
##
# Signs S3 URI using query-params according to the reference: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
def sign(expiration = 86400)
s3_config = fetch_s3_config
current_time = Time.now.utc
date_time = current_time.strftime("%Y%m%dT%H%m%SZ")
date = date_time[0,8]
credential_info = "#{date}/#{s3_config.region}/s3/aws4_request"
canonical_host = "#{uri.host}.s3.#{s3_config.region}.amazonaws.com"
query_params = generate_canonical_query_params(s3_config, date_time, credential_info, expiration)
canonical_request = generate_canonical_request(canonical_host, query_params)
string_to_sign = generate_string_to_sign(date_time, credential_info, canonical_request)
signature = generate_signature(s3_config, date, string_to_sign)
URI.parse("https://#{canonical_host}#{uri.path}?#{query_params}&X-Amz-Signature=#{signature}")
end
private
S3Config = Struct.new :access_key_id, :secret_access_key, :security_token, :region
def generate_canonical_query_params(s3_config, date_time, credential_info, expiration)
canonical_params = {}
canonical_params["X-Amz-Algorithm"] = "AWS4-HMAC-SHA256"
canonical_params["X-Amz-Credential"] = "#{s3_config.access_key_id}/#{credential_info}"
canonical_params["X-Amz-Date"] = date_time
canonical_params["X-Amz-Expires"] = expiration.to_s
canonical_params["X-Amz-SignedHeaders"] = "host"
canonical_params["X-Amz-Security-Token"] = s3_config.security_token if s3_config.security_token
# Sorting is required to generate proper signature
canonical_params.sort.to_h.map do |key, value|
"#{base64_uri_escape(key)}=#{base64_uri_escape(value)}"
end.join("&")
end
def generate_canonical_request(canonical_host, query_params)
[
"GET",
uri.path,
query_params,
"host:#{canonical_host}",
"", # empty params
"host",
"UNSIGNED-PAYLOAD",
].join("\n")
end
def generate_string_to_sign(date_time, credential_info, canonical_request)
[
"AWS4-HMAC-SHA256",
date_time,
credential_info,
OpenSSL::Digest::SHA256.hexdigest(canonical_request),
].join("\n")
end
def generate_signature(s3_config, date, string_to_sign)
date_key = OpenSSL::HMAC.digest("sha256", "AWS4" + s3_config.secret_access_key, date)
date_region_key = OpenSSL::HMAC.digest("sha256", date_key, s3_config.region)
date_region_service_key = OpenSSL::HMAC.digest("sha256", date_region_key, "s3")
signing_key = OpenSSL::HMAC.digest("sha256", date_region_service_key, "aws4_request")
OpenSSL::HMAC.hexdigest("sha256", signing_key, string_to_sign)
end
##
# Extracts S3 configuration for S3 bucket
def fetch_s3_config
return S3Config.new(uri.user, uri.password, nil, "us-east-1") if uri.user && uri.password
s3_source = Gem.configuration[:s3_source] || Gem.configuration["s3_source"]
host = uri.host
raise ConfigurationError.new("no s3_source key exists in .gemrc") unless s3_source
auth = s3_source[host] || s3_source[host.to_sym]
raise ConfigurationError.new("no key for host #{host} in s3_source in .gemrc") unless auth
provider = auth[:provider] || auth["provider"]
case provider
when "env"
id = ENV["AWS_ACCESS_KEY_ID"]
secret = ENV["AWS_SECRET_ACCESS_KEY"]
security_token = ENV["AWS_SESSION_TOKEN"]
when "instance_profile"
credentials = ec2_metadata_credentials_json
id = credentials["AccessKeyId"]
secret = credentials["SecretAccessKey"]
security_token = credentials["Token"]
else
id = auth[:id] || auth["id"]
secret = auth[:secret] || auth["secret"]
security_token = auth[:security_token] || auth["security_token"]
end
raise ConfigurationError.new("s3_source for #{host} missing id or secret") unless id && secret
region = auth[:region] || auth["region"] || "us-east-1"
S3Config.new(id, secret, security_token, region)
end
def base64_uri_escape(str)
str.gsub(/[\+\/=\n]/, BASE64_URI_TRANSLATE)
end
def ec2_metadata_credentials_json
require 'net/http'
require_relative 'request'
require_relative 'request/connection_pools'
require 'json'
iam_info = ec2_metadata_request(EC2_IAM_INFO)
# Expected format: arn:aws:iam::<id>:instance-profile/<role_name>
role_name = iam_info['InstanceProfileArn'].split('/').last
ec2_metadata_request(EC2_IAM_SECURITY_CREDENTIALS + role_name)
end
def ec2_metadata_request(url)
uri = URI(url)
@request_pool ||= create_request_pool(uri)
request = Gem::Request.new(uri, Net::HTTP::Get, nil, @request_pool)
response = request.fetch
case response
when Net::HTTPOK then
JSON.parse(response.body)
else
raise InstanceProfileError.new("Unable to fetch AWS metadata from #{uri}: #{response.message} #{response.code}")
end
end
def create_request_pool(uri)
proxy_uri = Gem::Request.proxy_uri(Gem::Request.get_proxy_from_env(uri.scheme))
certs = Gem::Request.get_cert_files
Gem::Request::ConnectionPools.new(proxy_uri, certs).pool_for(uri)
end
BASE64_URI_TRANSLATE = { "+" => "%2B", "/" => "%2F", "=" => "%3D", "\n" => "" }.freeze
EC2_IAM_INFO = "http://169.254.169.254/latest/meta-data/iam/info".freeze
EC2_IAM_SECURITY_CREDENTIALS = "http://169.254.169.254/latest/meta-data/iam/security-credentials/".freeze
end
|