All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.amazonaws.services.sns.message.SignatureVerifier Maven / Gradle / Ivy

Go to download

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

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy