alexa_request_verifier 0.1.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f04e24cc0281c027ccf0f9d8220c121000623eec
4
+ data.tar.gz: 3957243bbc7ecb22f3f0563a82061e903c811c84
5
+ SHA512:
6
+ metadata.gz: 67b7bc57673d5e3de1364ff0f76b7e644407af7a4b66b35184a48cb9730d4c06799503092988394883aac0d6a76a6e90154d28c02803400551e36a0632b76662
7
+ data.tar.gz: 01c9b9eb44b4ccd23a15d834203344b7d919a68ea4f027684513141164b211c3b3c5ecf973281a5fa36d1c9d7dfdfeb4ca68e9145d1095ccdc4e8bc964d5d868
@@ -0,0 +1,12 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+ Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
@@ -0,0 +1,9 @@
1
+ AllCops:
2
+ Exclude:
3
+ - spec/**/*.rb
4
+
5
+ Metrics/LineLength:
6
+ Enabled: false
7
+
8
+ Style/FrozenStringLiteralComment:
9
+ Enabled: false
@@ -0,0 +1 @@
1
+ 2.4.2
@@ -0,0 +1,14 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.0
5
+ - 2.1
6
+ - 2.2
7
+ - 2.3
8
+ - 2.4
9
+ - 2.4.2
10
+ - ruby-head
11
+
12
+ matrix:
13
+ allow_failures:
14
+ - rvm: ruby-head
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at m@rayner.io. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [http://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: http://contributor-covenant.org
74
+ [version]: http://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source 'https://rubygems.org'
2
+
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ # Specify your gem's dependencies in alexa_request_verifier.gemspec
6
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2017 Matt Rayner
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,95 @@
1
+ # Alexa Request Verifier
2
+
3
+ [AlexaRequestVerifier][alexa_request_verifier] is a gem created to verify that requests received within a Sinatra application originate from Amazon's Alexa API.
4
+
5
+ [![Build Status][shield-travis]][info-travis] [![License][shield-license]][info-license]
6
+
7
+ ## Requirements
8
+ [AlexaRequestVerifier][alexa_request_verifier] requires the following:
9
+ * [Ruby][ruby] - version 2.0 or greater
10
+
11
+
12
+ ## Installation
13
+
14
+ Add this line to your application's Gemfile:
15
+
16
+ ```ruby
17
+ gem 'alexa_request_verifier'
18
+ ```
19
+
20
+
21
+ ## Usage
22
+ This gem's main function is taking an [Sinatra][sinatra] request and verifying that it was sent by Amazon.
23
+
24
+ ```ruby
25
+ # within server.rb (or equivalent)
26
+
27
+ post '/' do
28
+ AlexaRequestVerifier.valid!(request)
29
+ end
30
+ ```
31
+
32
+ ### Methods
33
+ [AlexaRequestVerifier][alexa_request_verifier] has two main entry points, detailsed below:
34
+
35
+ Method | Parameter type | Returns
36
+ ---|---|---
37
+ `AlexaRequestVerifier.valid!(request)` | `Sinatra::Request` | `true` on successful verification. Raises an error if unsuccessful.
38
+ `AlexaRequestVerifier.valid?(request)` | `Sinatra::Request` | `true` on successful verificatipn. `false` if unsuccessful.
39
+
40
+
41
+ ### Handling errors
42
+ AlexaRequestVerifier#valid! will raise one of the following *expected* errors if verification cannot be performed.
43
+
44
+ > Please note that all errors come with (hopefully) helpful accompanying messages.
45
+
46
+ Error | Description
47
+ ---|---
48
+ `AlexaRequestVerifier::InvalidCertificateURIError` | Raised when the certificate URI does not pass validation.
49
+ `AlexaRequestVerifier::InvalidCertificateError` | Raised when the certificate itself does not pass validation e.g. out of date, does not contain the requires SAN extension, etc.
50
+ `AlexaRequestVerifier::InvalidRequestError` | Raised when the request cannot be verified (not timely, not signed with the certificate, etc.)
51
+
52
+
53
+ ## Getting Started with Development
54
+ To clone the repository and set up the dependencies, run the following:
55
+ ```bash
56
+ git clone https://github.com/mattrayner/alexa_request_verifier.git
57
+ cd alexa_request_verifier
58
+ bundle install
59
+ ```
60
+
61
+ ### Running the tests
62
+ We use [RSpec][rspec] as our testing framework and tests can be run using:
63
+ ```bash
64
+ bundle exec rake
65
+ ```
66
+
67
+
68
+ ## Contributing
69
+ If you wish to submit a bug fix or feature, you can create a pull request and it will be merged pending a code review.
70
+
71
+ 1. Fork the repository
72
+ 1. Create your feature branch (`git checkout -b my-new-feature`)
73
+ 1. Commit your changes (`git commit -am 'Add some feature'`)
74
+ 1. Push to the branch (`git push origin my-new-feature`)
75
+ 1. Ensure your changes are tested using [Rspec][rspec]
76
+ 1. Create a new Pull Request
77
+
78
+
79
+ ## License
80
+ [AlexaRequestVerifier][alexa_request_verifier] is licensed under the [MIT][info-license].
81
+
82
+ ## Code of Conduct
83
+
84
+ Everyone interacting in the AlexaRequestVerifier project’s codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct][code_of_conduct].
85
+
86
+ [alexa_request_verifier]: https://github.com/mattrayner/alexa_request_verifier
87
+ [ruby]: http://ruby-lang.org
88
+ [rspec]: http://rspec.info
89
+ [code_of_conduct]: https://github.com/mattrayner/alexa_request_verifier/blob/master/CODE_OF_CONDUCT.md
90
+
91
+ [info-travis]: https://travis-ci.org/mattrayner/alexa_request_verifier
92
+ [shield-travis]: https://img.shields.io/travis/mattrayner/alexa_request_verifier.svg
93
+
94
+ [info-license]: https://github.com/mattrayner/alexa_request_verifier/blob/master/LICENSE
95
+ [shield-license]: https://img.shields.io/badge/license-MIT-blue.svg
@@ -0,0 +1,6 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -0,0 +1,30 @@
1
+ lib = File.expand_path('../lib', __FILE__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require 'alexa_request_verifier/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'alexa_request_verifier'
7
+ spec.version = AlexaRequestVerifier::VERSION
8
+ spec.authors = ['Matt Rayner']
9
+ spec.email = ['m@rayner.io']
10
+
11
+ spec.summary = 'Verify HTTP requests sent to an Alexa skill are sent from Amazon.'
12
+ spec.description = 'This gem is designed to work with Sinatra applications that serve as back-ends for Amazon Alexa skills.'
13
+ spec.homepage = 'https://github.com/mattrayner/alexa_request_verifier'
14
+ spec.license = 'MIT'
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
+ f.match(%r{^(test|spec|features)/})
18
+ end
19
+ spec.bindir = 'exe'
20
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
21
+ spec.require_paths = ['lib']
22
+
23
+ spec.add_development_dependency 'bundler', '~> 1.16'
24
+ spec.add_development_dependency 'rake', '~> 10.0'
25
+ spec.add_development_dependency 'rspec', '~> 3.0'
26
+ spec.add_development_dependency 'simplecov', '~> 0.15'
27
+ spec.add_development_dependency 'timecop', '~> 0.9'
28
+ spec.add_development_dependency 'vcr', '~> 3.0'
29
+ spec.add_development_dependency 'webmock', '~> 3.0'
30
+ end
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'alexa_request_verifier'
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require 'irb'
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,108 @@
1
+ require 'alexa_request_verifier/certificate_store'
2
+ require 'alexa_request_verifier/verifier'
3
+ require 'alexa_request_verifier/version'
4
+
5
+ # Errors
6
+ require 'alexa_request_verifier/base_error'
7
+ require 'alexa_request_verifier/invalid_certificate_error'
8
+ require 'alexa_request_verifier/invalid_certificate_u_r_i_error'
9
+ require 'alexa_request_verifier/invalid_request_error'
10
+
11
+ # Verify that HTTP requests sent to an Alexa skill are sent from Amazon
12
+ # @since 0.1.0
13
+ module AlexaRequestVerifier
14
+ REQUEST_THRESHOLD = 150 # Requests must be received within X seconds
15
+
16
+ class << self
17
+ # Validate a request object from Sinatra.
18
+ # Raise an error if it is not valid.
19
+ #
20
+ # @param [Sinatra::Request] request a Sinatra HTTP Request
21
+ #
22
+ # @raise [AlexaRequestVerifier::InvalidCertificateURIError]
23
+ # there was a problem validating the certificate URI from your request
24
+ #
25
+ # @return [nil] will always return nil
26
+ def valid!(request)
27
+ signature_certificate_url = request.env['HTTP_SIGNATURECERTCHAINURL']
28
+
29
+ AlexaRequestVerifier::Verifier::CertificateURIVerifier.valid!(signature_certificate_url)
30
+
31
+ raw_body = request.body.read
32
+ request.body.rewind
33
+
34
+ check_that_request_is_timely(raw_body)
35
+
36
+ check_that_request_is_valid(signature_certificate_url, request, raw_body)
37
+
38
+ true
39
+ end
40
+
41
+ # Validate a request object from Sinatra.
42
+ # Return a boolean.
43
+ #
44
+ # @param [Sinatra::Request] request a Sinatra HTTP Request
45
+ # @return [Boolean] is the request valid?
46
+ def valid?(request)
47
+ begin
48
+ valid!(request)
49
+ rescue AlexaRequestVerifier::BaseError => e
50
+ puts e
51
+
52
+ return false
53
+ end
54
+
55
+ true
56
+ end
57
+
58
+ private
59
+
60
+ # Prevent replays of requests by checking that they are timely.
61
+ #
62
+ # @param [String] raw_body the raw body of our https request
63
+ # @raise [AlexaRequestVerifier::InvalidRequestError] raised when the timestamp is not timely, or is not set
64
+ def check_that_request_is_timely(raw_body)
65
+ request_json = JSON.parse(raw_body)
66
+
67
+ raise AlexaRequestVerifier::InvalidRequestError, 'Timestamp field not present in request' if request_json.fetch('request', {}).fetch('timestamp', nil).nil?
68
+
69
+ raise AlexaRequestVerifier::InvalidRequestError, 'Request is from more than 150 seconds ago' unless Time.parse(request_json['request']['timestamp'].to_s) >= (Time.now - REQUEST_THRESHOLD)
70
+ end
71
+
72
+ # Check that our request is valid.
73
+ #
74
+ # @param [String] signature_certificate_url the url for our signing certificate
75
+ # @param [Sinatra::Request] request the request object
76
+ # @param [String] raw_body the raw body of our https request
77
+ def check_that_request_is_valid(signature_certificate_url, request, raw_body)
78
+ certificate = AlexaRequestVerifier::CertificateStore.fetch(signature_certificate_url)
79
+
80
+ begin
81
+ AlexaRequestVerifier::Verifier::CertificateVerifier.valid!(certificate)
82
+
83
+ check_that_request_was_signed(certificate.public_key, request, raw_body)
84
+ rescue AlexaRequestVerifier::InvalidCertificateError, AlexaRequestVerifier::InvalidRequestError => error
85
+ # We don't want to cache a certificate that fails our checks as it could lock us out of valid requests for the cache length
86
+ AlexaRequestVerifier::CertificateStore.delete(signature_certificate_url)
87
+
88
+ raise error
89
+ end
90
+ end
91
+
92
+ # Check that our request was signed by a given public key.
93
+ #
94
+ # @param [OpenSSL::PKey::PKey] certificate_public_key the public key we are checking
95
+ # @param [Sinatra::Request] request the request object we are checking
96
+ # @param [String] raw_body the raw body of our https request
97
+ # @raise [AlexaRequestVerifier::InvalidRequestError] raised if our signature does not match the certificate provided
98
+ def check_that_request_was_signed(certificate_public_key, request, raw_body)
99
+ signed_by_certificate = certificate_public_key.verify(
100
+ OpenSSL::Digest::SHA1.new,
101
+ Base64.decode64(request.env['HTTP_SIGNATURE']),
102
+ raw_body
103
+ )
104
+
105
+ raise AlexaRequestVerifier::InvalidRequestError, 'Signature does not match certificate provided' unless signed_by_certificate
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,4 @@
1
+ module AlexaRequestVerifier
2
+ class BaseError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,81 @@
1
+ module AlexaRequestVerifier
2
+ # A module used to download, cache and serve certificates from our requests.
3
+ # @since 0.1
4
+ module CertificateStore
5
+ CERTIFICATE_CACHE_TIME = 1800 # 30 minutes
6
+
7
+ class << self
8
+ # Given a certificate uri, either download the certificate, or
9
+ # load it from our certificate store.
10
+ #
11
+ # @param [String] uri the uri of our certificate
12
+ # @return [OpenSSL::X509::Certificate] our certificate file
13
+ def fetch(uri)
14
+ store
15
+
16
+ if cache_valid?(@store[uri])
17
+ certificate = @store[uri][:certificate]
18
+ else
19
+ certificate_data = download_certificate(uri)
20
+ certificate = OpenSSL::X509::Certificate.new(certificate_data)
21
+
22
+ @store[uri] = { timestamp: Time.now, certificate: certificate }
23
+ end
24
+
25
+ certificate
26
+ end
27
+
28
+ # Given a certificate uri, remove the certificate from our store.
29
+ #
30
+ # @param [String] uri the uri of our certificate
31
+ # @return [nil|Hash] returns nil if the certificate was not in the store,
32
+ # or a Hash representing the deleted certificate
33
+ def delete(uri)
34
+ store
35
+
36
+ @store.delete(uri)
37
+ end
38
+
39
+ # Returns a copy of our certificate store
40
+ #
41
+ # @return [Hash] returns our certificate store
42
+ def store
43
+ @store ||= {}
44
+ end
45
+
46
+ private
47
+
48
+ # Given a certificate entry from our store, tell us if the cache is still valid
49
+ #
50
+ # @param [Hash] certificate_entry the entry we are checking
51
+ # @return [Boolean] is the certificate cache valid?
52
+ def cache_valid?(certificate_entry)
53
+ return false if certificate_entry.nil?
54
+
55
+ (Time.now <= (certificate_entry[:timestamp] + CERTIFICATE_CACHE_TIME))
56
+ end
57
+
58
+ # Given a certificate uri, download it and return the certificate data
59
+ #
60
+ # @param [String] uri the uri of our certificate
61
+ # @return [String] certificate data
62
+ def download_certificate(uri)
63
+ certificate_uri = URI.parse(uri)
64
+
65
+ certificate_data = nil
66
+
67
+ Net::HTTP.start(certificate_uri.host, certificate_uri.port, use_ssl: true) do |http|
68
+ http.verify_mode = OpenSSL::SSL::VERIFY_PEER
69
+
70
+ response = http.request(Net::HTTP::Get.new(certificate_uri))
71
+
72
+ raise AlexaRequestVerifier::InvalidCertificateError, "Unable to download certificate from #{certificate_uri} - Got #{response.code} status code" unless response.is_a? Net::HTTPOK
73
+
74
+ certificate_data = response.body
75
+ end
76
+
77
+ certificate_data
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,8 @@
1
+ module AlexaRequestVerifier
2
+ # An error that is raised when the certificate referenced from a request is
3
+ # invalid.
4
+ #
5
+ # @since 0.1
6
+ class InvalidCertificateError < AlexaRequestVerifier::BaseError
7
+ end
8
+ end
@@ -0,0 +1,31 @@
1
+ module AlexaRequestVerifier
2
+ # An error that is raised when the certificate URI from a request is invalid.
3
+ # @since 0.1
4
+ class InvalidCertificateURIError < AlexaRequestVerifier::BaseError
5
+ # Create a new instance of our InvalidCertificateURIError
6
+ #
7
+ # @param [String] message the main message we want to include
8
+ # @param [String] value an optional value used when creating a message.
9
+ #
10
+ # @example Error without a value
11
+ # AlexaRequestVerifier::InvalidCertificateURIError.new(
12
+ # 'No URI Passed'
13
+ # ) #=> #<AlexaRequestVerifier::InvalidCertificateURIError
14
+ # @message="Invalid certificate URI : No URI Passed.">
15
+ #
16
+ # @example Error with a valuex
17
+ # AlexaRequestVerifier::InvalidCertificateURIError.new(
18
+ # "Expected 'a'",
19
+ # 'b'
20
+ # ) #=> #<AlexaRequestVerifier::InvalidCertificateURIError
21
+ # @message="Invalid certificate URI : Expected 'a'. Got: 'b'.">
22
+ #
23
+ # @return [AlexaRequestVerifier::InvalidCertificateURIError] a new instance
24
+ def initialize(message, value = nil)
25
+ error_message = "Invalid certificate URI : #{message}."
26
+ error_message = "#{error_message} Got: '#{value}'." if value
27
+
28
+ super(error_message)
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,8 @@
1
+ module AlexaRequestVerifier
2
+ # An error that is raised when the certificate referenced from a request is
3
+ # invalid.
4
+ #
5
+ # @since 0.1
6
+ class InvalidRequestError < AlexaRequestVerifier::BaseError
7
+ end
8
+ end
@@ -0,0 +1,9 @@
1
+ require_relative 'verifier/certificate_verifier'
2
+ require_relative 'verifier/certificate_u_r_i_verifier'
3
+
4
+ module AlexaRequestVerifier
5
+ # A namespace for all of our verifiers to live under
6
+ # @since 0.1
7
+ module Verifier
8
+ end
9
+ end
@@ -0,0 +1,93 @@
1
+ module AlexaRequestVerifier
2
+ module Verifier
3
+ # Given an Alexa certificate URI, validate it according to:
4
+ # https://developer.amazon.com/docs/custom-skills/host-a-custom-skill-as-a-web-service.html#h2_verify_sig_cert
5
+ #
6
+ # @since 0.1
7
+ module CertificateURIVerifier
8
+ VALIDATIONS = {
9
+ scheme: 'https',
10
+ port: 443,
11
+ host: 's3.amazonaws.com'
12
+ }.freeze
13
+
14
+ PATH_REGEX = %r{\A\/echo.api\/}
15
+
16
+ class << self
17
+ # Check that a given certificate URI meets Amazon's requirements.
18
+ # Raise an error if it does not.
19
+ #
20
+ # @param [String] uri The URI value from HTTP_SIGNATURECERTCHAINURL
21
+ #
22
+ # @raise [AlexaRequestVerifier::InvalidCertificateURIError] An error
23
+ # raised when the URI does not meet a requirement
24
+ #
25
+ # @return [true] This method will either raise an error or return true
26
+ def valid!(uri)
27
+ begin
28
+ uri = URI.parse(uri)
29
+ rescue URI::InvalidURIError => e
30
+ puts e
31
+
32
+ raise AlexaRequestVerifier::InvalidCertificateURIError,
33
+ "#{uri} : #{e.message}"
34
+ end
35
+
36
+ test_validations(uri)
37
+
38
+ test_path(uri)
39
+
40
+ true
41
+ end
42
+
43
+ # Check that a given certificate URI meets Amazon's requirements
44
+ # Return true if it does, or false if it does not.
45
+ #
46
+ # @param [String] uri The URI value from HTTP_SIGNATURECERTCHAINURL
47
+ #
48
+ # @return [Boolean] Returns true if the uri is valid and false if not
49
+ def valid?(uri)
50
+ begin
51
+ valid!(uri)
52
+ rescue AlexaRequestVerifier::InvalidCertificateURIError => e
53
+ puts e
54
+
55
+ return false
56
+ end
57
+
58
+ true
59
+ end
60
+
61
+ private
62
+
63
+ # Test that a given URI meets all of our 'simple' validation rules.
64
+ #
65
+ # @param [URI] uri the URI object to test
66
+ def test_validations(uri)
67
+ VALIDATIONS.each do |method, value|
68
+ next if uri.send(method) == value
69
+
70
+ raise AlexaRequestVerifier::InvalidCertificateURIError.new(
71
+ "URI #{method} must be '#{value}'",
72
+ uri.send(method)
73
+ )
74
+ end
75
+ end
76
+
77
+ # Test that a given URI matches our 'path' regex.
78
+ #
79
+ # @param [URI] uri the URI object to test
80
+ def test_path(uri)
81
+ path = File.absolute_path(uri.path)
82
+
83
+ return if path.match(PATH_REGEX) # rubocop:disable Performance/RegexpMatch # Disabled for backwards compatibility below 2.4
84
+
85
+ raise AlexaRequestVerifier::InvalidCertificateURIError.new(
86
+ "URI path must start with '/echo.api/'",
87
+ uri.path
88
+ )
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,65 @@
1
+ require 'json'
2
+ require 'time'
3
+ require 'net/http'
4
+ require 'openssl'
5
+ require 'base64'
6
+
7
+ module AlexaRequestVerifier
8
+ module Verifier
9
+ # Given an OpenSSL certificate, validate it according to:
10
+ # https://developer.amazon.com/docs/custom-skills/host-a-custom-skill-as-a-web-service.html#h2_verify_sig_cert
11
+ #
12
+ # @since 0.1
13
+ module CertificateVerifier
14
+ SAN = 'echo-api.amazon.com'.freeze
15
+
16
+ class << self
17
+ # Check that a given certificate meet's Amazon's requirements.
18
+ # Raise an error if it does not.
19
+ #
20
+ # @param [OpenSSL::X509::Certificate] certificate certificate to check.
21
+ #
22
+ # @raise [AlexaRequestVerifier::InvalidCertificateError] raised when
23
+ # the provided certificate does not meet a requirement
24
+ #
25
+ # @return [true] either returns true or raises an error.
26
+ def valid!(certificate)
27
+ # Check that it's in date
28
+ certificate_in_date = Time.now.between?(certificate.not_before, certificate.not_after)
29
+ raise AlexaRequestVerifier::InvalidCertificateError, 'Certificate is not in date.' unless certificate_in_date
30
+
31
+ # Check that the required SAN is present
32
+ valid_sans = certificate.extensions.select do |extension|
33
+ valid_oid = (extension.oid == 'subjectAltName')
34
+ valid_value = (extension.value == "DNS:#{SAN}")
35
+
36
+ valid_oid && valid_value
37
+ end
38
+ raise AlexaRequestVerifier::InvalidCertificateError, "Certificate does not contain SAN: #{SAN}." if valid_sans.empty?
39
+
40
+ # TODO: Check that the certificate is valid up to the root CA
41
+
42
+ true
43
+ end
44
+
45
+ # Check that a given certificate meet's Amazon's requirements.
46
+ # Returns a boolean.
47
+ #
48
+ # @param [OpenSSL::X509::Certificate] certificate certificate to check.
49
+ #
50
+ # @return [Boolean] returns the result of our checks.
51
+ def valid?(certificate)
52
+ begin
53
+ valid!(certificate)
54
+ rescue AlexaRequestVerifier::InvalidCertificateError => e
55
+ puts e
56
+
57
+ return false
58
+ end
59
+
60
+ true
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,3 @@
1
+ module AlexaRequestVerifier
2
+ VERSION = '0.1.0'.freeze
3
+ end
metadata ADDED
@@ -0,0 +1,166 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: alexa_request_verifier
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Matt Rayner
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2017-11-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.16'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.16'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: simplecov
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.15'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.15'
69
+ - !ruby/object:Gem::Dependency
70
+ name: timecop
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.9'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.9'
83
+ - !ruby/object:Gem::Dependency
84
+ name: vcr
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '3.0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '3.0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: webmock
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.0'
111
+ description: This gem is designed to work with Sinatra applications that serve as
112
+ back-ends for Amazon Alexa skills.
113
+ email:
114
+ - m@rayner.io
115
+ executables: []
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - ".gitignore"
120
+ - ".rspec"
121
+ - ".rubocop.yml"
122
+ - ".ruby-version"
123
+ - ".travis.yml"
124
+ - CODE_OF_CONDUCT.md
125
+ - Gemfile
126
+ - LICENSE
127
+ - README.md
128
+ - Rakefile
129
+ - alexa_request_verifier.gemspec
130
+ - bin/console
131
+ - bin/setup
132
+ - lib/alexa_request_verifier.rb
133
+ - lib/alexa_request_verifier/base_error.rb
134
+ - lib/alexa_request_verifier/certificate_store.rb
135
+ - lib/alexa_request_verifier/invalid_certificate_error.rb
136
+ - lib/alexa_request_verifier/invalid_certificate_u_r_i_error.rb
137
+ - lib/alexa_request_verifier/invalid_request_error.rb
138
+ - lib/alexa_request_verifier/verifier.rb
139
+ - lib/alexa_request_verifier/verifier/certificate_u_r_i_verifier.rb
140
+ - lib/alexa_request_verifier/verifier/certificate_verifier.rb
141
+ - lib/alexa_request_verifier/version.rb
142
+ homepage: https://github.com/mattrayner/alexa_request_verifier
143
+ licenses:
144
+ - MIT
145
+ metadata: {}
146
+ post_install_message:
147
+ rdoc_options: []
148
+ require_paths:
149
+ - lib
150
+ required_ruby_version: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: '0'
155
+ required_rubygems_version: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ requirements: []
161
+ rubyforge_project:
162
+ rubygems_version: 2.6.14
163
+ signing_key:
164
+ specification_version: 4
165
+ summary: Verify HTTP requests sent to an Alexa skill are sent from Amazon.
166
+ test_files: []