Skip to content

Commit

Permalink
bugfix: cloudfront sigv4a
Browse files Browse the repository at this point in the history
  • Loading branch information
Sean O'Brien committed Dec 14, 2023
1 parent 9ae3bfd commit 9a7a3a9
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 133 deletions.
6 changes: 6 additions & 0 deletions features/crt/cloudfront-kvs.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
@crt @integ @cloudfront-kvs
Feature: Cloudfront Kvs Sigv4a

Scenario: Describe a cloudfront kvs
Given I have a cloudfront client and I have a key-value store
Then I can describe my key-value store using sigv4a
36 changes: 36 additions & 0 deletions src/Signature/S3SignatureV4.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@
namespace Aws\Signature;

use Aws\Credentials\CredentialsInterface;
use AWS\CRT\Auth\SignatureType;
use AWS\CRT\Auth\SigningAlgorithm;
use AWS\CRT\Auth\SigningConfigAWS;
use Psr\Http\Message\RequestInterface;

/**
Expand Down Expand Up @@ -41,6 +44,39 @@ public function signRequest(
return $this->signWithV4a($credentials, $request, $signingService);
}

/**
* @param CredentialsInterface $credentials
* @param RequestInterface $request
* @param $signingService
* @param SigningConfigAWS|null $signingConfig
* @return RequestInterface
*
* Instantiates a separate sigv4a signing config. All services except S3
* use double encoding. All services except S3 require path normalization.
*/
protected function signWithV4a(
CredentialsInterface $credentials,
RequestInterface $request,
$signingService,
SigningConfigAWS $signingConfig = null
){
$this->verifyCRTLoaded();
$credentials_provider = $this->createCRTStaticCredentialsProvider($credentials);
$signingConfig = new SigningConfigAWS([
'algorithm' => SigningAlgorithm::SIGv4_ASYMMETRIC,
'signature_type' => SignatureType::HTTP_REQUEST_HEADERS,
'credentials_provider' => $credentials_provider,
'signed_body_value' => $this->getPayload($request),
'region' => "*",
'should_normalize_uri_path' => false,
'use_double_uri_encode' => false,
'service' => $signingService,
'date' => time(),
]);

return parent::signWithV4a($credentials, $request, $signingService, $signingConfig);
}

