alexa_skills_ruby 0.0.7 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1b4eda332ab8e4a78464b23a3dfc9307ac1cac0c
4
- data.tar.gz: 7f48f5b4bbb293f6472f252a70250c13cb1075ad
3
+ metadata.gz: 51b36b59de95199d06f652746412db9e4c079f93
4
+ data.tar.gz: 228715baaf0c2b52a3b5686c342b463fb7a69773
5
5
  SHA512:
6
- metadata.gz: cb5d5637a8c8c98678e30f8410482f4ca9d00c276ef1b8dabba34401dbca5286d907219f39b2cffeff8610584716080fdcb285f266adb3f203dac3b4336d0b16
7
- data.tar.gz: fb16ac0c815dde299d5baa140ba1b9db8967ff8f6abd63efc1569c4aca0b350e3ea163c1d9d9cdb784397bbaf3520794c2b16a2e605bf33185f70a1a653bfe6c
6
+ metadata.gz: f24e5ef81403c726452728d74e0f66015f182244e78dcb8564a0d934d922fc06afea6c097a45fb8a21d5e248eacf26fc0d71f4c173dc3dfc4af0b103d752cc57
7
+ data.tar.gz: 50a2b116aa410629118124a902b5e6ea9164212f54d2f04ebd261e7f05494f2c93c86bae7945d4260e4b116c33a621e599ac88195078281575d0e22777586866
@@ -0,0 +1 @@
1
+ ruby-2.3.1
data/README.md CHANGED
@@ -8,6 +8,7 @@ register event handlers.
8
8
 
9
9
  The following handlers are available:
10
10
 
11
+ * `on_verify_signature` - called before checking message certificate and signature
11
12
  * `on_authenticate` - called before checking the ApplicationID
12
13
  * `on_session_start` - called first if the request is flagged as a new session
13
14
  * `on_launch` - called for a LaunchRequest
@@ -26,6 +27,12 @@ In event handlers, the following methods are available:
26
27
  The `AlexaSkillsRuby::Handler` constructor takes an options hash and processes the following keys:
27
28
  * `application_id` - If set, will raise a `AlexaSkillsRuby::InvalidApplicationId` if a request's application_id does not match
28
29
  * `logger` - Will be available through the `logger` method in the handler; not otherwise used by the base class
30
+ * `skip_signature_validation` - If true, skips any message signature or certificate validation
31
+ * `certificate_cache` - Optional key that allows use of an external cache for Amazon's certificate. Must be an instance of an object that has the same method definitions as `AlexaSkillsRuby::SimpleCertificateCache`
32
+ * `root_certificates` - If your CA certificates are not accessible to Ruby by default, you may pass a list of either filenames or OpenSSL::X509::Certificate objects for use in validating Amazon's certificate.
33
+
34
+ The `AlexaSkillsRuby::Handler#handle` method takes 2 arguments: a string containing the body of the request, and a hash of HTTP headers. If using
35
+ signature validation, the headers hash _must_ contain the Signature and SignatureCertChainUrl HTTP headers.
29
36
 
30
37
  ## Example Sinatra App Using this Library
31
38
 
@@ -50,8 +57,9 @@ post '/' do
50
57
  handler = CustomHandler.new(application_id: ENV['APPLICATION_ID'], logger: logger)
51
58
 
52
59
  begin
53
- handler.handle(request.body.read)
54
- rescue AlexaSkillsRuby::InvalidApplicationId => e
60
+ hdrs = { 'Signature' => request.env['HTTP_SIGNATURE'], 'SignatureCertChainUrl' => request.env['HTTP_SIGNATURECERTCHAINURL'] }
61
+ handler.handle(request.body.read, hdrs)
62
+ rescue AlexaSkillsRuby::Error => e
55
63
  logger.error e.to_s
56
64
  403
57
65
  end
data/Rakefile CHANGED
@@ -4,4 +4,9 @@ require 'rspec/core/rake_task'
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
6
  task :test => :spec
7
- task :default => :spec
7
+ task :default => :spec
8
+
9
+ desc "Open an irb session preloaded with this library"
10
+ task :console do
11
+ sh "irb -rubygems -I lib -r alexa_skills_ruby.rb"
12
+ end
@@ -18,9 +18,11 @@ Gem::Specification.new do |spec|
18
18
 
19
19
  spec.add_runtime_dependency 'activesupport', '>= 4.2'
20
20
  spec.add_runtime_dependency "multi_json", "~> 1.0"
