Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make signature verifier more resilient to HTTP retrieval errors #230

Merged
merged 1 commit into from
Nov 18, 2019
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,26 +15,27 @@

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.KeyStoreException;
import java.security.NoSuchAlgorithmException;
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;
Expand All @@ -49,6 +50,8 @@ public final class SkillRequestSignatureVerifier implements SkillServletVerifier
private static final String VALID_SIGNING_CERT_CHAIN_URL_HOST_NAME = "s3.amazonaws.com";
private static final String VALID_SIGNING_CERT_CHAIN_URL_PATH_PREFIX = "/echo.api/";
private static final int UNSPECIFIED_SIGNING_CERT_CHAIN_URL_PORT_VALUE = -1;
private static final int CERT_RETRIEVAL_RETRY_COUNT = 5;
private static final int DELAY_BETWEEN_RETRIES_MS = 500;

private final Proxy proxy;

Expand Down Expand Up @@ -79,9 +82,8 @@ public void verify(AlexaHttpRequest alexaHttpRequest) {
}

try {
X509Certificate signingCertificate;
if (CERTIFICATE_CACHE.containsKey(signingCertificateChainUrl)) {
signingCertificate = CERTIFICATE_CACHE.get(signingCertificateChainUrl);
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
Expand Down Expand Up @@ -121,56 +123,91 @@ public void verify(AlexaHttpRequest alexaHttpRequest) {
*/
private X509Certificate retrieveAndVerifyCertificateChain(
final String signingCertificateChainUrl) throws CertificateException {
try (InputStream in =
proxy != null ? getAndVerifySigningCertificateChainUrl(signingCertificateChainUrl).openConnection(proxy).getInputStream()
: getAndVerifySigningCertificateChainUrl(signingCertificateChainUrl).openConnection().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;
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() != 200) {
if (waitForRetry(attempt)) {
continue;
} else {
throw new CertificateException("Got a non-200 status code when retrieving certificate at URL: " + signingCertificateChainUrl);
}
}
}

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);
}
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);

/*
* 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 Echo SDK");
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");
}

return signingCertificate;
} catch (KeyStoreException | IOException | NoSuchAlgorithmException ex) {
throw new CertificateException("Unable to verify certificate at URL: "
+ signingCertificateChainUrl, ex);
private boolean waitForRetry(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;
}
}

Expand Down Expand Up @@ -247,4 +284,4 @@ static URL getAndVerifySigningCertificateChainUrl(final String signingCertificat
}
}

}
}