<?php 
namespace Aws\Sns; 
 
use Aws\Sns\Exception\InvalidSnsMessageException; 
 
/** 
 * Uses openssl to verify SNS messages to ensure that they were sent by AWS. 
 */ 
class MessageValidator 
{ 
    const SIGNATURE_VERSION_1 = '1'; 
 
    /** 
     * @var callable Callable used to download the certificate content. 
     */ 
    private $certClient; 
 
    /** @var string */ 
    private $hostPattern; 
 
    /** 
     * @var string  A pattern that will match all regional SNS endpoints, e.g.: 
     *                  - sns.<region>.amazonaws.com        (AWS) 
     *                  - sns.us-gov-west-1.amazonaws.com   (AWS GovCloud) 
     *                  - sns.cn-north-1.amazonaws.com.cn   (AWS China) 
     */ 
    private static $defaultHostPattern 
        = '/^sns\.[a-zA-Z0-9\-]{3,}\.amazonaws\.com(\.cn)?$/'; 
 
    /** 
     * Constructs the Message Validator object and ensures that openssl is 
     * installed. 
     * 
     * @param callable $certClient Callable used to download the certificate. 
     *                             Should have the following function signature: 
     *                             `function (string $certUrl) : string $certContent` 
     * @param string $hostNamePattern 
     */ 
    public function __construct( 
        callable $certClient = null, 
        $hostNamePattern = '' 
    ) { 
        $this->certClient = $certClient ?: 'file_get_contents'; 
        $this->hostPattern = $hostNamePattern ?: self::$defaultHostPattern; 
    } 
 
    /** 
     * Validates a message from SNS to ensure that it was delivered by AWS. 
     * 
     * @param Message $message Message to validate. 
     * 
     * @throws InvalidSnsMessageException If the cert cannot be retrieved or its 
     *                                    source verified, or the message 
     *                                    signature is invalid. 
     */ 
    public function validate(Message $message) 
    { 
        // Get the certificate. 
        $this->validateUrl($message['SigningCertURL']); 
        $certificate = call_user_func($this->certClient, $message['SigningCertURL']); 
 
        // Extract the public key. 
        $key = openssl_get_publickey($certificate); 
        if (!$key) { 
            throw new InvalidSnsMessageException( 
                'Cannot get the public key from the certificate.' 
            ); 
        } 
 
        // Verify the signature of the message. 
        $content = $this->getStringToSign($message); 
        $signature = base64_decode($message['Signature']); 
        if (!openssl_verify($content, $signature, $key, OPENSSL_ALGO_SHA1)) { 
            throw new InvalidSnsMessageException( 
                'The message signature is invalid.' 
            ); 
        } 
    } 
 
    /** 
     * Determines if a message is valid and that is was delivered by AWS. This 
     * method does not throw exceptions and returns a simple boolean value. 
     * 
     * @param Message $message The message to validate 
     * 
     * @return bool 
     */ 
    public function isValid(Message $message) 
    { 
        try { 
            $this->validate($message); 
            return true; 
        } catch (InvalidSnsMessageException $e) { 
            return false; 
        } 
    } 
 
    /** 
     * Builds string-to-sign according to the SNS message spec. 
     * 
     * @param Message $message Message for which to build the string-to-sign. 
     * 
     * @return string 
     * @link http://docs.aws.amazon.com/sns/latest/gsg/SendMessageToHttp.verify.signature.html 
     */ 
    public function getStringToSign(Message $message) 
    { 
        static $signableKeys = [ 
            'Message', 
            'MessageId', 
            'Subject', 
            'SubscribeURL', 
            'Timestamp', 
            'Token', 
            'TopicArn', 
            'Type', 
        ]; 
 
        if ($message['SignatureVersion'] !== self::SIGNATURE_VERSION_1) { 
            throw new InvalidSnsMessageException( 
                "The SignatureVersion \"{$message['SignatureVersion']}\" is not supported." 
            ); 
        } 
 
        $stringToSign = ''; 
        foreach ($signableKeys as $key) { 
            if (isset($message[$key])) { 
                $stringToSign .= "{$key}\n{$message[$key]}\n"; 
            } 
        } 
 
        return $stringToSign; 
    } 
 
    /** 
     * Ensures that the URL of the certificate is one belonging to AWS, and not 
     * just something from the amazonaws domain, which could include S3 buckets. 
     * 
     * @param string $url Certificate URL 
     * 
     * @throws InvalidSnsMessageException if the cert url is invalid. 
     */ 
    private function validateUrl($url) 
    { 
        $parsed = parse_url($url); 
        if (empty($parsed['scheme']) 
            || empty($parsed['host']) 
            || $parsed['scheme'] !== 'https' 
            || substr($url, -4) !== '.pem' 
            || !preg_match($this->hostPattern, $parsed['host']) 
        ) { 
            throw new InvalidSnsMessageException( 
                'The certificate is located on an invalid domain.' 
            ); 
        } 
    } 
} 
 
 |