21
+ spec.add_runtime_dependency 'addressable', '~> 2.5'
21
22
 
22
23
  spec.add_development_dependency "bundler"
23
24
  spec.add_development_dependency "rake"
24
- spec.add_development_dependency "rspec", "~> 3.4.0"
25
- spec.add_development_dependency "oj", "~> 2.10.2"
25
+ spec.add_development_dependency "rspec", "~> 3.5"
26
+ spec.add_development_dependency "oj", "~> 2.10"
27
+ spec.add_development_dependency "webmock", "~> 2.3"
26
28
  end
@@ -1,6 +1,10 @@
1
1
  require 'active_support/core_ext/class/attribute'
2
2
  require 'active_support/callbacks'
3
+ require 'addressable/uri'
4
+ require 'base64'
3
5
  require 'multi_json'
6
+ require 'net/http'
7
+ require 'openssl'
4
8
 
5
9
  require 'alexa_skills_ruby/version'
6
10
  require 'alexa_skills_ruby/json_object'
@@ -20,6 +24,9 @@ require 'alexa_skills_ruby/json_objects/response'
20
24
  require 'alexa_skills_ruby/json_objects/skills_request'
21
25
  require 'alexa_skills_ruby/json_objects/skills_response'
22
26
  require 'alexa_skills_ruby/errors'
27
+ require 'alexa_skills_ruby/certificate_validator'
28
+ require 'alexa_skills_ruby/simple_certificate_cache'
29
+ require 'alexa_skills_ruby/signature_validator'
23
30
  require 'alexa_skills_ruby/handler'
24
31
 
25
32
  module AlexaSkillsRuby
@@ -0,0 +1,65 @@
1
+ module AlexaSkillsRuby
2
+ class CertificateValidator
3
+
4
+ def initialize(extra_cas = [])
5
+ @store = OpenSSL::X509::Store.new.tap { |store| store.set_default_paths }
6
+ extra_cas.each do |ca|
7
+ case ca
8
+ when String
9
+ @store.add_file(ca)
10
+ when OpenSSL::X509::Certificate
11
+ @store.add_cert(ca)
12
+ else
13
+ raise AlexaSkillsRuby::ConfigurationError, 'root_certificates config option must contain only filenames as strings or OpenSSL::X509::Certificate objects'
14
+ end
15
+ end
16
+ end
17
+
18
+ def get_signing_certificate(pem_data)
19
+ chain = chain_certs(get_certs(pem_data))
20
+ chain[0...-1].each do |c|
21
+ if @store.verify(c)
22
+ @store.add_cert(c)
23
+ end
24
+ end
25
+
26
+ if @store.verify(chain.last)
27
+ chain.last
28
+ else
29
+ nil
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def get_certs(pem_data)
36
+ pem_data.scan(/-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----\n?/m).map do |pem|
37
+ OpenSSL::X509::Certificate.new(pem)
38
+ end
39
+ end
40
+
41
+ def chain_certs(certs)
42
+ certs = certs.dup
43
+ failed = false
44
+ chain = [certs.pop]
45
+
46
+ while certs.length > 0 && !failed
47
+ failed = true
48
+
49
+ certs.each do |c|
50
+ if c.subject == chain.first.issuer
51
+ failed = false
52
+ chain.unshift(c)
53
+ elsif c.issuer == chain.last.subject
54
+ failed = false
55
+ chain << c
56
+ end
57
+ end
58
+ end
59
+
60
+ chain
61
+
62
+ end
63
+
64
+ end
65
+ end
@@ -1,4 +1,17 @@
1
1
  module AlexaSkillsRuby
2
- class InvalidApplicationId < StandardError
2
+
3
+ class Error < StandardError
4
+ end
5
+
6
+ class InvalidApplicationId < Error
7
+ end
8
+
9
+ class ConfigurationError < Error
10
+ end
11
+
12
+ class SignatureValidationError < Error
13
+ end
14
+
15
+ class TimestampValidationError < Error
3
16
  end
4
17
  end
@@ -1,10 +1,10 @@
1
1
  module AlexaSkillsRuby
2
2
  class Handler
3
3
  include ActiveSupport::Callbacks
4
- define_callbacks :authenticate, :session_start, :launch, :intent, :session_end
4
+ define_callbacks :verify_signature, :authenticate, :session_start, :launch, :intent, :session_end
5
5
 
6
6
  attr_reader :request, :session, :response
7
- attr_accessor :application_id, :logger
7
+ attr_accessor :application_id, :logger, :skip_signature_validation
8
8
 