/**
* Always add a x-amz-content-sha-256 for data integrity.
*
Expand Down
22 changes: 14 additions & 8 deletions src/Signature/SignatureV4.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
use Aws\Credentials\CredentialsInterface;
use AWS\CRT\Auth\Signable;
use AWS\CRT\Auth\SignatureType;
use AWS\CRT\Auth\SignedBodyHeaderType;
use AWS\CRT\Auth\Signing;
use AWS\CRT\Auth\SigningAlgorithm;
use AWS\CRT\Auth\SigningConfigAWS;
Expand Down Expand Up @@ -446,7 +447,7 @@ private function buildRequest(array $req)
);
}

private function verifyCRTLoaded()
protected function verifyCRTLoaded()
{
if (!extension_loaded('awscrt')) {
throw new CommonRuntimeException(
Expand All @@ -457,7 +458,7 @@ private function verifyCRTLoaded()
}
}

private function createCRTStaticCredentialsProvider($credentials)
protected function createCRTStaticCredentialsProvider($credentials)
{
return new StaticCredentialsProvider([
'access_key_id' => $credentials->getAccessKeyId(),
Expand All @@ -472,7 +473,7 @@ private function removeIllegalV4aHeaders(&$request)
self::AMZ_CONTENT_SHA256_HEADER,
"aws-sdk-invocation-id",
"aws-sdk-retry",
'x-amz-region-set'
'x-amz-region-set',
];
$storedHeaders = [];

Expand Down Expand Up @@ -502,15 +503,20 @@ private function CRTRequestFromGuzzleRequest($request)
* @param $signingService
* @return RequestInterface
*/
protected function signWithV4a(CredentialsInterface $credentials, RequestInterface $request, $signingService)
{
protected function signWithV4a(
CredentialsInterface $credentials,
RequestInterface $request,
$signingService,
SigningConfigAWS $signingConfig = null
){
$this->verifyCRTLoaded();
$credentials_provider = $this->createCRTStaticCredentialsProvider($credentials);
$signingConfig = new SigningConfigAWS([
$signingConfig = $signingConfig ?: new SigningConfigAWS([
'algorithm' => SigningAlgorithm::SIGv4_ASYMMETRIC,
'signature_type' => SignatureType::HTTP_REQUEST_HEADERS,
'credentials_provider' => $credentials_provider,
'credentials_provider' => $this->createCRTStaticCredentialsProvider($credentials),
'signed_body_value' => $this->getPayload($request),
'should_normalize_uri_path' => true, //works for eventbridge,
'use_double_uri_encode' => true,
'region' => "*",
'service' => $signingService,
'date' => time(),
Expand Down
186 changes: 61 additions & 125 deletions tests/Integ/CrtContext.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
<?php
namespace Aws\Test\Integ;

use Aws\CloudFront\CloudFrontClient;
use Aws\CloudFrontKeyValueStore\CloudFrontKeyValueStoreClient;
use Aws\EventBridge\EventBridgeClient;
use Aws\EventBridge\Exception\EventBridgeException;
use Aws\Route53\Route53Client;
use Aws\Sts\StsClient;
use Behat\Behat\Context\Context;
use Behat\Behat\Context\SnippetAcceptingContext;
Expand All @@ -21,9 +22,6 @@ class CrtContext implements Context, SnippetAcceptingContext

private static $multiRegionAccessPoint;
private static $tempFile;
private static $eventBuses = [];
private static $healthCheckArn;
private static $healthCheckId;
private static $globalEndpoint;

/** @var EventBridgeClient */
Expand All @@ -36,6 +34,8 @@ class CrtContext implements Context, SnippetAcceptingContext
private $stream;
/** @var RequestInterface */
private $presignedRequest;
/** @var string */
private $keyvaluestore;

/**
* @BeforeFeature @mrap
Expand Down Expand Up @@ -79,116 +79,23 @@ public static function deleteTempFile()
/**
* @BeforeFeature @eventbridge
*/
public static function setEventBuses()
public static function setGlobalEndpoint()
{
$usEastClient = new EventBridgeClient([
$eventBridgeClient = new EventBridgeClient([
'region' => 'us-east-1',
'version' => 'latest'
]);
$eventBusEast = $usEastClient->describeEventBus(['Name' => 'default'])['Arn'];
$eventBusWest = str_replace('us-east-1', 'us-west-2', $eventBusEast);
array_push(
self::$eventBuses,
['EventBusArn' => $eventBusEast],
['EventBusArn' => $eventBusWest]
);
}

/**
* @BeforeFeature @eventbridge
*/
public static function createHealthCheck()
{
$route53Client = new Route53Client([
'region' => 'us-east-1',
'version' => 'latest'
]);
$response = $route53Client->createHealthCheck([
'CallerReference' => uniqid(),
'HealthCheckConfig' => [
'HealthThreshold' => 0,
'Type' => 'CALCULATED'
]
]);
self::$healthCheckId = $response['HealthCheck']['Id'];
self::$healthCheckArn = "arn:aws:route53:::healthcheck/{$response['HealthCheck']['Id']}";
}

/**
* @BeforeFeature @eventbridge
*/
public static function createGlobalEndpoint()
{
$eventBridgeClient = new EventBridgeClient([
'region' => 'us-east-1',
'version' => 'latest'
]);
$currentEndpoints = $eventBridgeClient->listEndpoints();
$testEndpointExists = false;
foreach ($currentEndpoints['Endpoints'] as $endpoint) {
if ($endpoint['Name'] == 'test-endpoint') {
$testEndpointExists = true;
break;
}
}
if (!$testEndpointExists) {
$eventBridgeClient->createEndpoint([
'Name' => 'test-endpoint',
'EventBuses' => self::$eventBuses,
'ReplicationConfig' => [
'State' => 'DISABLED'
],
'RoutingConfig' => [
'FailoverConfig' => [
'Primary' => [
'HealthCheck' => self::$healthCheckArn
],
'Secondary' => [
'Route' => 'us-west-2'
]
]
]
try {
$result = $eventBridgeClient->describeEndpoint([
'Name' => 'test-endpoint'
]);
}

$attempts = 0;
$active = false;
$result = null;
while (!$active && $attempts <= 5) {
$result = $eventBridgeClient->describeEndpoint(['Name' => 'test-endpoint']);
$active = $result['State'] === 'ACTIVE';
sleep((int) pow(1.2, $attempts));
$attempts++;
self::$globalEndpoint = $result['EndpointId'];
} catch (EventBridgeException $e) {
throw new \Exception('failed to set global endpiont');
}
self::$globalEndpoint = $result['EndpointId'];
}

/**
* @AfterFeature @eventbridge
*/
public static function deleteGlobalEndpoint()
{
$eventBridgeClient = new EventBridgeClient([
'region' => 'us-east-1',
'version' => 'latest'
]);
$eventBridgeClient->deleteEndpoint([
'Name' => 'test-endpoint'
]);
}

/**
* @AfterFeature @eventbridge
*/
public static function deleteHealthCheck()
{
$route53Client = new Route53Client([
'region' => 'us-east-1',
'version' => 'latest'
]);
$route53Client->deleteHealthCheck([
'HealthCheckId' => self::$healthCheckId
]);
}

/**
Expand Down Expand Up @@ -239,9 +146,7 @@ public function iHaveAS3ClientAndIHaveAMultiRegionAccessPoint()
public function iCanConfirmMyAccessPointHasAtLeastOneObject()
{
$result = $this->s3Client->listObjectsV2(['Bucket' => self::$multiRegionAccessPoint]);
Assert::assertTrue(
!empty($result['Contents'])
);
Assert::assertNotTrue(empty($result['Contents']));
}

/**
Expand All @@ -265,7 +170,7 @@ public function iCanConfirmTheObjectHasBeenUpdated()
$result = $this->s3Client->getObject(['Bucket' => self::$multiRegionAccessPoint, 'Key' => 'mrap-test']);
Assert::assertEquals(
'test',
(string) $result['Body']
(string)$result['Body']
);
}

Expand All @@ -275,7 +180,8 @@ public function iCanConfirmTheObjectHasBeenUpdated()
public function iCreateAPreSignedUrlForACommandWith(
$commandName,
TableNode $table
) {
)
{
$args = ['Bucket' => self::$multiRegionAccessPoint];
foreach ($table as $row) {
$args[$row['key']] = $row['value'];
Expand Down Expand Up @@ -344,20 +250,50 @@ public function iHaveAnEventbridgeClientAndIHaveAnEventConfiguration()
*/
public function iCanUploadAnEventUsingMyGlobalEndpoint()
{
$attempts = 0;
while ($attempts <= 5) {
try {
$this->eventBridgeClient->putEvents([
'EndpointId' => self::$globalEndpoint,
'Entries' => $this->eventConfig
]);
} catch (EventBridgeException $e) {
if (strpos($e->getMessage(), 'Could not resolve host') === false) {
throw $e;
}
$attempts++;
sleep((int) pow(1.2, $attempts));
}
try {
$result = $this->eventBridgeClient->putEvents([
'EndpointId' => self::$globalEndpoint,
'Entries' => $this->eventConfig
]);
Assert::assertEquals('200', $result['@metadata']['statusCode']);
} catch (EventBridgeException $e) {
throw new \Exception('Failed to send events to global endpoint');
}
}
}

/**
* @Given I have a cloudfront client and I have a key-value store
*/
public function iHaveACloudfrontClientAndIHaveAKeyValueStore()
{
$cloudfrontClient = new CloudfrontClient([
'region' => 'us-east-1',
'version' => 'latest'
]);

$result = $cloudfrontClient->listKeyValueStores();
$this->keyvaluestore = $result['KeyValueStoreList']['Items']['0']['ARN'];
}

/**
* @Then I can describe my key-value store using sigv4a
*/
public function iCanDescribeMyKeyValueStoreUsingSigV4a()
{
$keyValueStoreClient = new CloudFrontKeyValueStoreClient([
'region' => 'us-east-1',
'version' => 'latest'
]);

try {
$result = $keyValueStoreClient->describeKeyValueStore([
'KvsARN' => $this->keyvaluestore
]);

Assert::assertEquals('200', $result['@metadata']['statusCode']);
} catch (EventBridgeException $e) {
throw new \Exception('failed to describe key-value store');
}
}
}

0 comments on commit 9a7a3a9

Please sign in to comment.