alexa_ruby 1.3.1 → 1.4.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: a28d3e44888f08923d448b5a675e682385fcada0
4
- data.tar.gz: be3114c0d065b28dd04f48bb8237a0198c38cea7
3
+ metadata.gz: 28630927ffe709806449fc5c67540dbef0753e4f
4
+ data.tar.gz: 84c0caa9f8f813736565eaee144634fa4a6bc457
5
5
  SHA512:
6
- metadata.gz: e92206b7c69748dbd3ff41762a0bffc1c2d0190a308174b3939a0c8653cafaae6bc2fd50b1613c112317f76d27f5f035a0099c6150d2cbecf8cb376d1e1576b2
7
- data.tar.gz: e6a018b12bac14cbfe84b92ffe8ef061e1ab03db338df73a5a5c7ed0b3c906dca5360180ef26329d8f3020d1efff87c8c0d81574cfeae666198e768bf7a28e65
6
+ metadata.gz: dc51a947340ba26899fc75e033372c5842edc5c653770608f56c67c934e878eceac2d9dcf1137ba286b87555895d889154e756712c91e904a9388385e9e02b57
7
+ data.tar.gz: c9524f193e8ff842a9f007050da4cebd727cf0ccfb9da5c131b5ca7857cfa7ae14009b91f0164371bdb36570b7accee08baee8befaeb2c9af720fb4d97adc970
data/CHANGELOG CHANGED
@@ -1,3 +1,6 @@
1
+ [1.4.0]
2
+ - Validate Amazon request signature and SSL certificates
3
+
1
4
  [1.3.1]
2
5
  - Slot can have no value at all
3
6
 
data/Gemfile.lock CHANGED
@@ -2,13 +2,17 @@ PATH
2
2
  remote: .
3
3
  specs:
4
4
  alexa_ruby (1.3.1)
5
+ addressable (>= 2.5.1)
5
6
  bundler (>= 1.6.9)
7
+ httparty (>= 0.15.5)
6
8
  oj (~> 3.0)
7
9
  rake
8
10
 
9
11
  GEM
10
12
  remote: https://rubygems.org/
11
13
  specs:
14
+ addressable (2.5.1)
15
+ public_suffix (~> 2.0, >= 2.0.2)
12
16
  ansi (1.5.0)
13
17
  builder (3.2.3)
14
18
  coveralls (0.8.21)
@@ -18,6 +22,8 @@ GEM
18
22
  thor (~> 0.19.4)
19
23
  tins (~> 1.6)
20
24
  docile (1.1.5)
25
+ httparty (0.15.5)
26
+ multi_xml (>= 0.5.2)
21
27
  json (2.1.0)
22
28
  minitest (5.10.2)
23
29
  minitest-reporters (1.1.14)
@@ -25,7 +31,9 @@ GEM
25
31
  builder
26
32
  minitest (>= 5.0)
27
33
  ruby-progressbar
28
- oj (3.0.10)
34
+ multi_xml (0.6.0)
35
+ oj (3.1.3)
36
+ public_suffix (2.0.5)
29
37
  rake (12.0.0)
30
38
  ruby-progressbar (1.8.1)
31
39
  simplecov (0.14.1)
@@ -48,4 +56,4 @@ DEPENDENCIES
48
56
  minitest-reporters (~> 1.1, >= 1.1.14)
49
57
 
50
58
  BUNDLED WITH
51
- 1.15.0
59
+ 1.15.3
data/README.md CHANGED
@@ -63,12 +63,22 @@ class App < Roda
63
63
  end
64
64
  ```
65
65
 
66
- Request validations can be disabled:
66
+ Request structure validations can be disabled:
67
67
 
68
68
  ```ruby
69
69
  AlexaRuby.new(request, disable_validations: true)