9
9
  def initialize(opts = {})
10
10
  if opts[:application_id]
@@ -14,13 +14,21 @@ module AlexaSkillsRuby
14
14
  if opts[:logger]
15
15
  @logger = opts[:logger]
16
16
  end
17
+
18
+ certificate_cache = opts[:certificate_cache] || SimpleCertificateCache.new
19
+ @skip_signature_validation = !!opts[:skip_signature_validation]
20
+ @signature_validator = SignatureValidator.new(certificate_cache)
21
+
22
+ if opts[:root_certificates]
23
+ @signature_validator.add_certificate_authorities([opts[:root_certificates]].flatten)
24
+ end
17
25
  end
18
26
 
19
27
  def session_attributes
20
28
  @session.attributes ||= {}
21
29
  end
22
30
 
23
- def handle(request_json)
31
+ def handle(request_json, request_headers = {})
24
32
  @skill_request = JsonObjects::SkillsRequest.new(MultiJson.load(request_json))
25
33
  @skill_response = JsonObjects::SkillsResponse.new
26
34
 
@@ -28,6 +36,19 @@ module AlexaSkillsRuby
28
36
  @request = @skill_request.request
29
37
  @response = @skill_response.response
30
38
 
39
+ run_callbacks :verify_signature do
40
+ unless @skip_signature_validation
41
+ cert_chain_url = request_headers['SignatureCertChainUrl'].to_s.strip
42
+ signature = request_headers['Signature'].to_s.strip
43
+ if cert_chain_url.empty? || signature.empty?
44
+ raise AlexaSkillsRuby::ConfigurationError, 'Missing "SignatureCertChainUrl" or "Signature" header but signature validation is enabled'
45
+ end
46
+ @signature_validator.validate(request_json, cert_chain_url, signature)
47
+ end
48
+ end
49
+
50
+ timestamp_diff = (Time.now - Time.iso8601(@request.timestamp)).abs
51
+ raise TimestampValidationError, "Invalid timstamp" if timestamp_diff > 150
31
52
 
32
53
  run_callbacks :authenticate do
33
54
  if @application_id
@@ -59,6 +80,10 @@ module AlexaSkillsRuby
59
80
  MultiJson.dump(@skill_response.as_json)
60
81
  end
61
82
 
83
+ def self.on_verify_signature(&block)
84
+ set_callback :verify_signature, :before, block
85
+ end
86
+
62
87
  def self.on_authenticate(&block)
63
88
  set_callback :authenticate, :before, block
64
89
  end
