aboutsummaryrefslogtreecommitdiffstats
path: root/lib/rubygems/s3_uri_signer.rb
diff options
context:
space:
mode:
authorAlexander Pakulov <apakulov@stripe.com>2019-07-03 16:35:59 -0700
committerHiroshi SHIBATA <hsbt@ruby-lang.org>2019-07-31 07:47:01 +0800
commit4a8c2dd4192517fa030b6585ae82bf925c80516a (patch)
tree27d52fd4bc55eca0714198406e730015c69f0100 /lib/rubygems/s3_uri_signer.rb
parent1dfe132aa5138a5664b353739fc2ad32d6cd68dd (diff)
downloadruby-4a8c2dd4192517fa030b6585ae82bf925c80516a.tar.gz
[rubygems/rubygems] Extracting sign_s3_url & s3_source_auth into a separate S3URISigner class
https://github.com/rubygems/rubygems/commit/c30d21ec7a
Diffstat (limited to 'lib/rubygems/s3_uri_signer.rb')
-rw-r--r--lib/rubygems/s3_uri_signer.rb166
1 files changed, 166 insertions, 0 deletions
diff --git a/lib/rubygems/s3_uri_signer.rb b/lib/rubygems/s3_uri_signer.rb
new file mode 100644
index 0000000000..ff1f10e2bb
--- /dev/null
+++ b/lib/rubygems/s3_uri_signer.rb
@@ -0,0 +1,166 @@
+require 'base64'
+require 'digest'
+require 'openssl'
+require 'rubygems/request'
+require 'rubygems/request/connection_pools'
+
+##
+# 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 = nil)
+ 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"
+
+ 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
+ query_params = canonical_params.sort.to_h.map do |key, value|
+ "#{base64_uri_escape(key)}=#{base64_uri_escape(value)}"
+ end.join("&")
+
+ canonical_request = [
+ "GET",
+ uri.path,
+ query_params,
+ "host:#{canonical_host}",
+ "", # empty params
+ "host",
+ "UNSIGNED-PAYLOAD",
+ ].join("\n")
+
+ string_to_sign = [
+ "AWS4-HMAC-SHA256",
+ date_time,
+ credential_info,
+ Digest::SHA256.hexdigest(canonical_request)
+ ].join("\n")
+
+ 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")
+ signature = OpenSSL::HMAC.hexdigest("sha256", signing_key, 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
+
+ ##
+ # 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"
+ require "json"
+ credentials_response = ec2_metadata
+ credentials = JSON.parse(credentials_response)
+ id = credentials["AccessKeyId"]
+ secret = credentials["SecretAccessKey"]
+ security_token = credentials["Token"]
+ else
+ id = auth[:id] || auth["id"]
+ secret = auth[:secret] || auth["secret"]
+ raise ConfigurationError.new("s3_source for #{host} missing id or secret") unless id && secret
+
+ security_token = auth[:security_token] || auth["security_token"]
+ end
+
+ region = auth[:region] || auth["region"] || "us-east-1"
+ S3Config.new(id, secret, security_token, region)
+ end
+
+ def base64_uri_escape(str)
+ str.gsub("\n", "").gsub(/[\+\/=]/) { |c| BASE64_URI_TRANSLATE[c] }
+ end
+
+ def ec2_metadata
+ require 'net/http'
+
+ metadata_uri = URI(EC2_METADATA_CREDENTIALS)
+
+ @request_pool ||= create_request_pool(metadata_uri)
+ request = Gem::Request.new(metadata_uri, Net::HTTP::Get, nil, @request_pool)
+ response = request.fetch
+
+ case response
+ when Net::HTTPOK then
+ response.body
+ else
+ raise InstanceProfileError.new("Unable to fetch AWS credentials from #{metadata_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" }.freeze
+ EC2_METADATA_CREDENTIALS = "http://169.254.169.254/latest/meta-data/identity-credentials/ec2/security-credentials/ec2-instance".freeze
+
+end