70
70
  ```
71
71
 
72
+ If needed, you can validate request signature and Amazon SSL certificates chain. To do so specify several parameters:
73
+
74
+ ```ruby
75
+ AlexaRuby.new(
76
+ request,
77
+ certificates_chain_url: url,
78
+ request_signature: signature
79
+ )
80
+ ```
81
+
72
82
  After initializing new AlexaRuby instance you will have a possibility to access
73
83
  all parameters of the received request.
74
84
 
@@ -15,16 +15,19 @@ module AlexaRuby
15
15
  invalid_request_exception if invalid_request?
16
16
  @request = define_request
17
17
  raise ArgumentError, 'Unknown type of Alexa request' if @request.nil?
18
+ @request.valid? if ssl_check?
18
19
  @response = Response.new(@request.type, @request.version)
19
20
  end
20
21
 
21
22
  private
22
23
 
23
- # Check if validations are enabled
24
- #
25
- # @return [Boolean]
26
- def validations_enabled?
27
- !@opts[:disable_validations] || @opts[:disable_validations].nil?
24
+ # Request structure isn't valid, raise exception
25
+ def invalid_request_exception
26
+ raise ArgumentError,
27
+ 'Invalid request structure, ' \
28
+ 'please, refer to the Amazon Alexa manual: ' \
29
+ 'https://developer.amazon.com/public/solutions' \
30
+ '/alexa/alexa-skills-kit/docs/alexa-skills-kit-interface-reference'
28
31
  end
29
32
 
30
33
  # Check if it is an invalid request
@@ -34,13 +37,11 @@ module AlexaRuby
34
37
  @req[:version].nil? || @req[:request].nil? if validations_enabled?
35
38
  end
36
39
 
37
- # Request structure isn't valid, raise exception
38
- def invalid_request_exception
39
- raise ArgumentError,
40
- 'Invalid request structure, ' \
41
- 'please, refer to the Amazon Alexa manual: ' \
42
- 'https://developer.amazon.com/public/solutions' \
43
- '/alexa/alexa-skills-kit/docs/alexa-skills-kit-interface-reference'
40
+ # Check if validations are enabled
41
+ #
42
+ # @return [Boolean]
43
+ def validations_enabled?
44
+ !@opts[:disable_validations] || @opts[:disable_validations].nil?
44
45
  end
45
46
 
46
47
  # Initialize proper request object
@@ -58,5 +59,15 @@ module AlexaRuby
58
59
  AudioPlayerRequest.new(@req)
59
60
  end
60
61
  end
62
+
63
+ # Check if we have SSL certificates URL and request signature
64
+ # and need to validate Amazon request
65
+ #
66
+ # @return [Boolean]
67
+ def ssl_check?
68
+ @request.certificates_chain_url = @opts[:certificates_chain_url]
69
+ @request.signature = @opts[:request_signature]
70
+ @request.certificates_chain_url && @request.signature
71
+ end
61
72
  end
62
73
  end
@@ -0,0 +1,84 @@
1
+ require 'httparty'
2
+ require 'openssl'
3
+
4
+ module AlexaRuby
5
+ # SSL certificates validator
6
+ class Certificates
7
+ # Setup new certificates chain
8
+ #
9
+ # @param certificates_chain_url [String] SSL certificates chain URL
10
+ # @param signature [String] HTTP request signature
11
+ # @param request [String] plain HTTP request body
12
+ def initialize(certificates_chain_url, signature, request)
13
+ download_certificates(certificates_chain_url)
14
+ @signature = signature
15
+ @request = request
16
+ end
17
+
18
+ # Check if it is a valid certificates chain and request signature
19
+ #
20
+ # @return [Boolean]
21
+ def valid?
22
+ raise ArgumentError, 'Inactive Amazon SSL certificate' unless active?
23
+ raise ArgumentError, 'Inactive host in SSL certificate' unless amazon?
24
+ raise ArgumentError, 'Signature and request mismatch' unless verified?
25
+ true
26
+ end
27
+
28
+ private
29
+
30
+ # Download SSL certificates chain from Amazon URL
31
+ #
32
+ # @param certificates_chain_url [String] SSL certificates chain URL
33
+ def download_certificates(certificates_chain_url)
34
+ resp = HTTParty.get(certificates_chain_url)
35
+ raise ArgumentError, 'Invalid certificates chain' unless resp.code == 200
36
+ @cert = OpenSSL::X509::Certificate.new(resp.body)
37
+ end
38
+
39
+ # Check if it is an active certificate
40
+ #
41
+ # @return [Boolean]
42
+ def active?
43
+ now = Time.now
44
+ @cert.not_before < now && @cert.not_after > now
45
+ end
46
+
47
+ # Check if Subject Alternative Names includes Amazon domain name
48
+ #
49
+ # @return [Boolean]
50
+ def amazon?
51
+ @cert.subject.to_a.flatten.include? 'echo-api.amazon.com'
52
+ end
53
+
54
+ # Check if given signature matches given request
55
+ #
56
+ # @return [Boolean]
57
+ def verified?
58
+ sign = decode_signature
59
+ pkey = public_key
60
+ pkey.verify(hash, sign, @request)
61
+ end
62
+
63
+ # Decode base64-encoded signature
64
+ #
65
+ # @return [String] decoded signature
66
+ def decode_signature
67
+ Base64.decode64(@signature)
68
+ end
69
+
70
+ # Get public key from certificate
71
+ #
72
+ # @return [OpenSSL::PKey::RSA] OpenSSL PKey object
73
+ def public_key
74
+ @cert.public_key
75
+ end
76
+
77
+ # Get hash type for comparison
78
+ #
79
+ # @return [OpenSSL::Digest::SHA1] OpenSSL SHA1 hash
80
+ def hash
81
+ OpenSSL::Digest::SHA1.new
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,54 @@
1
+ module AlexaRuby
2
+ # URI request validator
3
+ class URI
4
+ attr_reader :uri
5
+
6
+ # Setup new URI
7
+ #
8
+ # @param uri [String] URI
9
+ def initialize(uri)
10
+ @uri = Addressable::URI.parse(uri).normalize!
11
+ end
12
+
13
+ # Check if it is a valid Amazon URI
14
+ #
15
+ # @return [Boolean]
16
+ def valid?
17
+ raise ArgumentError, 'Certificates chain URL must be HTTPS' unless https?
18
+ raise ArgumentError, 'Not Amazon host in certificates URL' unless amazon?
19
+ raise ArgumentError, 'Invalid certificates chain URL' unless echo_api?
20
+ raise ArgumentError, 'Certificates chain URL must be HTTPS' unless port?
21
+ true
22
+ end
23
+
24
+ private
25
+
26
+ # Check if URI scheme is HTTPS
27
+ #
28
+ # @return [Boolean]
29
+ def https?
30
+ @uri.scheme == 'https'
31
+ end
32
+
33
+ # Check if URI host is a valid Amazon host
34
+ #
35
+ # @return [Boolean]
36
+ def amazon?
37
+ @uri.host.casecmp('s3.amazonaws.com').zero?
38
+ end
39
+
40
+ # Check if URI path starts with /echo.api/
41
+ #
42
+ # @return [Boolean]
43
+ def echo_api?
44
+ @uri.path[0..9] == '/echo.api/'
45
+ end
46
+
47
+ # Check if URI port is 443 if port is present
48
+ #
49
+ # @return [Boolean]
50
+ def port?
51
+ @uri.port.nil? || @uri.port == 443
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,54 @@
1
+ module AlexaRuby
2
+ # Validator is responsible for Amazon request validation:
3
+ # - SignatureCertChainUrl validation
4
+ # - Amazon Alexa request signature validation
5
+ class Validator
6
+ TIMESTAMP_TOLERANCE = 150
7
+
8
+ # Setup new validator
9
+ #
10
+ # @param cert_chain_url [String] SSL certificates chain URI
11
+ # @param signature [String] HTTP request signature
12
+ # @param request [Object] json request
13
+ # @param timestamp_diff [Integer] valid distance in seconds between
14
+ # current time and the request timestamp
15
+ def initialize(cert_chain_url, signature, request, timestamp_diff = nil)
16
+ @chain_url = cert_chain_url
17
+ @signature = signature
18
+ @request = request
19
+ @timestamp_diff = timestamp_diff || TIMESTAMP_TOLERANCE
20
+ end
21
+
22
+ # Check if it is a valid Amazon request
23
+ #
24
+ # @return [Boolean]
25
+ def valid_request?
26
+ raise ArgumentError, 'Outdated request' unless timestamp_tolerant?
27
+ valid_uri? && valid_certificates?
28
+ end
29
+
30
+ private
31
+
32
+ # Check if request is timestamp tolerant
33
+ #
34
+ # @return [Boolean]
35
+ def timestamp_tolerant?
36
+ request_ts = @request[:request][:timestamp]
37
+ Time.parse(request_ts) >= (Time.now - @timestamp_diff)
38
+ end
39
+
40
+ # Check if it is a valid Amazon URI
41
+ #
42
+ # @return [Boolean]
43
+ def valid_uri?
44
+ URI.new(@chain_url).valid?
45
+ end
46
+
47
+ # Check if it is a valid certificates chain and request signature
48
+ #
49
+ # @return [Boolean]
50
+ def valid_certificates?
51
+ Certificates.new(@chain_url, @signature, Oj.to_json(@request)).valid?
52
+ end
53
+ end
54
+ end
@@ -2,6 +2,7 @@ module AlexaRuby
2
2
  # Amazon Alexa web service request
3
3
  class BaseRequest
4
4
  attr_reader :version, :type, :session, :context, :id, :timestamp, :locale
5
+ attr_accessor :certificates_chain_url, :signature
5
6
 
6
7
  # Initialize new request object
7
8
  #
@@ -17,6 +18,14 @@ module AlexaRuby
17
18
  parse_base_params(@req[:request])
18
19
  end
19
20
 
21
+ # Check if it is a valid Amazon request
22
+ #
23
+ # @return [Boolean]
24
+ def valid?
25
+ validator = Validator.new(certificates_chain_url, signature, @req)
26
+ validator.valid_request?
27
+ end
28
+
20
29
  # Return JSON representation of given request
21
30
  #
22
31
  # @return [String] request json
@@ -84,7 +84,7 @@ module AlexaRuby
84
84
  # @param url [String] some URL
85
85
  # @return [Boolean]
86
86
  def invalid_url?(url)
87
- URI.parse(url).scheme != 'https'
87
+ Addressable::URI.parse(url).scheme != 'https'
88
88
  end
89
89
 
90
90
  # Get station token
@@ -95,7 +95,7 @@ module AlexaRuby
95
95
  # @param url [String] some URL
96
96
  # @return [Boolean]
97
97
  def invalid_url?(url)
98
- URI.parse(url).scheme != 'https'
98
+ Addressable::URI.parse(url).scheme != 'https'
99
99
  end
100
100
  end
101
101
  end
@@ -1,3 +1,3 @@
1
1
  module AlexaRuby
2
- VERSION = '1.3.1'.freeze
2
+ VERSION = '1.4.0'.freeze
3
3
  end
data/lib/alexa_ruby.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # Utilities
2
+ require 'addressable/uri'
2
3
  require 'oj'
3
4
  require 'securerandom'
4
5
 
@@ -8,6 +9,9 @@ require 'alexa_ruby/request/base_request'
8
9
  require 'alexa_ruby/request/base_request/context'
9
10
  require 'alexa_ruby/request/base_request/context/device'
10
11
  require 'alexa_ruby/request/base_request/session'
12
+ require 'alexa_ruby/request/base_request/validator'
13
+ require 'alexa_ruby/request/base_request/validator/uri'
14
+ require 'alexa_ruby/request/base_request/validator/certificates'
11
15
  require 'alexa_ruby/request/base_request/user'
12
16
  require 'alexa_ruby/request/audio_player_request'
13
17
  require 'alexa_ruby/request/launch_request'
@@ -28,6 +32,8 @@ module AlexaRuby
28
32
  # can be hash or JSON encoded string
29
33
  # @param opts [Hash] additional options:
30
34
  # :disable_validations [Boolean] disables request validation if true
35
+ # :certificates_chain_url [String] URL of Amazon SSL certificates chain
36
+ # :request_signature [String] Base64-encoded request signature
31
37
  # @return [Object] new Request object instance
32
38
  # @raise [ArgumentError] if given object isn't a valid JSON object
33
39
  def new(request, opts = {})
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: alexa_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Mulev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2017-07-24 00:00:00.000000000 Z
11
+ date: 2017-08-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -52,6 +52,34 @@ dependencies:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
54
  version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: addressable
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 2.5.1
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 2.5.1
69
+ - !ruby/object:Gem::Dependency
70
+ name: httparty
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: 0.15.5
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: 0.15.5
55
83
  - !ruby/object:Gem::Dependency
56
84
  name: minitest
57
85
  requirement: !ruby/object:Gem::Requirement
@@ -113,6 +141,9 @@ files:
113
141
  - lib/alexa_ruby/request/base_request/context/device.rb
114
142
  - lib/alexa_ruby/request/base_request/session.rb
115
143
  - lib/alexa_ruby/request/base_request/user.rb
144
+ - lib/alexa_ruby/request/base_request/validator.rb
145
+ - lib/alexa_ruby/request/base_request/validator/certificates.rb
146
+ - lib/alexa_ruby/request/base_request/validator/uri.rb
116
147
  - lib/alexa_ruby/request/intent_request.rb
117
148
  - lib/alexa_ruby/request/intent_request/slot.rb
118
149
  - lib/alexa_ruby/request/launch_request.rb
@@ -142,7 +173,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
142
173
  version: '0'
143
174
  requirements: []
144
175
  rubyforge_project:
145
- rubygems_version: 2.2.2
176
+ rubygems_version: 2.6.12
146
177
  signing_key:
147
178
  specification_version: 4
148
179
  summary: Ruby toolkit for Amazon Alexa API