@@ -0,0 +1,109 @@
1
+ module AlexaSkillsRuby
2
+ class SignatureValidator
3
+
4
+ def initialize(certificate_cache)
5
+ @certificate_cache = certificate_cache
6
+ @extra_cas = []
7
+ end
8
+
9
+ def validate(body, signature_cert_chain_url, signature)
10
+
11
+ cert_uri = Addressable::URI.parse(signature_cert_chain_url).normalize
12
+
13
+ raise SignatureValidationError, "Invalid signature URL: [#{cert_uri.to_s}]" unless valid_cert_uri?(cert_uri)
14
+
15
+ pem_data = @certificate_cache.get(cert_uri.to_s) || fetch_data(cert_uri.to_s)
16
+ validator = CertificateValidator.new(@extra_cas)
17
+ cert = validator.get_signing_certificate(pem_data)
18
+
19
+ raise SignatureValidationError, "Invalid certificate" unless cert
20
+
21
+ @certificate_cache.set(cert_uri.to_s, pem_data)
22
+
23
+ public_key = cert.public_key
24
+ signature = Base64.decode64(signature)
25
+ unless public_key.verify(OpenSSL::Digest::SHA1.new, signature, body)
26
+ raise SignatureValidationError, "Signature is invalid"
27
+ end
28
+ end
29
+
30
+ def add_certificate_authorities(certs)
31
+ @extra_cas = certs
32
+ end
33
+
34
+ private
35
+
36
+ def get_pem_from_cache(url)
37
+ pem_data = @certificate_cache.get(url)
38
+ if pem_data
39
+ cert = OpenSSL::X509::Certificate.new(cert_data)
40
+ if valid_cert?(cert)
41
+ cert
42
+ else
43
+ nil
44
+ end
45
+ else
46
+ nil
47
+ end
48
+ end
49
+
50
+ def valid_cert_uri?(uri)
51
+ case
52
+ when uri.scheme != 'https'
53
+ false
54
+ when uri.host != 's3.amazonaws.com'
55
+ false
56
+ when ![nil, 443].include?(uri.port)
57
+ false
58
+ when uri.path !~ /^\/echo.api\//
59
+ false
60
+ else
61
+ true
62
+ end
63
+ end
64
+
65
+ def valid_cert?(cert)
66
+ cert_errors(cert).empty?
67
+ end
68
+
69
+ def cert_errors(cert)
70
+ errors = []
71
+ unless certificate_store.verify(cert)
72
+ errors << 'Unable to verify identity'
73
+ end
74
+
75
+ unless (cert.not_before..cert.not_after).include?(Time.now)
76
+ errors << 'Invalid for current date'
77
+ end
78
+
79
+ errors
80
+ end
81
+
82
+ def certificate_valid?(cert)
83
+ certificate_store.verify(cert)
84
+ end
85
+
86
+ def certificate_store
87
+ @store ||= OpenSSL::X509::Store.new.tap do |store|
88
+ store.set_default_paths
89
+ end
90
+ end
91
+
92
+ def fetch_data(uri_str, limit = 10)
93
+ raise SignatureValidationError, "too many HTTP redirects while fetching #{uri_str}" if limit == 0
94
+
95
+ response = Net::HTTP.get_response(URI(uri_str))
96
+
97
+ case response
98
+ when Net::HTTPSuccess then
99
+ response.body
100
+ when Net::HTTPRedirection then
101
+ location = response['location']
102
+ fetch(location, limit - 1)
103
+ else
104
+ response.value
105
+ end
106
+ end
107
+
108
+ end
109
+ end
@@ -0,0 +1,17 @@
1
+ module AlexaSkillsRuby
2
+ class SimpleCertificateCache
3
+
4
+ def initialize
5
+ @cache = {}
6
+ end
7
+
8
+ def get(url)
9
+ @cache[url]
10
+ end
11
+
12
+ def set(url, value)
13
+ @cache[url] = value
14
+ end
15
+
16
+ end
17
+ end
@@ -1,3 +1,3 @@
1
1
  module AlexaSkillsRuby
2
- VERSION = '0.0.7'
2
+ VERSION = '1.0.0'
3
3
  end
@@ -2,6 +2,8 @@ require 'rubygems'
2
2
  require 'bundler/setup'
3
3
  require 'oj'
4
4
  require 'rspec'
5
+ require 'time'
6
+ require 'webmock/rspec'
5
7
  require File.expand_path('../../lib/alexa_skills_ruby', __FILE__)
6
8
 
7
9
  MultiJson.use :oj
@@ -8,4 +8,14 @@ module FixtureSupport
8
8
  Oj.load(load_json(name))
9
9
  end
10
10
 
11
+ def add_timestamp(json)
12
+ json['request']['timestamp'] = Time.now.iso8601
13
+ end
14
+
15
+ def load_example_json(name)
16
+ exp = load_fixture(name)
17
+ add_timestamp(exp)
18
+ Oj.dump(exp)
19
+ end
20
+
11
21
  end
