Skip to content

Commit fd40ca5

Browse files
authored
Fix wrong signature in SES (#567)
* Fix wrong signature in SES * Improve comment * Switch to 1.1 * Fix CS * Typo * Remove BC code and conflict with S3<1.1 * Update changelog * Fix CS * Replace ref variable by context object * Reword changelog * Reword
1 parent 645a0e9 commit fd40ca5

File tree

4 files changed

+84
-79
lines changed

4 files changed

+84
-79
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
### Fixed
1010

11+
- Fixed invalid chunking of request with large body for most clients but S3. This version removed the invalid code from SignerV4 to make sure requests are not chunked.
1112
- Use camelCase for all getter methods.
1213

1314
## 1.0.0

composer.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
"symfony/http-client-contracts": "^1.1.8 || ^2.0",
2121
"symfony/service-contracts": "^1.0 || ^2.0"
2222
},
23+
"conflict": {
24+
"async-aws/s3": "<1.1"
25+
},
2326
"extra": {
2427
"branch-alias": {
2528
"dev-master": "1.1-dev"

src/Signer/SignerV4.php

Lines changed: 15 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@
66
use AsyncAws\Core\Exception\InvalidArgument;
77
use AsyncAws\Core\Request;
88
use AsyncAws\Core\RequestContext;
9-
use AsyncAws\Core\Stream\FixedSizeStream;
10-
use AsyncAws\Core\Stream\IterableStream;
119
use AsyncAws\Core\Stream\ReadOnceResultStream;
12-
use AsyncAws\Core\Stream\RequestStream;
1310
use AsyncAws\Core\Stream\RewindableStream;
1411
use AsyncAws\Core\Stream\StringStream;
1512

@@ -21,8 +18,6 @@
2118
class SignerV4 implements Signer
2219
{
2320
private const ALGORITHM_REQUEST = 'AWS4-HMAC-SHA256';
24-
private const ALGORITHM_CHUNK = self::ALGORITHM_REQUEST . '-PAYLOAD';
25-
private const CHUNK_SIZE = 64 * 1024;
2621

2722
private const BLACKLIST_HEADERS = [
2823
'cache-control' => true,
@@ -101,6 +96,12 @@ protected function buildBodyDigest(Request $request, bool $isPresign): string
10196
return $hash;
10297
}
10398

99+
protected function convertBodyToStream(SigningContext $context): void
100+
{
101+
$request = $context->getRequest();
102+
$request->setBody(StringStream::create($request->getBody()));
103+
}
104+
104105
private function handleSignature(Request $request, Credentials $credentials, \DateTimeImmutable $now, \DateTimeImmutable $expires, bool $isPresign): void
105106
{
106107
$this->removePresign($request);
@@ -109,16 +110,17 @@ private function handleSignature(Request $request, Credentials $credentials, \Da
109110

110111
$this->buildTime($request, $now, $expires, $isPresign);
111112
$credentialScope = $this->buildCredentialString($request, $credentials, $now, $isPresign);
112-
$credentialString = \implode('/', $credentialScope);
113-
$signingKey = $this->buildSigningKey($credentials, $credentialScope);
114-
113+
$context = new SigningContext(
114+
$request,
115+
$now,
116+
\implode('/', $credentialScope),
117+
$this->buildSigningKey($credentials, $credentialScope)
118+
);
115119
if ($isPresign) {
116120
// Should be called before `buildBodyDigest` because this method may alter the body
117121
$this->convertBodyToQuery($request);
118122
} else {
119-
// $signature does not exists but passed by reference then computed buildSignature
120-
$signature = '';
121-
$this->convertBodyToStream($request, $now, $credentialString, $signingKey, $signature);
123+
$this->convertBodyToStream($context);
122124
}
123125

124126
$bodyDigest = $this->buildBodyDigest($request, $isPresign);
@@ -130,8 +132,8 @@ private function handleSignature(Request $request, Credentials $credentials, \Da
130132

131133
$canonicalHeaders = $this->buildCanonicalHeaders($request, $isPresign);
132134
$canonicalRequest = $this->buildCanonicalRequest($request, $canonicalHeaders, $bodyDigest);
133-
$stringToSign = $this->buildStringToSign($now, $credentialString, $canonicalRequest);
134-
$signature = $this->buildSignature($stringToSign, $signingKey);
135+
$stringToSign = $this->buildStringToSign($context->getNow(), $context->getCredentialString(), $canonicalRequest);
136+
$context->setSignature($signature = $this->buildSignature($stringToSign, $context->getSigningKey()));
135137

136138
if ($isPresign) {
137139
$request->setQueryAttribute('X-Amz-Signature', $signature);
@@ -254,57 +256,6 @@ private function convertBodyToQuery(Request $request): void
254256
$request->setBody(StringStream::create(''));
255257
}
256258

257-
private function convertBodyToStream(Request $request, \DateTimeImmutable $now, string $credentialString, string $signingKey, string &$signature): void
258-
{
259-
$body = $request->getBody();
260-
if ($request->hasHeader('content-length')) {
261-
$contentLength = (int) $request->getHeader('content-length');
262-
} else {
263-
$contentLength = $body->length();
264-
}
265-
266-
// If content length is unknown, use the rewindable stream to read it once locally in order to get the length
267-
if (null === $contentLength) {
268-
$request->setBody($body = RewindableStream::create($body));
269-
$body->read();
270-
$contentLength = $body->length();
271-
}
272-
273-
// no need to stream small body. It's simple to convert it to string directly
274-
if ($contentLength < self::CHUNK_SIZE) {
275-
$request->setBody($body = StringStream::create($body));
276-
277-
return;
278-
}
279-
280-
// Convert the body into a chunked stream
281-
$request->setHeader('content-encoding', 'aws-chunked');
282-
$request->setHeader('x-amz-decoded-content-length', (string) $contentLength);
283-
$request->setHeader('x-amz-content-sha256', 'STREAMING-' . self::ALGORITHM_CHUNK);
284-
285-
// Compute size of content + metadata used sign each Chunk
286-
$chunkCount = (int) ceil($contentLength / self::CHUNK_SIZE);
287-
$fullChunkCount = $chunkCount * self::CHUNK_SIZE === $contentLength ? $chunkCount : ($chunkCount - 1);
288-
$metaLength = \strlen(";chunk-signature=\r\n\r\n") + 64;
289-
$request->setHeader('content-length', (string) ($contentLength + $fullChunkCount * ($metaLength + \strlen((string) dechex(self::CHUNK_SIZE))) + ($chunkCount - $fullChunkCount) * ($metaLength + \strlen((string) dechex($contentLength % self::CHUNK_SIZE))) + $metaLength + 1));
290-
291-
$body = IterableStream::create((function (RequestStream $body) use ($now, $credentialString, $signingKey, &$signature): iterable {
292-
foreach (FixedSizeStream::create($body, self::CHUNK_SIZE) as $chunk) {
293-
$stringToSign = $this->buildChunkStringToSign($now, $credentialString, $signature, $chunk);
294-
$signature = $this->buildSignature($stringToSign, $signingKey);
295-
296-
yield sprintf("%s;chunk-signature=%s\r\n", dechex(\strlen($chunk)), $signature) . "$chunk\r\n";
297-
}
298-
299-
$stringToSign = $this->buildChunkStringToSign($now, $credentialString, $signature, '');
300-
$signature = $this->buildSignature($stringToSign, $signingKey);
301-
302-
yield sprintf("%s;chunk-signature=%s\r\n\r\n", dechex(0), $signature);
303-
})($body));
304-
305-
$request->setBody($body);
306-
}
307-
308259
private function buildCanonicalHeaders(Request $request, bool $isPresign): array
309260
{
310261
// Case-insensitively aggregate all of the headers.
@@ -393,21 +344,6 @@ private function buildStringToSign(\DateTimeImmutable $now, string $credentialSt
393344
]);
394345
}
395346

396-
private function buildChunkStringToSign(\DateTimeImmutable $now, string $credentialString, string $signature, string $chunk): string
397-
{
398-
static $emptyHash;
399-
$emptyHash = $emptyHash ?? hash('sha256', '');
400-
401-
return implode("\n", [
402-
self::ALGORITHM_CHUNK,
403-
$now->format('Ymd\THis\Z'),
404-
$credentialString,
405-
$signature,
406-
$emptyHash,
407-
hash('sha256', $chunk),
408-
]);
409-
}
410-
411347
private function buildSigningKey(Credentials $credentials, array $credentialScope): string
412348
{
413349
$signingKey = 'AWS4' . $credentials->getSecretKey();

src/Signer/SigningContext.php

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
namespace AsyncAws\Core\Signer;
4+
5+
use AsyncAws\Core\Request;
6+
7+
/**
8+
* @internal
9+
*
10+
* @author Jérémy Derussé <jeremy@derusse.com>
11+
*/
12+
class SigningContext
13+
{
14+
private $request;
15+
16+
private $now;
17+
18+
private $credentialString;
19+
20+
private $signingKey;
21+
22+
private $signature = '';
23+
24+
public function __construct(
25+
Request $request,
26+
\DateTimeImmutable $now,
27+
string $credentialString,
28+
string $signingKey
29+
) {
30+
$this->request = $request;
31+
$this->now = $now;
32+
$this->credentialString = $credentialString;
33+
$this->signingKey = $signingKey;
34+
}
35+
36+
public function getRequest(): Request
37+
{
38+
return $this->request;
39+
}
40+
41+
public function getNow(): \DateTimeImmutable
42+
{
43+
return $this->now;
44+
}
45+
46+
public function getCredentialString(): string
47+
{
48+
return $this->credentialString;
49+
}
50+
51+
public function getSigningKey(): string
52+
{
53+
return $this->signingKey;
54+
}
55+
56+
public function getSignature(): string
57+
{
58+
return $this->signature;
59+
}
60+
61+
public function setSignature(string $signature): void
62+
{
63+
$this->signature = $signature;
64+
}
65+
}

0 commit comments

Comments
 (0)