alexa_skills_ruby 0.0.7 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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