@@ -0,0 +1,75 @@
1
+ RSpec.shared_context('x509', { x509: true }) do
2
+
3
+ let(:root_key) { OpenSSL::PKey::RSA.new 512 }
4
+ let(:root_ca) { build_root_ca(root_key) }
5
+ let(:signing_key) { OpenSSL::PKey::RSA.new 512 }
6
+ let(:signing_cert) { build_signing_cert(root_ca, signing_key) }
7
+
8
+ let(:certificate_url) { 'https://s3.amazonaws.com/echo.api/echo-api-cert.pem' }
9
+
10
+ before do
11
+ stub_request(:get, certificate_url).to_return { |_| { status: 200, body: signing_cert.to_s } }
12
+ end
13
+
14
+ def build_headers(body)
15
+ {
16
+ 'SignatureCertChainUrl' => certificate_url,
17
+ 'Signature' => Base64.encode64(get_signature(body))
18
+ }
19
+ end
20
+
21
+ def get_signature(body)
22
+ signing_key.sign(OpenSSL::Digest::SHA1.new, body)
23
+ end
24
+
25
+ def get_serial
26
+ @serial_counter ||= 0
27
+ @serial_counter += 1
28
+ end
29
+
30
+ def build_root_ca(key)
31
+ root_ca = OpenSSL::X509::Certificate.new
32
+ root_ca.version = 2 # cf. RFC 5280 - to make it a "v3" certificate
33
+ root_ca.serial = get_serial
34
+ root_ca.subject = OpenSSL::X509::Name.parse "/DC=org/DC=umn/CN=mpc-root"
35
+ root_ca.issuer = root_ca.subject # root CA's are "self-signed"
36
+ root_ca.public_key = key.public_key
37
+ root_ca.not_before = Time.now
38
+ root_ca.not_after = root_ca.not_before + 2 * 365 * 24 * 60 * 60 # 2 years validity
39
+ ef = OpenSSL::X509::ExtensionFactory.new
40
+ ef.subject_certificate = root_ca
41
+ ef.issuer_certificate = root_ca
42
+ root_ca.add_extension(ef.create_extension("basicConstraints","CA:TRUE",true))
43
+ root_ca.add_extension(ef.create_extension("keyUsage","keyCertSign, cRLSign", true))
44
+ root_ca.add_extension(ef.create_extension("subjectKeyIdentifier","hash",false))
45
+ root_ca.add_extension(ef.create_extension("authorityKeyIdentifier","keyid:always",false))
46
+ root_ca.sign(key, OpenSSL::Digest::SHA256.new)
47
+
48
+ root_ca
49
+ end
50
+
51
+ def build_signing_cert(root_ca, signing_key, valid = true)
52
+ cert = OpenSSL::X509::Certificate.new
53
+ cert.version = 2
54
+ cert.serial = get_serial
55
+ cert.subject = OpenSSL::X509::Name.parse "/DC=org/DC=umn/CN=mpc-root-#{get_serial}"
56
+ cert.issuer = root_ca.subject # root CA is the issuer
57
+ cert.public_key = signing_key.public_key
58
+ if valid
59
+ cert.not_before = Time.now
60
+ else
61
+ cert.not_before = Time.now + 500
62
+ end
63
+ cert.not_after = cert.not_before + 1 * 365 * 24 * 60 * 60 # 1 years validity
64
+ ef = OpenSSL::X509::ExtensionFactory.new
65
+ ef.subject_certificate = cert
66
+ ef.issuer_certificate = root_ca
67
+ cert.add_extension(ef.create_extension("keyUsage","digitalSignature", true))
68
+ cert.add_extension(ef.create_extension("subjectKeyIdentifier","hash",false))
69
+ cert.add_extension(ef.create_extension("subjectAltName","DNS:echo-api.amazon.com",false))
70
+ cert.sign(root_key, OpenSSL::Digest::SHA256.new)
71
+
72
+ cert
73
+ end
74
+
75
+ end
@@ -1,13 +1,14 @@
1
1
  require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
2
 
3
- describe AlexaSkillsRuby::Handler do
3
+ describe AlexaSkillsRuby::Handler, x509: true do
4
4
 
5
5
  class TestHandler < AlexaSkillsRuby::Handler
6
6
 
7
- attr_reader :auths, :intents, :launch, :ends, :starts
7
+ attr_reader :verifies, :auths, :intents, :launch, :ends, :starts
8
8
 
9
9
  def initialize(*args)
10
10
  super
11
+ @verifies = []
11
12
  @auths = []
12
13
  @intents = []
13
14
  @launch = []
@@ -15,6 +16,10 @@ describe AlexaSkillsRuby::Handler do
15
16
  @starts = []
16
17
  end
17
18
 
19
+ on_verify_signature do
20
+ @verifies << request
21
+ end
22
+
18
23
  on_session_start do
19
24
  @starts << request
20
25
  end
@@ -40,14 +45,15 @@ describe AlexaSkillsRuby::Handler do
40
45
  end
41
46
  end
42
47
 
43
- let(:handler) { TestHandler.new }
48
+ let(:handler) { TestHandler.new(root_certificates: root_ca) }
44
49
 
45
50
  describe 'with a launch request' do
46
- let(:request_json) { load_json 'example_launch.json' }
51
+ let(:request_json) { load_example_json 'example_launch.json' }
47
52
 
48
53
  it 'fires the handlers' do
49
- handler.handle(request_json)
54
+ handler.handle(request_json, build_headers(request_json))
50
55
 
56
+ expect(handler.verifies.count).to eq 1
51
57
  expect(handler.auths.count).to eq 1
52
58
  expect(handler.intents.count).to eq 0
53
59
  expect(handler.ends.count).to eq 0
@@ -57,11 +63,12 @@ describe AlexaSkillsRuby::Handler do
57
63
  end
58
64
 
59
65
  describe 'with an intent request' do
