Skip to content

Commit

Permalink
Make signature verifier more resiliant to HTTP retrieval errors (#230)
Browse files Browse the repository at this point in the history
  • Loading branch information
breedloj authored Nov 18, 2019
1 parent 046f3d8 commit 85fd0f0
Showing 1 changed file with 88 additions and 51 deletions.
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
}
}

}
}

0 comments on commit 85fd0f0

Please sign in to comment.