com.amazonaws.services.sns.message.SignatureVerifier Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of aws-java-sdk-sns Show documentation
Show all versions of aws-java-sdk-sns Show documentation
The AWS Java SDK for Amazon SNS module holds the client classes that are used for communicating with Amazon Simple Notification Service
The newest version!
/*
* Copyright 2012-2024 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.amazonaws.services.sns.message;
import com.amazonaws.SdkBaseException;
import com.amazonaws.SdkClientException;
import com.amazonaws.annotation.GuardedBy;
import com.amazonaws.annotation.SdkInternalApi;
import com.amazonaws.annotation.ThreadSafe;
import com.amazonaws.http.apache.utils.ApacheUtils;
import com.amazonaws.internal.FIFOCache;
import com.amazonaws.services.sns.util.SignatureChecker;
import com.amazonaws.util.IOUtils;
import com.fasterxml.jackson.databind.JsonNode;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.nio.charset.Charset;
import java.security.PublicKey;
import java.security.cert.CertificateExpiredException;
import java.security.cert.CertificateFactory;
import java.security.cert.CertificateNotYetValidException;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.net.ssl.SSLException;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
/**
* Verifies the signature of an SNS message.
*/
@ThreadSafe
@SdkInternalApi
class SignatureVerifier {
/**
* Field name of the SigningCertUrl.
*/
private static final String SIGNING_CERT_URL = "SigningCertURL";
private static final Pattern X509_PATTERN = Pattern.compile(
"^[\\s]*-----BEGIN [A-Z]+-----\\n[A-Za-z\\d+\\/\\n]+[=]{0,2}\\n-----END [A-Z]+-----[\\s]*$");
private final HttpClient client;
private final DefaultHostnameVerifier hostnameVerifier = new DefaultHostnameVerifier();
private final SignatureChecker signatureChecker = new SignatureChecker();
private final SigningCertUrlVerifier signingCertUrlVerifier;
/**
* Expected common name in the certificate used to validate the message signature.
*/
private final String expectedCertCommonName;
/**
* Cache the last two certificates so we don't have to download every time.
*/
@GuardedBy("this")
private final FIFOCache certificateCache = new FIFOCache(2);
/**
* @param client {@link HttpClient} instance to fetch certificate from.
* @param expectedSnsEndpoint Expected endpoint for the signing cert URL.
* @param expectedCertCommonName Expected common name for the certificate downloaded.
*/
SignatureVerifier(HttpClient client, String expectedSnsEndpoint, String expectedCertCommonName) {
this.client = client;
this.signingCertUrlVerifier = new SigningCertUrlVerifier(expectedSnsEndpoint);
this.expectedCertCommonName = expectedCertCommonName;
}
/**
* Verifies the signature of the SNS message by downloading the public certificate from SNS and computing
* the signature from the request properties. If the signature does not match, or if the cert is inaccessible or invalid
* and exception will be thrown.
*
* @param messageJson JSON message object.
*/
void verifySignature(JsonNode messageJson) {
if (!signatureChecker.verifySignature(toMap(messageJson), fetchPublicKey(messageJson))) {
throw new SdkClientException("Signature in SNS message was invalid");
}
}
/**
* Retrieve the certificate public key from the cache if available, otherwise download it from the URL
* in the message JSON.
*
* @param messageJson Message JSON.
* @return Public key of SNS certificate.
*/
private synchronized PublicKey fetchPublicKey(JsonNode messageJson) {
URI certUrl = URI.create(messageJson.get(SIGNING_CERT_URL).asText());
PublicKey publicKey = certificateCache.get(certUrl.toString());
if (publicKey == null) {
String certificateData = downloadCertWithRetries(certUrl);
validateCertificateData(certificateData);
publicKey = createPublicKey(certificateData);
certificateCache.add(certUrl.toString(), publicKey);
}
return publicKey;
}
/**
* Downloads the certificate from the provided URL, retrying once if applicable.
*
* @param certUrl URL to download certificate from.
* @return String contents of certificate.
*/
private String downloadCertWithRetries(URI certUrl) {
try {
return downloadCert(certUrl);
} catch (SdkBaseException e) {
if (isRetryable(e)) {
return downloadCert(certUrl);
} else {
throw e;
}
}
}
/**
* Determines if the exception received when downloading the certificate is retryable or not.
*
* @param e Exception when downloading certificate from provided URL.
* @return True if it is retryable, false otherwise.
*/
private boolean isRetryable(SdkBaseException e) {
if (e.getCause() instanceof IOException) {
return true;
} else if (e instanceof HttpException) {
// Only retry on 500s.
return ((HttpException) e).getStatusCode() / 100 == 5;
} else {
return false;
}
}
/**
* Downloads the certificate from the provided URL. Asserts that the endpoint is an SNS endpoint and that
* the certificate is vended over HTTPs.
*
* @param certUrl URL to download certificate from.
* @return String contents of certificate.
* @throws SdkClientException If certificate cannot be downloaded or URL is invalid.
*/
private String downloadCert(URI certUrl) {
try {
signingCertUrlVerifier.verifyCertUrl(certUrl);
HttpResponse response = client.execute(new HttpGet(certUrl));
if (ApacheUtils.isRequestSuccessful(response)) {
try {
return IOUtils.toString(response.getEntity().getContent());
} finally {
response.getEntity().getContent().close();
}
} else {
throw new HttpException("Could not download the certificate from SNS", response);
}
} catch (IOException e) {
throw new SdkClientException("Unable to download SNS certificate from " + certUrl.toString(), e);
}
}
/**
* Transforms the {@link JsonNode} into a map to integrate with the {@link SignatureChecker} utility.
*
* @param messageJson JSON of message.
* @return Transformed map.
*/
private Map toMap(JsonNode messageJson) {
Map fields = new HashMap(messageJson.size());
Iterator> jsonFields = messageJson.fields();
while (jsonFields.hasNext()) {
Map.Entry next = jsonFields.next();
fields.put(next.getKey(), next.getValue().asText());
}
return fields;
}
/**
* Build a PublicKey object from a cert
*
* @param cert The cert body
* @return A public key
*/
private PublicKey createPublicKey(String cert) {
try {
CertificateFactory fact = CertificateFactory.getInstance("X.509");
InputStream stream = new ByteArrayInputStream(cert.getBytes(Charset.forName("UTF-8")));
X509Certificate cer = (X509Certificate) fact.generateCertificate(stream);
validateCertificate(cer);
return cer.getPublicKey();
} catch (SdkBaseException e) {
throw e;
} catch (Exception e) {
throw new SdkClientException("Could not create public key from certificate", e);
}
}
/**
* Check that the certificate is valid and that the principal is actually SNS.
*
* @param cer Certificate to validate.
* @throws CertificateExpiredException
* @throws CertificateNotYetValidException
*/
private void validateCertificate(X509Certificate cer) throws CertificateExpiredException, CertificateNotYetValidException {
verifyHostname(cer);
cer.checkValidity();
}
/**
* Verifies the hostname in the certificate matches {@link #expectedCertCommonName}.
*
* @param cer Certificate to validate.
*/
private void verifyHostname(X509Certificate cer) {
try {
hostnameVerifier.verify(expectedCertCommonName, cer);
} catch (SSLException e) {
throw new SdkClientException("Certificate does not match expected common name: " + expectedCertCommonName, e);
}
}
/**
* Verifies that the downloaded certificate information matches the X509 format.
*
* @param data Text to check for correct format.
*/
private void validateCertificateData(String data) {
Matcher m = X509_PATTERN.matcher(data);
if (!m.matches()) {
throw new SdkClientException("Certificate does not match expected X509 PEM format.");
}
}
}