60
- let(:request_json) { load_json 'example_intent.json' }
66
+ let(:request_json) { load_example_json 'example_intent.json' }
61
67
 
62
68
  it 'fires the handlers' do
63
- handler.handle(request_json)
69
+ handler.handle(request_json, build_headers(request_json))
64
70
 
71
+ expect(handler.verifies.count).to eq 1
65
72
  expect(handler.auths.count).to eq 1
66
73
  expect(handler.intents.count).to eq 1
67
74
  expect(handler.ends.count).to eq 0
@@ -74,12 +81,14 @@ describe AlexaSkillsRuby::Handler do
74
81
  let(:request_json) do
75
82
  json = load_fixture 'example_intent.json'
76
83
  json['request']['intent']['name'] = 'special'
84
+ add_timestamp(json)
77
85
  Oj.dump(json)
78
86
  end
79
87
 
80
88
  it 'fires the handlers' do
81
- handler.handle(request_json)
89
+ handler.handle(request_json, build_headers(request_json))
82
90
 
91
+ expect(handler.verifies.count).to eq 1
83
92
  expect(handler.auths.count).to eq 1
84
93
  expect(handler.intents.count).to eq 2
85
94
  expect(handler.ends.count).to eq 0
@@ -89,11 +98,12 @@ describe AlexaSkillsRuby::Handler do
89
98
  end
90
99
 
91
100
  describe 'with a session ended request' do
92
- let(:request_json) { load_json 'example_session_ended.json' }
101
+ let(:request_json) { load_example_json 'example_session_ended.json' }
93
102
 
94
103
  it 'fires the handlers' do
95
- handler.handle(request_json)
104
+ handler.handle(request_json, build_headers(request_json))
96
105
 
106
+ expect(handler.verifies.count).to eq 1
97
107
  expect(handler.auths.count).to eq 1
98
108
  expect(handler.intents.count).to eq 0
99
109
  expect(handler.ends.count).to eq 1
@@ -104,14 +114,15 @@ describe AlexaSkillsRuby::Handler do
104
114
 
105
115
  describe 'with an application_id set' do
106
116
 
107
- let(:handler) { TestHandler.new({application_id: 'amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe'}) }
117
+ let(:handler) { TestHandler.new({application_id: 'amzn1.echo-sdk-ams.app.000000-d0ed-0000-ad00-000000d00ebe', root_certificates: root_ca}) }
108
118
 
109
119
  describe 'with a valid app id in request' do
110
- let(:request_json) { load_json 'example_session_ended.json' }
120
+ let(:request_json) { load_example_json 'example_session_ended.json' }
111
121
 
112
122
  it 'fires the handlers' do
113
- handler.handle(request_json)
123
+ handler.handle(request_json, build_headers(request_json))
114
124
 
125
+ expect(handler.verifies.count).to eq 1
115
126
  expect(handler.auths.count).to eq 1
116
127
  expect(handler.intents.count).to eq 0
117
128
  expect(handler.ends.count).to eq 1
@@ -124,12 +135,14 @@ describe AlexaSkillsRuby::Handler do
124
135
  let(:request_json) do
125
136
  json = load_fixture 'example_intent.json'
126
137
  json['session']['application']['applicationId'] = 'broke'
138
+ add_timestamp(json)
127
139
  Oj.dump(json)
128
140
  end
129
141
 
130
142
  it 'fires the handlers' do
131
- expect { handler.handle(request_json) }.to raise_error AlexaSkillsRuby::InvalidApplicationId
143
+ expect { handler.handle(request_json, build_headers(request_json)) }.to raise_error AlexaSkillsRuby::InvalidApplicationId
132
144
 
145
+ expect(handler.verifies.count).to eq 1
133
146
  expect(handler.auths.count).to eq 1
134
147
  expect(handler.intents.count).to eq 0
135
148
  expect(handler.ends.count).to eq 0
@@ -139,4 +152,58 @@ describe AlexaSkillsRuby::Handler do
139
152
  end
140
153
  end
141
154
 
