-
Notifications
You must be signed in to change notification settings - Fork 748
/
SkillRequestSignatureVerifier.java
339 lines (301 loc) · 14.5 KB
/
SkillRequestSignatureVerifier.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
/*
Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file
except in compliance with the License. A copy of the License is located at
http://aws.amazon.com/apache2.0/
or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for
the specific language governing permissions and limitations under the License.
*/
package com.amazon.ask.servlet.verifiers;
import com.amazon.ask.servlet.ServletConstants;
import org.apache.commons.codec.binary.Base64;
import org.apache.commons.io.IOUtils;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.Signature;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* Provides a utility method to verify the signature of a skill request.
*/
public final class SkillRequestSignatureVerifier implements SkillServletVerifier {
/**
* Map which serves as a cache to store public key certificates.
*/
private static final Map<String, X509Certificate> CERTIFICATE_CACHE = new ConcurrentHashMap<>();
/**
* Used to check if the entry is for a domain name.
*/
private static final Integer DOMAIN_NAME_SUBJECT_ALTERNATIVE_NAME_ENTRY = 2;
/**
* Certificate chain protocol.
*/
private static final String VALID_SIGNING_CERT_CHAIN_PROTOCOL = "https";
/**
* Certificate chain host name.
*/
private static final String VALID_SIGNING_CERT_CHAIN_URL_HOST_NAME = "s3.amazonaws.com";
/**
* Certificate chain url path prefix.
*/
private static final String VALID_SIGNING_CERT_CHAIN_URL_PATH_PREFIX = "/echo.api/";
/**
* Used to validate the port to make the connection on.
*/
private static final int UNSPECIFIED_SIGNING_CERT_CHAIN_URL_PORT_VALUE = -1;
/**
* Maximum number of trials to retrieve a certificate.
*/
private static final int CERT_RETRIEVAL_RETRY_COUNT = 5;
/**
* Delay between each retry in milliseconds.
*/
private static final int DELAY_BETWEEN_RETRIES_MS = 500;
/**
* Http OK response code.
*/
private static final int HTTP_OK_RESPONSE_CODE = 200;
/**
* Represents a proxy setting, typically a type (http, socks) and a socket address.
*/
private final Proxy proxy;
/**
* Constructor to build an instance of SkillRequestSignatureVerifier.
*/
public SkillRequestSignatureVerifier() {
this.proxy = null;
}
/**
* @param proxy proxy configuration for certificate retrieval
*/
public SkillRequestSignatureVerifier(final Proxy proxy) {
this.proxy = proxy;
}
/**
* Verifies the certificate authenticity using the configured TrustStore and the signature of
* the skill request. This method will throw a {@link SecurityException} if the signature
* does not pass verification.
*
* {@inheritDoc}
*/
public void verify(final AlexaHttpRequest alexaHttpRequest) {
String baseEncoded64Signature = alexaHttpRequest.getBaseEncoded64Signature();
String signingCertificateChainUrl = alexaHttpRequest.getSigningCertificateChainUrl();
if ((baseEncoded64Signature == null) || (signingCertificateChainUrl == null)) {
throw new SecurityException(
"Missing signature/certificate for the provided skill request");
}
try {
X509Certificate signingCertificate = CERTIFICATE_CACHE.get(signingCertificateChainUrl);
if (signingCertificate != null && signingCertificate.getNotAfter().after(new Date())) {
/*
* check the before/after dates on the certificate are still valid for the present
* time
*/
signingCertificate.checkValidity();
} else {
signingCertificate = retrieveAndVerifyCertificateChain(signingCertificateChainUrl);
// if certificate is valid, then add it to the cache
CERTIFICATE_CACHE.put(signingCertificateChainUrl, signingCertificate);
}
// verify that the request was signed by the provided certificate
Signature signature = Signature.getInstance(ServletConstants.SIGNATURE_ALGORITHM);
signature.initVerify(signingCertificate.getPublicKey());
signature.update(alexaHttpRequest.getSerializedRequestEnvelope());
if (!signature.verify(Base64.decodeBase64(baseEncoded64Signature
.getBytes(ServletConstants.CHARACTER_ENCODING)))) {
throw new SecurityException(
"Failed to verify the signature/certificate for the provided skill request");
}
} catch (GeneralSecurityException | IOException ex) {
throw new SecurityException(
"Failed to verify the signature/certificate for the provided skill request",
ex);
}
}
/**
* Retrieves the certificate from the specified URL and confirms that the certificate is valid.
*
* @param signingCertificateChainUrl
* the URL to retrieve the certificate chain from
* @return the certificate at the specified URL, if the certificate is valid
* @throws CertificateException
* if the certificate cannot be retrieve or is invalid
*/
private X509Certificate retrieveAndVerifyCertificateChain(final String signingCertificateChainUrl) throws CertificateException {
for (int attempt = 0; attempt <= CERT_RETRIEVAL_RETRY_COUNT; attempt++) {
InputStream in = null;
try {
HttpURLConnection connection =
proxy != null ? (HttpURLConnection) getAndVerifySigningCertificateChainUrl(signingCertificateChainUrl).openConnection(proxy)
: (HttpURLConnection) getAndVerifySigningCertificateChainUrl(signingCertificateChainUrl).openConnection();
if (connection.getResponseCode() != HTTP_OK_RESPONSE_CODE) {
if (waitForRetry(attempt)) {
continue;
} else {
throw new CertificateException("Got a non-200 status code when retrieving certificate at URL: " + signingCertificateChainUrl);
}
}
in = connection.getInputStream();
CertificateFactory certificateFactory =
CertificateFactory.getInstance(ServletConstants.SIGNATURE_CERTIFICATE_TYPE);
@SuppressWarnings("unchecked")
Collection<X509Certificate> certificateChain =
(Collection<X509Certificate>) certificateFactory.generateCertificates(in);
/*
* check the before/after dates on the certificate date to confirm that it is valid on
* the current date
*/
X509Certificate signingCertificate = certificateChain.iterator().next();
signingCertificate.checkValidity();
// check the certificate chain
TrustManagerFactory trustManagerFactory =
TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init((KeyStore) null);
X509TrustManager x509TrustManager = null;
for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
if (trustManager instanceof X509TrustManager) {
x509TrustManager = (X509TrustManager) trustManager;
}
}
if (x509TrustManager == null) {
throw new IllegalStateException(
"No X509 TrustManager available. Unable to check certificate chain");
} else {
x509TrustManager.checkServerTrusted(
certificateChain.toArray(new X509Certificate[certificateChain.size()]),
ServletConstants.SIGNATURE_TYPE);
}
/*
* verify Echo API's hostname is specified as one of subject alternative names on the
* signing certificate
*/
if (!subjectAlernativeNameListContainsEchoSdkDomainName(signingCertificate
.getSubjectAlternativeNames())) {
throw new CertificateException(
"The provided certificate is not valid for the ASK SDK");
}
return signingCertificate;
} catch (IOException e) {
if (!waitForRetry(attempt)) {
throw new CertificateException("Unable to retrieve certificate from URL: " + signingCertificateChainUrl, e);
}
} catch (Exception e) {
throw new CertificateException("Unable to verify certificate at URL: " + signingCertificateChainUrl, e);
} finally {
if (in != null) {
IOUtils.closeQuietly(in);
}
}
}
throw new RuntimeException("Unable to retrieve signing certificate due to an unhandled exception");
}
/**
* Checks if the system should retry to fetch a certificate.
* @param attempt Nth attempt.
* @return true if the number of attempts to retrieve certificates does not exceed pre-mentioned value.
*/
private boolean waitForRetry(final int attempt) {
if (attempt < CERT_RETRIEVAL_RETRY_COUNT) {
try {
Thread.sleep(DELAY_BETWEEN_RETRIES_MS);
return true;
} catch (InterruptedException ex) {
throw new RuntimeException("Interrupted while waiting for certificate retrieval retry attempt", ex);
}
} else {
return false;
}
}
/**
* Verify Echo API's hostname is specified as one of subject alternative names on the signing certificate.
* @param subjectAlternativeNameEntries name entries.
* @return true if subject alternative entry is in the expected form and if the entry is for a domain name and that domain name
* matches the domain name for the echo sdk.
*/
private boolean subjectAlernativeNameListContainsEchoSdkDomainName(
final Collection<List<?>> subjectAlternativeNameEntries) {
for (List<?> entry : subjectAlternativeNameEntries) {
// first ensure that the subject alternative entry is in the expected form
if (entry.get(0) instanceof Integer && entry.get(1) instanceof String) {
/*
* if the entry is for a domain name and that domain name matches the domain name
* for the echo sdk then return true
*/
if (DOMAIN_NAME_SUBJECT_ALTERNATIVE_NAME_ENTRY.equals(entry.get(0))
&& ServletConstants.ECHO_API_DOMAIN_NAME.equals((entry.get(1)))) {
return true;
}
}
}
return false;
}
/**
* Verifies the signing certificate chain URL and returns a {@code URL} object.
*
* @param signingCertificateChainUrl
* the external form of the URL
* @return the URL
* @throws CertificateException
* if the URL is malformed or contains an invalid hostname, an unsupported protocol,
* or an invalid port (if specified)
*/
static URL getAndVerifySigningCertificateChainUrl(final String signingCertificateChainUrl)
throws CertificateException {
try {
URL url = new URI(signingCertificateChainUrl).normalize().toURL();
// Validate the hostname
if (!VALID_SIGNING_CERT_CHAIN_URL_HOST_NAME.equalsIgnoreCase(url.getHost())) {
throw new CertificateException(String.format(
"SigningCertificateChainUrl [%s] does not contain the required hostname"
+ " of [%s]", signingCertificateChainUrl,
VALID_SIGNING_CERT_CHAIN_URL_HOST_NAME));
}
// Validate the path prefix
String path = url.getPath();
if (!path.startsWith(VALID_SIGNING_CERT_CHAIN_URL_PATH_PREFIX)) {
throw new CertificateException(String.format(
"SigningCertificateChainUrl path [%s] is invalid. Expecting path to "
+ "start with [%s]", signingCertificateChainUrl,
VALID_SIGNING_CERT_CHAIN_URL_PATH_PREFIX));
}
// Validate the protocol
String urlProtocol = url.getProtocol();
if (!VALID_SIGNING_CERT_CHAIN_PROTOCOL.equalsIgnoreCase(urlProtocol)) {
throw new CertificateException(String.format(
"SigningCertificateChainUrl [%s] contains an unsupported protocol [%s]",
signingCertificateChainUrl, urlProtocol));
}
// Validate the port uses the default of 443 for HTTPS if explicitly defined in the URL
int urlPort = url.getPort();
if ((urlPort != UNSPECIFIED_SIGNING_CERT_CHAIN_URL_PORT_VALUE)
&& (urlPort != url.getDefaultPort())) {
throw new CertificateException(String.format(
"SigningCertificateChainUrl [%s] contains an invalid port [%d]",
signingCertificateChainUrl, urlPort));
}
return url;
} catch (IllegalArgumentException | MalformedURLException | URISyntaxException ex) {
throw new CertificateException(String.format(
"SigningCertificateChainUrl [%s] is malformed", signingCertificateChainUrl), ex);
}
}
}