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 +4 -4
- data/.ruby-version +1 -0
- data/README.md +10 -2
- data/Rakefile +6 -1
- data/alexa_skills_ruby.gemspec +4 -2
- data/lib/alexa_skills_ruby.rb +7 -0
- data/lib/alexa_skills_ruby/certificate_validator.rb +65 -0
- data/lib/alexa_skills_ruby/errors.rb +14 -1
- data/lib/alexa_skills_ruby/handler.rb +28 -3
- data/lib/alexa_skills_ruby/signature_validator.rb +109 -0
- data/lib/alexa_skills_ruby/simple_certificate_cache.rb +17 -0
- data/lib/alexa_skills_ruby/version.rb +1 -1
- data/spec/spec_helper.rb +2 -0
- data/spec/support/fixture_support.rb +10 -0
- data/spec/support/x509_support.rb +75 -0
- data/spec/unit/handler_spec.rb +81 -14
- data/spec/unit/signature_validator_spec.rb +55 -0
- metadata +43 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 51b36b59de95199d06f652746412db9e4c079f93
|
4
|
+
data.tar.gz: 228715baaf0c2b52a3b5686c342b463fb7a69773
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f24e5ef81403c726452728d74e0f66015f182244e78dcb8564a0d934d922fc06afea6c097a45fb8a21d5e248eacf26fc0d71f4c173dc3dfc4af0b103d752cc57
|
7
|
+
data.tar.gz: 50a2b116aa410629118124a902b5e6ea9164212f54d2f04ebd261e7f05494f2c93c86bae7945d4260e4b116c33a621e599ac88195078281575d0e22777586866
|
data/.ruby-version
ADDED
@@ -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
|
-
|
54
|
-
|
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
|
data/alexa_skills_ruby.gemspec
CHANGED
@@ -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.
|
25
|
-
spec.add_development_dependency "oj", "~> 2.10
|
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
|
data/lib/alexa_skills_ruby.rb
CHANGED
@@ -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
|
-
|
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
|
data/spec/spec_helper.rb
CHANGED
@@ -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
|
data/spec/unit/handler_spec.rb
CHANGED
@@ -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) {
|
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) {
|
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) {
|
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) {
|
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
|
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
|
+
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.
|
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.
|
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
|
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.
|
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.
|
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
|