Skip to content

Commit 926cf23

Browse files
committed
Merge pull request #6 from aws/move-string-to-sign
Moved getStringToSign from the Message to the MessageValidator
2 parents 1711c5b + 69aeb2e commit 926cf23

File tree

4 files changed

+124
-139
lines changed

4 files changed

+124
-139
lines changed

src/Message.php

Lines changed: 22 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -7,34 +7,14 @@
77
class Message implements \ArrayAccess, \IteratorAggregate
88
{
99
private static $requiredKeys = [
10-
'__default' => [
11-
'Message',
12-
'MessageId',
13-
'Timestamp',
14-
'TopicArn',
15-
'Type',
16-
'Signature',
17-
'SigningCertURL',
18-
],
19-
'SubscriptionConfirmation' => [
20-
'SubscribeURL',
21-
'Token',
22-
],
23-
'UnsubscribeConfirmation' => [
24-
'SubscribeURL',
25-
'Token',
26-
],
27-
];
28-
29-
private static $signableKeys = [
3010
'Message',
3111
'MessageId',
32-
'Subject',
33-
'SubscribeURL',
3412
'Timestamp',
35-
'Token',
3613
'TopicArn',
3714
'Type',
15+
'Signature',
16+
'SigningCertURL',
17+
'SignatureVersion',
3818
];
3919

4020
/** @var array The message data */
@@ -48,10 +28,12 @@ class Message implements \ArrayAccess, \IteratorAggregate
4828
*/
4929
public static function fromRawPostData()
5030
{
31+
// Make sure the SNS-provided header exists.
5132
if (!isset($_SERVER['HTTP_X_AMZ_SNS_MESSAGE_TYPE'])) {
5233
throw new \RuntimeException('SNS message type header not provided.');
5334
}
5435

36+
// Read the raw POST data and JSON-decode it.
5537
$data = json_decode(file_get_contents('php://input'), true);
5638
if (JSON_ERROR_NONE !== json_last_error() || !is_array($data)) {
5739
throw new \RuntimeException('Invalid POST data.');
@@ -70,28 +52,12 @@ public static function fromRawPostData()
7052
*/
7153
public function __construct(array $data)
7254
{
73-
// Make sure the type key is set
74-
if (!isset($data['Type'])) {
75-
throw new \InvalidArgumentException(
76-
'The "Type" must be provided to instantiate a Message object.'
77-
);
78-
}
79-
80-
// Determine the required keys for this message type.
81-
$requiredKeys = array_merge(
82-
self::$requiredKeys['__default'],
83-
isset(self::$requiredKeys[$data['Type']]) ?
84-
self::$requiredKeys[$data['Type']]
85-
: []
86-
);
87-
88-
// Ensure that all the required keys are provided.
89-
foreach ($requiredKeys as $key) {
90-
if (!isset($data[$key])) {
91-
throw new \InvalidArgumentException(
92-
"Missing key {$key} in the provided data."
93-
);
94-
}
55+
// Ensure that all the required keys for the message's type are present.
56+
$this->validateRequiredKeys($data, self::$requiredKeys);
57+
if ($data['Type'] === 'SubscriptionConfirmation'
58+
|| $data['Type'] === 'UnsubscribeConfirmation'
59+
) {
60+
$this->validateRequiredKeys($data, ['SubscribeURL', 'Token']);
9561
}
9662

9763
$this->data = $data;
@@ -102,24 +68,6 @@ public function getIterator()
10268
return new \ArrayIterator($this->data);
10369
}
10470

105-
/**
106-
* Builds a newline delimited string-to-sign according to the specs.
107-
*
108-
* @return string
109-
* @link http://docs.aws.amazon.com/sns/latest/gsg/SendMessageToHttp.verify.signature.html
110-
*/
111-
public function getStringToSign()
112-
{
113-
$stringToSign = '';
114-
foreach (self::$signableKeys as $key) {
115-
if (isset($this[$key])) {
116-
$stringToSign .= "{$key}\n{$this[$key]}\n";
117-
}
118-
}
119-
120-
return $stringToSign;
121-
}
122-
12371
public function offsetExists($key)
12472
{
12573
return isset($this->data[$key]);
@@ -149,4 +97,15 @@ public function toArray()
14997
{
15098
return $this->data;
15199
}
100+
101+
private function validateRequiredKeys(array $data, array $keys)
102+
{
103+
foreach ($keys as $key) {
104+
if (!isset($data[$key])) {
105+
throw new \InvalidArgumentException(
106+
"\"{$key}\" is required to verify the SNS Message."
107+
);
108+
}
109+
}
110+
}
152111
}

src/MessageValidator.php

Lines changed: 60 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -8,55 +8,51 @@
88
*/
99
class MessageValidator
1010
{
11-
const SUPPORTED_SIGNATURE_VERSION = '1';
11+
const SIGNATURE_VERSION_1 = '1';
1212

1313
/**
14-
* @var callable
14+
* @var callable Callable used to download the certificate content.
1515
*/
16-
private $remoteFileReader;
16+
private $certClient;
1717

1818
/**
19-
* Constructs the Message Validator object and ensures that openssl is
20-
* installed.
21-
*
22-
* @param callable $remoteFileReader
19+
* @param callable $certClient Callable used to download the certificate.
20+
* Should have the following function signature:
21+
* `function (string $certUrl) : string $certContent`
2322
*
2423
* @throws \RuntimeException If openssl is not installed
2524
*/
26-
public function __construct(callable $remoteFileReader = null)
25+
public function __construct(callable $certClient = null)
2726
{
28-
$this->remoteFileReader = $remoteFileReader ?: 'file_get_contents';
27+
$this->certClient = $certClient ?: 'file_get_contents';
2928
}
3029

3130
/**
32-
* Validates a message from SNS to ensure that it was delivered by AWS
31+
* Validates a message from SNS to ensure that it was delivered by AWS.
3332
*
34-
* @param Message $message The message to validate
33+
* @param Message $message Message to validate.
3534
*
36-
* @throws InvalidSnsMessageException If the certificate cannot be
37-
* retrieved, if the certificate's source cannot be verified, or if the
38-
* message's signature is invalid.
35+
* @throws InvalidSnsMessageException If the cert cannot be retrieved or its
36+
* source verified, or the message
37+
* signature is invalid.
3938
*/
4039
public function validate(Message $message)
4140
{
42-
$this->validateSignatureVersion($message['SignatureVersion']);
43-
44-
$certUrl = $message['SigningCertURL'];
45-
$this->validateUrl($certUrl);
41+
// Get the certificate.
42+
$this->validateUrl($message['SigningCertURL']);
43+
$certificate = call_user_func($this->certClient, $message['SigningCertURL']);
4644

47-
// Get the cert itself and extract the public key
48-
$certificate = call_user_func($this->remoteFileReader, $certUrl);
45+
// Extract the public key.
4946
$key = openssl_get_publickey($certificate);
5047
if (!$key) {
5148
throw new InvalidSnsMessageException(
5249
'Cannot get the public key from the certificate.'
5350
);
5451
}
5552

56-
// Verify the signature of the message
57-
$content = $message->getStringToSign();
53+
// Verify the signature of the message.
54+
$content = $this->getStringToSign($message);
5855
$signature = base64_decode($message['Signature']);
59-
6056
if (!openssl_verify($content, $signature, $key, OPENSSL_ALGO_SHA1)) {
6157
throw new InvalidSnsMessageException(
6258
'The message signature is invalid.'
@@ -83,12 +79,49 @@ public function isValid(Message $message)
8379
}
8480

8581
/**
86-
* Ensures that the url of the certificate is one belonging to AWS, and not
87-
* just something from the amazonaws domain, which includes S3 buckets.
82+
* Builds string-to-sign according to the SNS message spec.
83+
*
84+
* @param Message $message Message for which to build the string-to-sign.
85+
*
86+
* @return string
87+
* @link http://docs.aws.amazon.com/sns/latest/gsg/SendMessageToHttp.verify.signature.html
88+
*/
89+
public function getStringToSign(Message $message)
90+
{
91+
static $signableKeys = [
92+
'Message',
93+
'MessageId',
94+
'Subject',
95+
'SubscribeURL',
96+
'Timestamp',
97+
'Token',
98+
'TopicArn',
99+
'Type',
100+
];
101+
102+
if ($message['SignatureVersion'] !== self::SIGNATURE_VERSION_1) {
103+
throw new InvalidSnsMessageException(
104+
"The SignatureVersion \"{$message['SignatureVersion']}\" is not supported."
105+
);
106+
}
107+
108+
$stringToSign = '';
109+
foreach ($signableKeys as $key) {
110+
if (isset($message[$key])) {
111+
$stringToSign .= "{$key}\n{$message[$key]}\n";
112+
}
113+
}
114+
115+
return $stringToSign;
116+
}
117+
118+
/**
119+
* Ensures that the URL of the certificate is one belonging to AWS, and not
120+
* just something from the amazonaws domain, which could include S3 buckets.
88121
*
89-
* @param string $url
122+
* @param string $url Certificate URL
90123
*
91-
* @throws InvalidSnsMessageException if the cert url is invalid
124+
* @throws InvalidSnsMessageException if the cert url is invalid.
92125
*/
93126
private function validateUrl($url)
94127
{
@@ -106,13 +139,4 @@ private function validateUrl($url)
106139
);
107140
}
108141
}
109-
110-
private function validateSignatureVersion($version)
111-
{
112-
if ($version !== self::SUPPORTED_SIGNATURE_VERSION) {
113-
throw new InvalidSnsMessageException(
114-
"Only v1 signatures can be validated; v{$version} provided"
115-
);
116-
}
117-
}
118142
}

tests/MessageTest.php

Lines changed: 10 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,16 +7,17 @@
77
class MessageTest extends \PHPUnit_Framework_TestCase
88
{
99
public $messageData = array(
10-
'Message' => 'a',
11-
'MessageId' => 'b',
12-
'Timestamp' => 'c',
13-
'TopicArn' => 'd',
14-
'Type' => 'e',
15-
'Subject' => 'f',
16-
'Signature' => 'g',
10+
'Message' => 'a',
11+
'MessageId' => 'b',
12+
'Timestamp' => 'c',
13+
'TopicArn' => 'd',
14+
'Type' => 'e',
15+
'Subject' => 'f',
16+
'Signature' => 'g',
17+
'SignatureVersion' => '1',
1718
'SigningCertURL' => 'h',
18-
'SubscribeURL' => 'i',
19-
'Token' => 'j',
19+
'SubscribeURL' => 'i',
20+
'Token' => 'j',
2021
);
2122

2223
public function testGetters()
@@ -86,31 +87,4 @@ public function testCreateFromRawPostFailsWithMissingData()
8687
Message::fromRawPostData();
8788
unset($_SERVER['HTTP_X_AMZ_SNS_MESSAGE_TYPE']);
8889
}
89-
90-
public function testBuildsStringToSignCorrectly( ) {
91-
$message = new Message([
92-
'TopicArn' => 'd',
93-
'Message' => 'a',
94-
'Timestamp' => 'c',
95-
'Type' => 'e',
96-
'MessageId' => 'b',
97-
'FooBar' => 'f',
98-
'Signature' => true,
99-
'SigningCertURL' => true,
100-
]);
101-
$stringToSign = <<< STRINGTOSIGN
102-
Message
103-
a
104-
MessageId
105-
b
106-
Timestamp
107-
c
108-
TopicArn
109-
d
110-
Type
111-
e
112-
113-
STRINGTOSIGN;
114-
$this->assertEquals($stringToSign, $message->getStringToSign());
115-
}
11690
}

tests/MessageValidatorTest.php

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public static function tearDownAfterClass()
2727

2828
public function testIsValidReturnsFalseOnFailedValidation()
2929
{
30-
$validator = new MessageValidator();
30+
$validator = new MessageValidator($this->getMockHttpClient());
3131
$message = $this->getTestMessage([
3232
'SignatureVersion' => '2',
3333
]);
@@ -36,11 +36,11 @@ public function testIsValidReturnsFalseOnFailedValidation()
3636

3737
/**
3838
* @expectedException \Aws\Sns\Exception\InvalidSnsMessageException
39-
* @expectedExceptionMessage Only v1 signatures can be validated; v2 provided
39+
* @expectedExceptionMessage The SignatureVersion "2" is not supported.
4040
*/
4141
public function testValidateFailsWhenSignatureVersionIsInvalid()
4242
{
43-
$validator = new MessageValidator();
43+
$validator = new MessageValidator($this->getMockCertServerClient());
4444
$message = $this->getTestMessage([
4545
'SignatureVersion' => '2',
4646
]);
@@ -90,12 +90,35 @@ public function testValidateSucceedsWhenMessageIsValid()
9090
$message = $this->getTestMessage();
9191

9292
// Get the signature for a real message
93-
$message['Signature'] = $this->getSignature($message->getStringToSign());
93+
$message['Signature'] = $this->getSignature($validator->getStringToSign($message));
9494

9595
// The message should validate
9696
$this->assertTrue($validator->isValid($message));
9797
}
9898

99+
public function testBuildsStringToSignCorrectly()
100+
{
101+
$validator = new MessageValidator();
102+
$stringToSign = <<< STRINGTOSIGN
103+
Message
104+
foo
105+
MessageId
106+
bar
107+
Timestamp
108+
1435697129
109+
TopicArn
110+
baz
111+
Type
112+
Notification
113+
114+
STRINGTOSIGN;
115+
116+
$this->assertEquals(
117+
$stringToSign,
118+
$validator->getStringToSign($this->getTestMessage())
119+
);
120+
}
121+
99122
/**
100123
* @param array $customData
101124
*
@@ -140,3 +163,8 @@ private function getSignature($stringToSign)
140163
return base64_encode($signature);
141164
}
142165
}
166+
167+
function time()
168+
{
169+
return 1435697129;
170+
}

0 commit comments

Comments
 (0)