155
+ describe 'with signature validation disabled' do
156
+ let(:handler) { TestHandler.new({skip_signature_validation: true}) }
157
+ let(:request_json) { load_example_json 'example_launch.json' }
158
+
159
+ it 'fires the handlers' do
160
+ handler.handle(request_json, build_headers(request_json))
161
+
162
+ expect(handler.verifies.count).to eq 1
163
+ expect(handler.auths.count).to eq 1
164
+ expect(handler.intents.count).to eq 0
165
+ expect(handler.ends.count).to eq 0
166
+ expect(handler.starts.count).to eq 1
167
+ expect(handler.launch.count).to eq 1
168
+ end
169
+
170
+ it 'ignores the headers' do
171
+ handler.handle(request_json)
172
+
173
+ expect(handler.verifies.count).to eq 1
174
+ expect(handler.auths.count).to eq 1
175
+ expect(handler.intents.count).to eq 0
176
+ expect(handler.ends.count).to eq 0
177
+ expect(handler.starts.count).to eq 1
178
+ expect(handler.launch.count).to eq 1
179
+ end
180
+ end
181
+
182
+ describe 'with bad timestamps' do
183
+ let(:request_json) do
184
+ json = load_fixture 'example_intent.json'
185
+ json['request']['timestamp'] = (Time.now - 200).iso8601
186
+ Oj.dump(json)
187
+ end
188
+
189
+ it 'raises an error' do
190
+ expect { handler.handle(request_json, build_headers(request_json)) }.to raise_error AlexaSkillsRuby::TimestampValidationError
191
+ end
192
+
193
+ it 'fires the handlers' do
194
+ begin
195
+ handler.handle(request_json, build_headers(request_json))
196
+ rescue
197
+ end
198
+
199
+ expect(handler.verifies.count).to eq 1
200
+ expect(handler.auths.count).to eq 0
201
+ expect(handler.intents.count).to eq 0
202
+ expect(handler.ends.count).to eq 0
203
+ expect(handler.starts.count).to eq 0
204
+ expect(handler.launch.count).to eq 0
205
+
206
+ end
207
+ end
208
+
142
209
  end
@@ -0,0 +1,55 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')
2
+
3
+ describe AlexaSkillsRuby::SignatureValidator, x509: true do
4
+
5
+ let(:cert_cache) { AlexaSkillsRuby::SimpleCertificateCache.new }
6
+ let(:validator) { AlexaSkillsRuby::SignatureValidator.new(cert_cache).tap { |v| v.add_certificate_authorities([root_ca]) } }
7
+ let(:body) { load_example_json('example_launch.json') }
8
+ let(:signature) { Base64.encode64(get_signature(body)) }
9
+
10
+ it 'validates a good signature' do
11
+ validator.validate(body, certificate_url, signature)
12
+ end
13
+
14
+ it 'raises on invalid URLs' do
15
+ [
16
+ 'http://s3.amazonaws.com/echo.api/echo-api-cert.pem',
17
+ 'https://notamazon.com/echo.api/echo-api-cert.pem',
18
+ 'https://s3.amazonaws.com/EcHo.aPi/echo-api-cert.pem',
19
+ 'https://s3.amazonaws.com/invalid.path/echo-api-cert.pem',
20
+ 'https://s3.amazonaws.com:563/echo.api/echo-api-cert.pem'
21
+ ].each do |url|
22
+
23
+ stub_request(:get, url).to_return(status: 200, body: signing_cert.to_s)
24
+
25
+ expect { validator.validate(body, url, signature) }.to raise_error(AlexaSkillsRuby::SignatureValidationError, /Invalid signature URL:/)
26
+ end
27
+ end
28
+
29
+ it 'validates with valid URLs' do
30
+ [
31
+ 'https://s3.amazonaws.com/echo.api/echo-api-cert.pem',
32
+ 'https://s3.amazonaws.com:443/echo.api/echo-api-cert.pem',
33
+ 'https://s3.amazonaws.com/echo.api/../echo.api/echo-api-cert.pem'
34
+ ].each do |url|
35
+
36
+ stub_request(:get, url).to_return(status: 200, body: signing_cert.to_s)
37
+
38
+ validator.validate(body, url, signature)
39
+ end
40
+ end
41
+
42
+ it 'raises on invalid signature' do
43
+ different_body = load_example_json('example_intent.json')
44
+ expect { validator.validate(different_body, certificate_url, signature) }.to raise_error(AlexaSkillsRuby::SignatureValidationError, /Signature is invalid/)
45
+ end
46
+
47
+ describe 'with an invalid signing cert' do
48
+ let(:signing_cert) { build_signing_cert(root_ca, signing_key, false) }
49
+
50
+ it 'raises an error' do
51
+ expect { validator.validate(body, certificate_url, signature) }.to raise_error(AlexaSkillsRuby::SignatureValidationError, /Invalid certificate/)
52
+ end
53
+ end
54
+
55
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alexa_skills_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.7
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Elbert
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-11-07 00:00:00.000000000 Z
11
+ date: 2016-12-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '1.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: addressable
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '2.5'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '2.5'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: bundler
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -72,28 +86,42 @@ dependencies:
72
86
  requirements:
