diff --git a/src/Escher.php b/src/Escher.php index 2b24a04..7ab4f31 100644 --- a/src/Escher.php +++ b/src/Escher.php @@ -55,7 +55,6 @@ public function authenticate($keyDB, array $serverVars = null, $requestBody = nu $authElements->validateMandatorySignedHeaders($this->dateHeaderKey); $authElements->validateHashAlgo(); $authElements->validateDates($helper, $this->clockSkew); - $authElements->validateHost($helper); $authElements->validateCredentials($this->credentialScope); $authElements->validateSignature($helper, $this, $keyDB, $vendorKey); return $authElements->getAccessKeyId(); @@ -350,7 +349,7 @@ public function getAuthElements($vendorKey, $algoPrefix) } else if($this->getRequestMethod() === 'GET' && isset($queryParams[$this->paramKey($vendorKey, 'Signature')])) { return EscherAuthElements::parseFromQuery($headerList, $queryParams, $vendorKey, $algoPrefix); } - throw new EscherException('Request has not been signed.'); + throw new EscherException('Escher authentication is missing'); } public function getTimeStamp() @@ -483,7 +482,7 @@ public static function parseFromHeaders(array $headerList, $authHeaderKey, $date $host = self::checkHost($headerList); if (!isset($headerList[strtolower($dateHeaderKey)])) { - throw new EscherException('The '.$dateHeaderKey.' header is missing'); + throw new EscherException('The '.strtolower($dateHeaderKey).' header is missing'); } if (strtolower($dateHeaderKey) !== 'date') { @@ -492,12 +491,9 @@ public static function parseFromHeaders(array $headerList, $authHeaderKey, $date try { $dateTime = new DateTime($headerList[strtolower($dateHeaderKey)], new DateTimeZone('GMT')); } catch (Exception $ex) { - throw new EscherException('Invalid date format'); + throw new EscherException('Invalid date header, expected format is: Wed, 04 Nov 2015 09:20:22 GMT'); } } - if (!$dateTime) { - throw new EscherException('Invalid date format'); - } return new EscherAuthElements($elementParts, $accessKeyId, $shortDate, $credentialScope, $dateTime, $host, true); } @@ -509,40 +505,40 @@ public static function parseFromHeaders(array $headerList, $authHeaderKey, $date */ public static function parseAuthHeader($headerContent, $algoPrefix) { - $parts = explode(' ', $headerContent); - if (count($parts) !== 4) { + $pattern = '/^' . $algoPrefix . '-HMAC-([A-Z0-9\,]+)(.*)' . + 'Credential=([A-Za-z0-9\/\-_]+),(.*)' . + 'SignedHeaders=([A-Za-z\-;]+),(.*)' . + 'Signature=([0-9a-f]+)$/'; + + if (!preg_match($pattern, $headerContent, $matches)) { throw new EscherException('Could not parse auth header'); } return array( - 'Algorithm' => self::match(self::algoPattern($algoPrefix), $parts[0]), - 'Credentials' => self::match('Credential=([A-Za-z0-9\/\-_]+),', $parts[1]), - 'SignedHeaders' => self::match('SignedHeaders=([A-Za-z\-;]+),', $parts[2]), - 'Signature' => self::match('Signature=([0-9a-f]+)', $parts[3]), + 'Algorithm' => $matches[1], + 'Credentials' => $matches[3], + 'SignedHeaders' => $matches[5], + 'Signature' => $matches[7], ); } - private static function match($pattern, $part) - { - if (!preg_match("/^$pattern$/", $part, $matches)) { - throw new EscherException('Could not parse auth header'); - } - return $matches[1]; - } - public static function parseFromQuery($headerList, $queryParams, $vendorKey, $algoPrefix) { $elementParts = array(); $paramKey = self::checkParam($queryParams, $vendorKey, 'Algorithm'); - $elementParts['Algorithm'] = self::match(self::algoPattern($algoPrefix), $queryParams[$paramKey]); + + $pattern = '/^' . $algoPrefix . '-HMAC-([A-Z0-9\,]+)$/'; + if (!preg_match($pattern, $queryParams[$paramKey], $matches)) + { + throw new EscherException('invalid ' . $paramKey . ' query key format'); + } + $elementParts['Algorithm'] = $matches[1]; + foreach (self::basicQueryParamKeys() as $paramId) { $paramKey = self::checkParam($queryParams, $vendorKey, $paramId); $elementParts[$paramId] = $queryParams[$paramKey]; } list($accessKeyId, $shortDate, $credentialScope) = explode('/', $elementParts['Credentials'], 3); $dateTime = EscherUtils::parseLongDate($elementParts['Date']); - if (!$dateTime) { - throw new EscherException('Invalid date format'); - } return new EscherAuthElements($elementParts, $accessKeyId, $shortDate, $credentialScope, $dateTime, self::checkHost($headerList), false); } @@ -557,15 +553,6 @@ private static function basicQueryParamKeys() ); } - /** - * @param $algoPrefix - * @return string - */ - private static function algoPattern($algoPrefix) - { - return $algoPrefix . '-HMAC-([A-Z0-9\,]+)'; - } - /** * @param $queryParams * @param $vendorKey @@ -577,7 +564,7 @@ private static function checkParam($queryParams, $vendorKey, $paramId) { $paramKey = 'X-' . $vendorKey . '-' . $paramId; if (!isset($queryParams[$paramKey])) { - throw new EscherException('Missing query parameter: ' . $paramKey); + throw new EscherException('Query key is missing: ' . $paramKey); } return $paramKey; } @@ -594,7 +581,7 @@ public function validateDates(EscherRequestHelper $helper, $clockSkew) { $shortDate = $this->dateTime->format('Ymd'); if ($shortDate !== $this->getShortDate()) { - throw new EscherException('The authorization header\'s shortDate does not match with the request date'); + throw new EscherException('Invalid date in authorization header, it should equal with header'); } if (!$this->isInAcceptableInterval($helper->getTimeStamp(), EscherUtils::getTimeStampOfDateTime($this->dateTime), $clockSkew)) { @@ -602,17 +589,10 @@ public function validateDates(EscherRequestHelper $helper, $clockSkew) } } - public function validateHost(EscherRequestHelper $helper) - { - if($helper->getServerHost() !== $this->getHost()) { - throw new EscherException('The Host header does not match: ' . $this->getHost() . ' != ' . $helper->getServerHost()); - } - } - public function validateCredentials($credentialScope) { if (!$this->checkCredentials($credentialScope)) { - throw new EscherException('The credential scope is invalid'); + throw new EscherException('Invalid Credential Scope'); } } @@ -654,7 +634,7 @@ public function validateHashAlgo() { if(!in_array(strtoupper($this->getAlgorithm()), array('SHA256','SHA512'))) { - throw new EscherException('Only SHA256 and SHA512 hash algorithms are allowed.'); + throw new EscherException('Invalid hash algorithm, only SHA256 and SHA512 are allowed'); } } @@ -669,7 +649,7 @@ public function validateMandatorySignedHeaders($dateHeaderKey) throw new EscherException('The host header is not signed'); } if ($this->isFromHeaders && !in_array(strtolower($dateHeaderKey), $signedHeaders)) { - throw new EscherException('The date header is not signed'); + throw new EscherException('The ' . strtolower($dateHeaderKey) . ' header is not signed'); } } @@ -912,7 +892,7 @@ class EscherUtils public static function parseLongDate($dateString) { if (!preg_match('/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z$/', $dateString)) { - throw new EscherException('Invalid date format'); + throw new EscherException('Invalid date header, expected format is: 20151104T092022Z'); } if (!self::advancedDateTimeFunctionsAvailable()) { return new DateTime($dateString, new DateTimeZone('GMT')); diff --git a/test/unit/AuthenticateRequestTest.php b/test/unit/AuthenticateRequestTest.php index 8c22a15..cbcf9be 100644 --- a/test/unit/AuthenticateRequestTest.php +++ b/test/unit/AuthenticateRequestTest.php @@ -99,15 +99,14 @@ public function itShouldFailToValidateInvalidRequests($tamperedKey, $tamperedVal public function requestTamperingProvider() { return array( - 'wrong date' => array('HTTP_X_EMS_DATE', 'INVALIDDATE', 'Invalid date format'), + 'wrong date' => array('HTTP_X_EMS_DATE', 'INVALIDDATE', 'Invalid date header, expected format is: 20151104T092022Z'), 'wrong request time' => array('REQUEST_TIME', '20110909T113600Z', 'The request date is not within the accepted time range'), - 'wrong host' => array('HTTP_HOST', 'example.com', 'The Host header does not match: example.com != iam.amazonaws.com'), 'wrong auth header' => array('HTTP_X_EMS_AUTH', 'Malformed auth header', 'Could not parse auth header'), 'tampered signature' => array('HTTP_X_EMS_AUTH', 'EMS-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff', 'The signatures do not match'), - 'wrong hash algo' => array('HTTP_X_EMS_AUTH', 'EMS-HMAC-SHA123 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd', 'Only SHA256 and SHA512 hash algorithms are allowed'), + 'wrong hash algo' => array('HTTP_X_EMS_AUTH', 'EMS-HMAC-SHA123 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd', 'Invalid hash algorithm, only SHA256 and SHA512 are allowed'), 'host not signed' => array('HTTP_X_EMS_AUTH', 'EMS-HMAC-SHA123 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd', 'The host header is not signed'), - 'date not signed' => array('HTTP_X_EMS_AUTH', 'EMS-HMAC-SHA123 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd', 'The date header is not signed'), - 'invalid credential' => array('HTTP_X_EMS_AUTH', 'EMS-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-2/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd', 'The credential scope is invalid'), + 'date not signed' => array('HTTP_X_EMS_AUTH', 'EMS-HMAC-SHA123 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd', 'The x-ems-date header is not signed'), + 'invalid credential' => array('HTTP_X_EMS_AUTH', 'EMS-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-2/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd', 'Invalid Credential Scope'), 'invalid Escher key' => array('HTTP_X_EMS_AUTH', 'EMS-HMAC-SHA256 Credential=FOOBAR/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd', 'Invalid Escher key'), ); } @@ -172,47 +171,6 @@ public function itShouldFailToValidateInvalidQueryStrings() $this->createEscher('us-east-1/host/aws4_request')->authenticate($keyDB, $serverVars, ''); } - /** - * @test - * @dataProvider invalidPortProvider - */ - public function itShouldFailToAuthenticateWrongPort($httpHost, $serverName, $serverPort, $https) - { - $serverVars = array( - 'HTTP_X_EMS_DATE' => '20110909T233600Z', - 'HTTP_X_EMS_AUTH' => 'EMS-HMAC-SHA256 Credential=AKIDEXAMPLE/20110909/us-east-1/iam/aws4_request, SignedHeaders=content-type;host;x-ems-date, Signature=f36c21c6e16a71a6e8dc56673ad6354aeef49c577a22fd58a190b5fcf8891dbd', - 'REQUEST_TIME' => $this->strtotime('20110909T233600Z'), - 'REQUEST_METHOD' => 'POST', - 'HTTP_HOST' => $httpHost, - 'CONTENT_TYPE' => 'application/x-www-form-urlencoded; charset=utf-8', - 'REQUEST_URI' => '/', - 'HTTPS' => $https, - 'SERVER_PORT' => $serverPort, - 'SERVER_NAME' => $serverName, - ); - $keyDB = array('AKIDEXAMPLE' => 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'); - try - { - $this->createEscher('us-east-1/iam/aws4_request') - ->authenticate($keyDB, $serverVars, 'Action=ListUsers&Version=2010-05-08'); - $this->fail('Should fail to validate!'); - } - catch (EscherException $e) - { - $this->assertStringStartsWith('The Host header does not match', $e->getMessage()); - } - } - - public function invalidPortProvider() - { - return array ( - 'server on default http port, request to something else' => array('iam.amazonaws.com:123', 'iam.amazonaws.com', '80', ''), - 'server on default https port, request to something else' => array('iam.amazonaws.com:123', 'iam.amazonaws.com', '443', 'on'), - 'server on default http port, request to default https port' => array('iam.amazonaws.com:443', 'iam.amazonaws.com', '80', ''), - 'server on default https port, request to default http port' => array('iam.amazonaws.com:80', 'iam.amazonaws.com', '443', 'on') - ); - } - private function strtotime($dateString) { return EscherUtils::parseLongDate($dateString)->format('U');