alexa_verifier 0.1.0 → 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 +5 -5
- data/.gitignore +2 -1
- data/.hound.yml +2 -0
- data/.rspec +1 -0
- data/.rubocop.yml +13 -0
- data/.ruby-version +1 -0
- data/.travis.yml +21 -2
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +2 -0
- data/{LICENSE.txt → LICENSE} +6 -6
- data/README.md +209 -2
- data/Rakefile +3 -3
- data/alexa_verifier.gemspec +21 -17
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/alexa_verifier.rb +50 -113
- data/lib/alexa_verifier/base_error.rb +4 -0
- data/lib/alexa_verifier/certificate_store.rb +98 -0
- data/lib/alexa_verifier/configuration.rb +53 -0
- data/lib/alexa_verifier/invalid_certificate_error.rb +8 -0
- data/lib/alexa_verifier/invalid_certificate_u_r_i_error.rb +31 -0
- data/lib/alexa_verifier/invalid_request_error.rb +8 -0
- data/lib/alexa_verifier/verifier.rb +125 -0
- data/lib/alexa_verifier/verifier/certificate_u_r_i_verifier.rb +93 -0
- data/lib/alexa_verifier/verifier/certificate_verifier.rb +99 -0
- data/lib/alexa_verifier/version.rb +3 -0
- metadata +92 -31
@@ -0,0 +1,93 @@
|
|
1
|
+
module AlexaVerifier
|
2
|
+
class 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 [AlexaVerifier::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 AlexaVerifier::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 AlexaVerifier::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 AlexaVerifier::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 AlexaVerifier::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,99 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'time'
|
3
|
+
require 'net/http'
|
4
|
+
require 'openssl'
|
5
|
+
require 'base64'
|
6
|
+
|
7
|
+
module AlexaVerifier
|
8
|
+
class 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
|
+
# @param [Array<OpenSSL::X509::Certificate>] chain chain of certificates to a root trusted CA.
|
22
|
+
#
|
23
|
+
# @raise [AlexaVerifier::InvalidCertificateError] raised when
|
24
|
+
# the provided certificate does not meet a requirement
|
25
|
+
#
|
26
|
+
# @return [true] either returns true or raises an error.
|
27
|
+
def valid!(certificate, chain)
|
28
|
+
check_that_certificate_is_in_date(certificate)
|
29
|
+
|
30
|
+
check_that_certificate_has_the_expected_extensions(certificate)
|
31
|
+
|
32
|
+
check_that_we_can_create_a_chain_of_trust_to_a_root_ca(certificate, chain)
|
33
|
+
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
# Check that a given certificate meet's Amazon's requirements.
|
38
|
+
# Returns a boolean.
|
39
|
+
#
|
40
|
+
# @param [OpenSSL::X509::Certificate] certificate certificate to check.
|
41
|
+
# @param [Array<OpenSSL::X509::Certificate>] chain chain of certificates to a root CA.
|
42
|
+
#
|
43
|
+
# @return [Boolean] returns the result of our checks.
|
44
|
+
def valid?(certificate, chain)
|
45
|
+
begin
|
46
|
+
valid!(certificate, chain)
|
47
|
+
rescue AlexaVerifier::InvalidCertificateError => e
|
48
|
+
puts e
|
49
|
+
|
50
|
+
return false
|
51
|
+
end
|
52
|
+
|
53
|
+
true
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
# Given a certificate file, check that it is in date.
|
59
|
+
#
|
60
|
+
# @param [OpenSSL::X509::Certificate] certificate the certificate we should check
|
61
|
+
#
|
62
|
+
# @raise [AlexaVerifier::InvalidCertificateError] raised if the certificate is not in date
|
63
|
+
def check_that_certificate_is_in_date(certificate)
|
64
|
+
certificate_in_date = Time.now.between?(certificate.not_before, certificate.not_after)
|
65
|
+
raise AlexaVerifier::InvalidCertificateError, 'Certificate is not in date.' unless certificate_in_date
|
66
|
+
end
|
67
|
+
|
68
|
+
# Given a certificate file, check that it contains our expected extensions
|
69
|
+
#
|
70
|
+
# @param [OpenSSL::X509::Certificate] certificate the certificate we should check
|
71
|
+
#
|
72
|
+
# @raise [AlexaVerifier::InvalidCertificateError] raised if the extensions are not present
|
73
|
+
def check_that_certificate_has_the_expected_extensions(certificate)
|
74
|
+
valid_sans = certificate.extensions.select do |extension|
|
75
|
+
valid_oid = (extension.oid == 'subjectAltName')
|
76
|
+
valid_value = (extension.value == "DNS:#{SAN}")
|
77
|
+
|
78
|
+
valid_oid && valid_value
|
79
|
+
end
|
80
|
+
raise AlexaVerifier::InvalidCertificateError, "Certificate does not contain SAN: #{SAN}." if valid_sans.empty?
|
81
|
+
end
|
82
|
+
|
83
|
+
# Check that the certificate, along with any chain from our downloaded certificate file, makes a chain of trust to a trusted root CA
|
84
|
+
#
|
85
|
+
# @param [OpenSSL::X509::Certificate] certificate certificate we should check
|
86
|
+
# @param [Array<OpenSSL::X509::Certificate>] chain chain of certificates to a root trusted CA
|
87
|
+
#
|
88
|
+
# @raise [AlexaVerifier::InvalidCertificateError] raised if a chain of trust could not be established
|
89
|
+
def check_that_we_can_create_a_chain_of_trust_to_a_root_ca(certificate, chain)
|
90
|
+
openssl_x509_store = OpenSSL::X509::Store.new
|
91
|
+
openssl_x509_store.set_default_paths
|
92
|
+
|
93
|
+
valid_certificate_chain = openssl_x509_store.verify(certificate, chain)
|
94
|
+
raise AlexaVerifier::InvalidCertificateError, "Unable to create a 'chain of trust' from the provided certificate to a trusted root CA." unless valid_certificate_chain
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
metadata
CHANGED
@@ -1,102 +1,163 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: alexa_verifier
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Christopher Mullins
|
8
|
+
- Matt Rayner
|
8
9
|
autorequire:
|
9
10
|
bindir: exe
|
10
11
|
cert_chain: []
|
11
|
-
date:
|
12
|
+
date: 2017-11-24 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: bundler
|
15
16
|
requirement: !ruby/object:Gem::Requirement
|
16
17
|
requirements:
|
17
|
-
- - ~>
|
18
|
+
- - "~>"
|
18
19
|
- !ruby/object:Gem::Version
|
19
|
-
version: '1.
|
20
|
+
version: '1.16'
|
20
21
|
type: :development
|
21
22
|
prerelease: false
|
22
23
|
version_requirements: !ruby/object:Gem::Requirement
|
23
24
|
requirements:
|
24
|
-
- - ~>
|
25
|
+
- - "~>"
|
25
26
|
- !ruby/object:Gem::Version
|
26
|
-
version: '1.
|
27
|
+
version: '1.16'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: coveralls
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - "~>"
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: 0.8.21
|
35
|
+
type: :development
|
36
|
+
prerelease: false
|
37
|
+
version_requirements: !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - "~>"
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 0.8.21
|
27
42
|
- !ruby/object:Gem::Dependency
|
28
43
|
name: rake
|
29
44
|
requirement: !ruby/object:Gem::Requirement
|
30
45
|
requirements:
|
31
|
-
- - ~>
|
46
|
+
- - "~>"
|
32
47
|
- !ruby/object:Gem::Version
|
33
48
|
version: '10.0'
|
34
49
|
type: :development
|
35
50
|
prerelease: false
|
36
51
|
version_requirements: !ruby/object:Gem::Requirement
|
37
52
|
requirements:
|
38
|
-
- - ~>
|
53
|
+
- - "~>"
|
39
54
|
- !ruby/object:Gem::Version
|
40
55
|
version: '10.0'
|
41
56
|
- !ruby/object:Gem::Dependency
|
42
57
|
name: rspec
|
43
58
|
requirement: !ruby/object:Gem::Requirement
|
44
59
|
requirements:
|
45
|
-
- -
|
60
|
+
- - "~>"
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '3.0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - "~>"
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '3.0'
|
70
|
+
- !ruby/object:Gem::Dependency
|
71
|
+
name: simplecov
|
72
|
+
requirement: !ruby/object:Gem::Requirement
|
73
|
+
requirements:
|
74
|
+
- - "~>"
|
75
|
+
- !ruby/object:Gem::Version
|
76
|
+
version: '0.14'
|
77
|
+
type: :development
|
78
|
+
prerelease: false
|
79
|
+
version_requirements: !ruby/object:Gem::Requirement
|
80
|
+
requirements:
|
81
|
+
- - "~>"
|
82
|
+
- !ruby/object:Gem::Version
|
83
|
+
version: '0.14'
|
84
|
+
- !ruby/object:Gem::Dependency
|
85
|
+
name: timecop
|
86
|
+
requirement: !ruby/object:Gem::Requirement
|
87
|
+
requirements:
|
88
|
+
- - "~>"
|
46
89
|
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
90
|
+
version: '0.9'
|
48
91
|
type: :development
|
49
92
|
prerelease: false
|
50
93
|
version_requirements: !ruby/object:Gem::Requirement
|
51
94
|
requirements:
|
52
|
-
- -
|
95
|
+
- - "~>"
|
53
96
|
- !ruby/object:Gem::Version
|
54
|
-
version: '0'
|
97
|
+
version: '0.9'
|
55
98
|
- !ruby/object:Gem::Dependency
|
56
|
-
name:
|
99
|
+
name: vcr
|
57
100
|
requirement: !ruby/object:Gem::Requirement
|
58
101
|
requirements:
|
59
|
-
- - ~>
|
102
|
+
- - "~>"
|
60
103
|
- !ruby/object:Gem::Version
|
61
|
-
version: 0
|
104
|
+
version: '3.0'
|
62
105
|
type: :development
|
63
106
|
prerelease: false
|
64
107
|
version_requirements: !ruby/object:Gem::Requirement
|
65
108
|
requirements:
|
66
|
-
- - ~>
|
109
|
+
- - "~>"
|
67
110
|
- !ruby/object:Gem::Version
|
68
|
-
version: 0
|
111
|
+
version: '3.0'
|
69
112
|
- !ruby/object:Gem::Dependency
|
70
113
|
name: webmock
|
71
114
|
requirement: !ruby/object:Gem::Requirement
|
72
115
|
requirements:
|
73
|
-
- -
|
116
|
+
- - "~>"
|
74
117
|
- !ruby/object:Gem::Version
|
75
|
-
version: '0'
|
118
|
+
version: '3.0'
|
76
119
|
type: :development
|
77
120
|
prerelease: false
|
78
121
|
version_requirements: !ruby/object:Gem::Requirement
|
79
122
|
requirements:
|
80
|
-
- -
|
123
|
+
- - "~>"
|
81
124
|
- !ruby/object:Gem::Version
|
82
|
-
version: '0'
|
83
|
-
description:
|
125
|
+
version: '3.0'
|
126
|
+
description: This gem is designed to work with Rack applications that serve as back-ends
|
127
|
+
for Amazon Alexa skills.
|
84
128
|
email:
|
85
129
|
- chris@sidoh.org
|
130
|
+
- m@rayner.io
|
86
131
|
executables: []
|
87
132
|
extensions: []
|
88
133
|
extra_rdoc_files: []
|
89
134
|
files:
|
90
|
-
- .gitignore
|
91
|
-
- .
|
92
|
-
- .
|
135
|
+
- ".gitignore"
|
136
|
+
- ".hound.yml"
|
137
|
+
- ".rspec"
|
138
|
+
- ".rubocop.yml"
|
139
|
+
- ".ruby-version"
|
140
|
+
- ".travis.yml"
|
141
|
+
- CODE_OF_CONDUCT.md
|
93
142
|
- Gemfile
|
94
|
-
- LICENSE
|
143
|
+
- LICENSE
|
95
144
|
- README.md
|
96
145
|
- Rakefile
|
97
146
|
- alexa_verifier.gemspec
|
147
|
+
- bin/console
|
148
|
+
- bin/setup
|
98
149
|
- lib/alexa_verifier.rb
|
99
|
-
|
150
|
+
- lib/alexa_verifier/base_error.rb
|
151
|
+
- lib/alexa_verifier/certificate_store.rb
|
152
|
+
- lib/alexa_verifier/configuration.rb
|
153
|
+
- lib/alexa_verifier/invalid_certificate_error.rb
|
154
|
+
- lib/alexa_verifier/invalid_certificate_u_r_i_error.rb
|
155
|
+
- lib/alexa_verifier/invalid_request_error.rb
|
156
|
+
- lib/alexa_verifier/verifier.rb
|
157
|
+
- lib/alexa_verifier/verifier/certificate_u_r_i_verifier.rb
|
158
|
+
- lib/alexa_verifier/verifier/certificate_verifier.rb
|
159
|
+
- lib/alexa_verifier/version.rb
|
160
|
+
homepage: https://github.com/sidoh/alexa_verifier
|
100
161
|
licenses:
|
101
162
|
- MIT
|
102
163
|
metadata: {}
|
@@ -106,18 +167,18 @@ require_paths:
|
|
106
167
|
- lib
|
107
168
|
required_ruby_version: !ruby/object:Gem::Requirement
|
108
169
|
requirements:
|
109
|
-
- -
|
170
|
+
- - ">="
|
110
171
|
- !ruby/object:Gem::Version
|
111
172
|
version: '0'
|
112
173
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
113
174
|
requirements:
|
114
|
-
- -
|
175
|
+
- - ">="
|
115
176
|
- !ruby/object:Gem::Version
|
116
177
|
version: '0'
|
117
178
|
requirements: []
|
118
179
|
rubyforge_project:
|
119
|
-
rubygems_version: 2.
|
180
|
+
rubygems_version: 2.7.2
|
120
181
|
signing_key:
|
121
182
|
specification_version: 4
|
122
|
-
summary:
|
183
|
+
summary: Verify HTTP requests sent to an Alexa skill are sent from Amazon.
|
123
184
|
test_files: []
|