73
87
  - - "~>"
74
88
  - !ruby/object:Gem::Version
75
- version: 3.4.0
89
+ version: '3.5'
76
90
  type: :development
77
91
  prerelease: false
78
92
  version_requirements: !ruby/object:Gem::Requirement
79
93
  requirements:
80
94
  - - "~>"
81
95
  - !ruby/object:Gem::Version
82
- version: 3.4.0
96
+ version: '3.5'
83
97
  - !ruby/object:Gem::Dependency
84
98
  name: oj
85
99
  requirement: !ruby/object:Gem::Requirement
86
100
  requirements:
87
101
  - - "~>"
88
102
  - !ruby/object:Gem::Version
89
- version: 2.10.2
103
+ version: '2.10'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '2.10'
111
+ - !ruby/object:Gem::Dependency
112
+ name: webmock
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '2.3'
90
118
  type: :development
91
119
  prerelease: false
92
120
  version_requirements: !ruby/object:Gem::Requirement
93
121
  requirements:
94
122
  - - "~>"
95
123
  - !ruby/object:Gem::Version
96
- version: 2.10.2
124
+ version: '2.3'
97
125
  description:
98
126
  email:
99
127
  - dan.elbert@gmail.com
@@ -102,6 +130,7 @@ extensions: []
102
130
  extra_rdoc_files: []
103
131
  files:
104
132
  - ".gitignore"
133
+ - ".ruby-version"
105
134
  - ".travis.yml"
106
135
  - Gemfile
107
136
  - LICENSE
@@ -111,6 +140,7 @@ files:
111
140
  - gemfiles/as-4.2.gemfile
112
141
  - gemfiles/as-5.0.gemfile
113
142
  - lib/alexa_skills_ruby.rb
143
+ - lib/alexa_skills_ruby/certificate_validator.rb
114
144
  - lib/alexa_skills_ruby/errors.rb
115
145
  - lib/alexa_skills_ruby/handler.rb
116
146
  - lib/alexa_skills_ruby/json_object.rb
@@ -129,6 +159,8 @@ files:
129
159
  - lib/alexa_skills_ruby/json_objects/skills_request.rb
130
160
  - lib/alexa_skills_ruby/json_objects/skills_response.rb
131
161
  - lib/alexa_skills_ruby/json_objects/user.rb
162
+ - lib/alexa_skills_ruby/signature_validator.rb
163
+ - lib/alexa_skills_ruby/simple_certificate_cache.rb
132
164
  - lib/alexa_skills_ruby/version.rb
133
165
  - spec/fixtures/example_intent.json
134
166
  - spec/fixtures/example_launch.json
@@ -136,10 +168,12 @@ files:
136
168
  - spec/fixtures/example_session_ended.json
137
169
  - spec/spec_helper.rb
138
170
  - spec/support/fixture_support.rb
171
+ - spec/support/x509_support.rb
139
172
  - spec/unit/handler_spec.rb
140
173
  - spec/unit/json_object_spec.rb
141
174
  - spec/unit/json_objects/skills_request_spec.rb
142
175
  - spec/unit/json_objects/skills_response_spec.rb
176
+ - spec/unit/signature_validator_spec.rb
143
177
  homepage: https://github.com/DanElbert/alexa_skills_ruby
144
178
  licenses: []
145
179
  metadata: {}
@@ -159,7 +193,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
159
193
  version: '0'
160
194
  requirements: []
161
195
  rubyforge_project:
162
- rubygems_version: 2.4.6
196
+ rubygems_version: 2.5.1
163
197
  signing_key:
164
198
  specification_version: 4
165
199
  summary: Simple library to interface with the Alexa Skills Kit
@@ -170,7 +204,9 @@ test_files:
170
204
  - spec/fixtures/example_session_ended.json
171
205
  - spec/spec_helper.rb
172
206
  - spec/support/fixture_support.rb
207
+ - spec/support/x509_support.rb
173
208
  - spec/unit/handler_spec.rb
174
209
  - spec/unit/json_object_spec.rb
175
210
  - spec/unit/json_objects/skills_request_spec.rb
176
211
  - spec/unit/json_objects/skills_response_spec.rb
212
+ - spec/unit/signature_validator_spec.rb