diff --git a/lib/fog/aws/kms.rb b/lib/fog/aws/kms.rb index 3d176cbe61..8cc0106419 100644 --- a/lib/fog/aws/kms.rb +++ b/lib/fog/aws/kms.rb @@ -20,6 +20,9 @@ class KMS < Fog::Service request :list_keys request :create_key request :describe_key + request :get_public_key + request :schedule_key_deletion + request :sign model_path 'fog/aws/models/kms' model :key @@ -30,7 +33,8 @@ def self.data @data ||= Hash.new do |hash, region| hash[region] = Hash.new do |region_hash, access_key| region_hash[access_key] = { - :keys => {}, + keys: {}, + pkeys: {} } end end diff --git a/lib/fog/aws/parsers/kms/describe_key.rb b/lib/fog/aws/parsers/kms/describe_key.rb index d5597bc483..808a4e765a 100644 --- a/lib/fog/aws/parsers/kms/describe_key.rb +++ b/lib/fog/aws/parsers/kms/describe_key.rb @@ -17,9 +17,9 @@ def start_element(name, attrs = []) def end_element(name) case name - when 'KeyUsage', 'AWSAccountId', 'Description', 'KeyId', 'Arn' + when 'Arn', 'AWSAccountId', 'Description', 'KeyId', 'KeySpec', 'KeyState', 'KeyUsage' @key[name] = value - when 'CreationDate' + when 'CreationDate', 'DeletionDate' @key[name] = Time.parse(value) when 'Enabled' @key[name] = (value == 'true') diff --git a/lib/fog/aws/parsers/kms/get_public_key.rb b/lib/fog/aws/parsers/kms/get_public_key.rb new file mode 100644 index 0000000000..365f5fccea --- /dev/null +++ b/lib/fog/aws/parsers/kms/get_public_key.rb @@ -0,0 +1,30 @@ +module Fog + module Parsers + module AWS + module KMS + class GetPublicKey < Fog::Parsers::Base + def reset + @response = {} + end + + def start_element(name, attrs = []) + super + case name + when 'EncryptionAlgorithms', 'KeyAgreementAlgorithms', 'SigningAlgorithms' + @response[name] = [] + end + end + + def end_element(name) + case name + when 'KeyId', 'KeySpec', 'KeyUsage', 'PublicKey' + @response[name] = value + when 'EncryptionAlgorithms', 'KeyAgreementAlgorithms', 'SigningAlgorithms' + @response[name] << value + end + end + end + end + end + end +end diff --git a/lib/fog/aws/parsers/kms/schedule_key_deletion.rb b/lib/fog/aws/parsers/kms/schedule_key_deletion.rb new file mode 100644 index 0000000000..947c0a392f --- /dev/null +++ b/lib/fog/aws/parsers/kms/schedule_key_deletion.rb @@ -0,0 +1,28 @@ +module Fog + module Parsers + module AWS + module KMS + class ScheduleKeyDeletion < Fog::Parsers::Base + def reset + @response = {} + end + + def start_element(name, attrs = []) + super + end + + def end_element(name) + case name + when 'DeletionDate' + @response[name] = Time.parse(value) + when 'KeyId', 'KeyState' + @response[name] = value + when 'PendingWindowInDays' + @response[name] = value.to_i + end + end + end + end + end + end +end diff --git a/lib/fog/aws/parsers/kms/sign.rb b/lib/fog/aws/parsers/kms/sign.rb new file mode 100644 index 0000000000..3a52e8b76e --- /dev/null +++ b/lib/fog/aws/parsers/kms/sign.rb @@ -0,0 +1,24 @@ +module Fog + module Parsers + module AWS + module KMS + class Sign < Fog::Parsers::Base + def reset + @response = {} + end + + def start_element(name, attrs = []) + super + end + + def end_element(name) + case name + when 'KeyId', 'Signature', 'SigningAlgorithm' + @response[name] = value + end + end + end + end + end + end +end diff --git a/lib/fog/aws/requests/kms/create_key.rb b/lib/fog/aws/requests/kms/create_key.rb index eb712c53b3..2d7f16f999 100644 --- a/lib/fog/aws/requests/kms/create_key.rb +++ b/lib/fog/aws/requests/kms/create_key.rb @@ -2,61 +2,99 @@ module Fog module AWS class KMS class Real - DEFAULT_KEY_POLICY = <<-JSON -{ - "Version": "2012-10-17", - "Id": "key-default-1", - "Statement": [ - { - "Sid": "Enable IAM User Permissions", - "Effect": "Allow", - "Principal": { - "AWS": "arn:aws:iam::915445820265:root" - }, - "Action": "kms:*", - "Resource": "*" - } - ] -} - JSON - require 'fog/aws/parsers/kms/describe_key' - def create_key(policy = nil, description = nil, usage = "ENCRYPT_DECRYPT") - request( - 'Action' => 'CreateKey', - 'Description' => description, - 'KeyUsage' => usage, - 'Policy' => policy, - :parser => Fog::Parsers::AWS::KMS::DescribeKey.new - ) + # Create Key + # + # ==== Parameters + # * options<~Hash>: + # * 'Description'<~String>: + # * 'KeyUsage'<~String>: + # * 'Policy'<~String>: + # * ... (see docs from see also) + # + # === Returns + # + # ==== See Also + # https://docs.aws.amazon.com/kms/latest/APIReference/API_CreateKey.html + def create_key(*args) + options = Fog::AWS::KMS.parse_create_key_args(args) + request({ + 'Action' => 'CreateKey', + :parser => Fog::Parsers::AWS::KMS::DescribeKey.new + }.merge!(options)) end end class Mock - def create_key(policy = nil, description = nil, usage = "ENCRYPT_DECRYPT") + def create_key(*args) + options = Fog::AWS::KMS.parse_create_key_args(args) + response = Excon::Response.new key_id = UUID.uuid key_arn = Fog::AWS::Mock.arn("kms", self.account_id, "key/#{key_id}", @region) key = { - "KeyUsage" => usage, - "AWSAccountId" => self.account_id, - "KeyId" => key_id, - "Description" => description, - "CreationDate" => Time.now, - "Arn" => key_arn, - "Enabled" => true, - } + 'Arn' => key_arn, + 'AWSAccountId' => self.account_id, + 'CreationDate' => Time.now.utc, + 'DeletionDate' => nil, + 'Description' => nil, + 'Enabled' => true, + 'KeyId' => key_id, + 'KeySpec' => 'SYMMETRIC_DEFAULT', + 'KeyState' => 'Enabled', + 'KeyUsage' => 'ENCRYPT_DECRYPT', + 'Policy' => nil + }.merge!(options) # @todo use default policy self.data[:keys][key_id] = key - response.body = { "KeyMetadata" => key } + klass, arg = { + 'ECC_NIST_P256' => [OpenSSL::PKey::EC, 'prime256v1'], + 'ECC_NIST_P384' => [OpenSSL::PKey::EC, 'secp384r1'], + 'ECC_NIST_P521' => [OpenSSL::PKey::EC, 'secp521r1'], + 'ECC_SECG_P256K1' => [OpenSSL::PKey::EC, 'secp256k1'], + 'RSA_2048' => [OpenSSL::PKey::RSA, 2048], + 'RSA_3072' => [OpenSSL::PKey::RSA, 3072], + 'RSA_4096' => [OpenSSL::PKey::RSA, 4096] + }[key['KeySpec']] + raise "Unknown or not-yet-implemented #{key['KeySpec']} KeySpec for kms create_key mocks" unless klass + + self.data[:pkeys][key_id] = klass.generate(arg) + + response.body = { 'KeyMetadata' => key } response end end + + # previous args (policy, description, usage) was deprecated in favor of a hash of options + def self.parse_create_key_args(args) + case args.size + when 0 + {} + when 1 + if args[0].is_a?(Hash) + args[0] + else + Fog::Logger.deprecation("create_key with distinct arguments is deprecated, use options hash instead [light_black](#{caller.first})[/]") + { + 'Policy' => args[0] + } + end + when 2, 3 + Fog::Logger.deprecation("create_key with distinct arguments is deprecated, use options hash instead [light_black](#{caller.first})[/]") + { + 'Policy' => args[0], + 'Description' => args[1], + 'KeyUsage' => args[2] || 'ENCRYPT_DECRYPT' + } + else + raise "Unknown argument style: #{args.inspect}, use options hash instead." + end + end end end end diff --git a/lib/fog/aws/requests/kms/get_public_key.rb b/lib/fog/aws/requests/kms/get_public_key.rb new file mode 100644 index 0000000000..2259206344 --- /dev/null +++ b/lib/fog/aws/requests/kms/get_public_key.rb @@ -0,0 +1,35 @@ +module Fog + module AWS + class KMS + class Real + require 'fog/aws/parsers/kms/get_public_key' + + def get_public_key(identifier, grant_tokens = nil) + request( + 'Action' => 'GetPublicKey', + 'GrantTokens' => grant_tokens, + 'KeyId' => identifier, + :parser => Fog::Parsers::AWS::KMS::GetPublicKey.new + ) + end + end + + class Mock + def get_public_key(identifier, _grant_tokens = []) + response = Excon::Response.new + key = self.data[:keys][identifier] + pkey = self.data[:pkeys][identifier] + + response.body = { + 'KeyId' => key['Arn'], + 'KeyUsage' => key['KeyUsage'], + 'KeySpec' => key['KeySpec'], + 'PublicKey' => Base64.strict_encode64(pkey.public_to_der), + 'SigningAlgorithms' => key['SigningAlgorithms'] + } + response + end + end + end + end +end diff --git a/lib/fog/aws/requests/kms/list_keys.rb b/lib/fog/aws/requests/kms/list_keys.rb index d2415ad534..20cc10e6ee 100644 --- a/lib/fog/aws/requests/kms/list_keys.rb +++ b/lib/fog/aws/requests/kms/list_keys.rb @@ -2,7 +2,6 @@ module Fog module AWS class KMS class Real - require 'fog/aws/parsers/kms/list_keys' def list_keys(options={}) @@ -43,9 +42,9 @@ def list_keys(options={}) key_set = if marker self.data[:markers][marker] || [] else - self.data[:keys].inject([]) { |r,(k,v)| - r << { "KeyId" => k, "KeyArn" => v["Arn"] } - } + self.data[:keys].inject([]) do |r, (k, v)| + r << { 'KeyArn' => v['Arn'], 'KeyId' => k } + end end keys = if limit diff --git a/lib/fog/aws/requests/kms/schedule_key_deletion.rb b/lib/fog/aws/requests/kms/schedule_key_deletion.rb new file mode 100644 index 0000000000..f03fa7803f --- /dev/null +++ b/lib/fog/aws/requests/kms/schedule_key_deletion.rb @@ -0,0 +1,37 @@ +module Fog + module AWS + class KMS + class Real + require 'fog/aws/parsers/kms/schedule_key_deletion' + + def schedule_key_deletion(identifier, pending_window_in_days) + request( + 'Action' => 'ScheduleKeyDeletion', + 'KeyId' => identifier, + 'PendingWindowInDays' => pending_window_in_days, + :parser => Fog::Parsers::AWS::KMS::ScheduleKeyDeletion.new + ) + end + end + + class Mock + def schedule_key_deletion(identifier, pending_window_in_days) + response = Excon::Response.new + key = self.data[:keys][identifier] + + key['DeletionDate'] = Time.now + (60 * 60 * 24 * pending_window_in_days) + key['Enabled'] = false + key['KeyState'] = 'PendingDeletion' + + response.body = { + 'DeletionDate' => key['DeletionDate'], + 'KeyId' => key['KeyId'], + 'KeyState' => key['KeyState'], + 'PendingWindowInDays' => pending_window_in_days + } + response + end + end + end + end +end diff --git a/lib/fog/aws/requests/kms/sign.rb b/lib/fog/aws/requests/kms/sign.rb new file mode 100644 index 0000000000..7d7826d231 --- /dev/null +++ b/lib/fog/aws/requests/kms/sign.rb @@ -0,0 +1,62 @@ +module Fog + module AWS + class KMS + class Real + require 'fog/aws/parsers/kms/sign' + + # Sign + # + # ==== Parameters + # * identifier<~String>: id, arn, alias name, or alias arn for key to sign with + # * message<~String>: base64 encoded message to sign + # + # === Returns + # * response<~Excon::Response>: + # + # ==== See Also + # https://docs.aws.amazon.com/kms/latest/APIReference/API_Sign.html + # + def sign(identifier, message, algorithm, options = {}) + request({ + 'Action' => 'Sign', + 'KeyId' => identifier, + 'Message' => message, + 'SigningAlgorithm' => algorithm, + :parser => Fog::Parsers::AWS::KMS::Sign.new + }.merge!(options)) + end + end + + class Mock + def sign(identifier, message, algorithm, options = {}) + response = Excon::Response.new + pkey = self.data[:pkeys][identifier] + unless pkey + response.status = 404 + raise(Excon::Errors.status_error({ expects: 200 }, response)) + end + + data = Base64.decode64(message) + + # FIXME: SM2 support? + sha = "SHA#{algorithm.split('_SHA_').last}" + signopts = {} + signopts[:rsa_padding_mode] = 'pss' if algorithm.start_with?('RSASSA_PSS') + + signature = if options['MessageType'] == 'DIGEST' + pkey.sign_raw(sha, data, signopts) + else + pkey.sign(sha, data, signopts) + end + + response.body = { + 'KeyId' => identifier, + 'Signature' => Base64.strict_encode64(signature), + 'SigningAlgorithm' => algorithm + } + response + end + end + end + end +end diff --git a/tests/requests/kms/helper.rb b/tests/requests/kms/helper.rb index 9c3d97c733..5e27a6b418 100644 --- a/tests/requests/kms/helper.rb +++ b/tests/requests/kms/helper.rb @@ -3,25 +3,51 @@ module KMS module Formats BASIC = { 'ResponseMetadata' => { 'RequestId' => String } - } + }.freeze DESCRIBE_KEY = { - "KeyMetadata" => { - "KeyUsage" => String, - "AWSAccountId" => String, - "KeyId" => String, - "Description" => Fog::Nullable::String, - "CreationDate" => Time, - "Arn" => String, - "Enabled" => Fog::Boolean + 'KeyMetadata' => { + 'Arn' => String, + 'AWSAccountId' => String, + 'CreationDate' => Time, + 'DeletionDate' => Fog::Nullable::Time, + 'Description' => Fog::Nullable::String, + 'Enabled' => Fog::Boolean, + 'KeyId' => String, + 'KeySpec' => String, + 'KeyState' => String, + 'KeyUsage' => String } - } + }.freeze + + GET_PUBLIC_KEY = { + 'EncryptionAlgorithms' => Fog::Nullable::Array, + 'KeyAgreementAlgorithms' => Fog::Nullable::Array, + 'KeyId' => String, + 'KeySpec' => String, + 'KeyUsage' => String, + 'PublicKey' => String, + 'SigningAlgorithms' => Fog::Nullable::Array + }.freeze LIST_KEYS = { - "Keys" => [{ "KeyId" => String, "KeyArn" => String }], - "Truncated" => Fog::Boolean, - "Marker" => Fog::Nullable::String - } + 'Keys' => [{ 'KeyArn' => String, 'KeyId' => String }], + 'Marker' => Fog::Nullable::String, + 'Truncated' => Fog::Boolean + }.freeze + + SCHEDULE_KEY_DELETION = { + 'DeletionDate' => Time, + 'KeyId' => String, + 'KeyState' => String, + 'PendingWindowInDays' => Integer + }.freeze + + SIGN = { + 'KeyId' => String, + 'Signature' => String, + 'SigningAlgorithm' => String + }.freeze end end end diff --git a/tests/requests/kms/key_tests.rb b/tests/requests/kms/key_tests.rb index b65f162cba..a89b7f6d6d 100644 --- a/tests/requests/kms/key_tests.rb +++ b/tests/requests/kms/key_tests.rb @@ -1,23 +1,139 @@ -Shindo.tests('AWS::KMS | key requests', ['aws', 'kms']) do +KEY_SPECS = %w[RSA_2048 RSA_3072 RSA_4096 ECC_NIST_P256 ECC_NIST_P384 ECC_NIST_P521 ECC_SECG_P256K1].freeze +SIGNING_ALGORITHMS = %w[RSASSA_PSS_SHA_256 RSASSA_PSS_SHA_384 RSASSA_PSS_SHA_512 RSASSA_PKCS1_V1_5_SHA_256 RSASSA_PKCS1_V1_5_SHA_384 RSASSA_PKCS1_V1_5_SHA_512 ECDSA_SHA_256 ECDSA_SHA_384 ECDSA_SHA_512] + +Shindo.tests('AWS::KMS | key requests', %w[aws kms]) do key_id = nil + key_arn = nil + pkey = nil + data = 'sign me' - tests('success') do - tests("#create_key").data_matches_schema(AWS::KMS::Formats::DESCRIBE_KEY) do - result = Fog::AWS[:kms].create_key.body - key_id = result["KeyMetadata"]["KeyId"] + tests('#create_key').data_matches_schema(AWS::KMS::Formats::DESCRIBE_KEY) do + result = Fog::AWS[:kms].create_key( + 'KeySpec' => 'RSA_2048', + 'KeyUsage' => 'SIGN_VERIFY' + ).body + key_id = result['KeyMetadata']['KeyId'] + key_arn = result['KeyMetadata']['Arn'] - result - end + result end - tests("#describe_key").data_matches_schema(AWS::KMS::Formats::DESCRIBE_KEY) do + tests('#describe_key') do result = Fog::AWS[:kms].describe_key(key_id).body - returns(key_id) { result['KeyMetadata']['KeyId'] } - result + + tests('format').data_matches_schema(AWS::KMS::Formats::DESCRIBE_KEY) { result } + + tests('result_contains correct key_id').returns(key_id) { result['KeyMetadata']['KeyId'] } + end + + tests('#get_public_key') do + result = Fog::AWS[:kms].get_public_key(key_id).body + public_key = Base64.decode64(result['PublicKey']) + pkey = OpenSSL::PKey::RSA.new(public_key) + + tests('format').data_matches_schema(AWS::KMS::Formats::GET_PUBLIC_KEY) { result } + + tests('result contains correct key_id (arn)').returns(key_arn) { result['KeyId'] } + end + + tests('#list_keys') do + result = Fog::AWS[:kms].list_keys.body + + tests('format').data_matches_schema(AWS::KMS::Formats::LIST_KEYS) { result } + + tests('result contains correct key_id').returns(true) { result['Keys'].map { |k| k['KeyId'] }.include?(key_id) } + end + + tests('#sign') do + tests('DIGEST') do + hash = OpenSSL::Digest.digest('SHA256', data) + sign_response = Fog::AWS[:kms].sign( + key_id, + Base64.encode64(hash), + 'RSASSA_PKCS1_V1_5_SHA_256', + 'MessageType' => 'DIGEST' + ).body + + tests('format').data_matches_schema(AWS::KMS::Formats::SIGN) { sign_response } + + tests('#verify').returns(true) do + signature = Base64.decode64(sign_response['Signature']) + pkey.verify_raw('SHA256', signature, hash) + end + end + + tests('RAW') do + sign_response = Fog::AWS[:kms].sign( + key_id, + Base64.encode64(data), + 'RSASSA_PKCS1_V1_5_SHA_256', + 'MessageType' => 'RAW' + ).body + + tests('format').data_matches_schema(AWS::KMS::Formats::SIGN) { sign_response } + + tests('#verify').returns(true) do + signature = Base64.decode64(sign_response['Signature']) + pkey.verify('SHA256', signature, data) + end + end end - tests("#list_keys").data_matches_schema(AWS::KMS::Formats::LIST_KEYS) do - Fog::AWS[:kms].list_keys.body + tests('#schedule_key_deletion').data_matches_schema(AWS::KMS::Formats::SCHEDULE_KEY_DELETION) do + Fog::AWS[:kms].schedule_key_deletion(key_id, 7).body end + tests('mock sign') do + pending unless Fog.mock? + + KEY_SPECS.each do |key_spec| + SIGNING_ALGORITHMS.select { |sa| sa.start_with?(key_spec[0...2]) }.each do |signing_algorithm| + key_id = Fog::AWS[:kms].create_key( + 'KeySpec' => key_spec, + 'KeyUsage' => 'SIGN_VERIFY' + ).body['KeyMetadata']['KeyId'] + + result = Fog::AWS[:kms].get_public_key(key_id).body + public_key = Base64.decode64(result['PublicKey']) + pkey = if key_spec.start_with?('RSA') + OpenSSL::PKey::RSA.new(public_key) + elsif key_spec.start_with?('EC') + OpenSSL::PKey::EC.new(public_key) + end + sha = "SHA#{signing_algorithm.split('_SHA_').last}" + sign_opts = if signing_algorithm.include?('_PSS_') + { rsa_padding_mode: 'pss' } + else + {} + end + + tests("#sign #{key_spec} #{signing_algorithm} DIGEST").returns(true) do + hash = OpenSSL::Digest.digest(sha, data) + sign_response = Fog::AWS[:kms].sign( + key_id, + Base64.encode64(hash), + signing_algorithm, + 'MessageType' => 'DIGEST' + ).body + signature = Base64.decode64(sign_response['Signature']) + + pkey.verify_raw(sha, signature, hash, sign_opts) + end + + tests("#sign #{key_spec} #{signing_algorithm} RAW").returns(true) do + sign_response = Fog::AWS[:kms].sign( + key_id, + Base64.encode64(data), + signing_algorithm, + 'MessageType' => 'RAW' + ).body + signature = Base64.decode64(sign_response['Signature']) + + pkey.verify(sha, signature, data, sign_opts) + end + + Fog::AWS[:kms].schedule_key_deletion(key_id, 7) + end + end + end end