
Note
The External Captive Portal and ExtremeCloud IQ Controller must be time synchronized. AWS4 signature includes a time stamp; therefore, both systems must be configured with the correct date and time when using AWS4 signature.<?php
class SimpleAws {
const AWS4_ERROR_NONE=0;
const AWS4_ERROR_NULL_INPUT=1;
const AWS4_ERROR_INPUT_BUFFER_TOO_SMALL=2;
const AWS4_ERROR_INVALID_PROTOCOL=3;
const AWS4_ERROR_INPUT_URL_TOO_BIG=4;
const AWS4_ERROR_INPUT_ID_TOO_BIG=5;
const AWS4_ERROR_INPUT_KEY_TOO_BIG=6;
const AWS4_ERROR_INVALID_REGION=7;
const AWS4_ERROR_INVALID_SIGNATURE=8;
const AWS4_ERROR_MISSING_QUERY=9;
const AWS4_ERROR_MISSING_QUERY_DATE=10;
const AWS4_ERROR_MISSING_QUERY_SIGNED_HEADERS=11;
const AWS4_ERROR_MISSING_QUERY_EXPIRES=12;
const AWS4_ERROR_MISSING_QUERY_SIGNATURE=13;
const AWS4_ERROR_MISSING_QUERY_CREDENTIAL=14;
const AWS4_ERROR_MISSING_QUERY_ALGORITHM=15;
const AWS4_ERROR_MISSING_QUERY_PARAMS=16;
const AWS4_ERROR_MISSING_CRED_PARAMS=17;
const AWS4_ERROR_STALE_REQUEST=2001;
const AWS4_ERROR_UNKNOWN_IDENTITY=2002;
const AWS4_EXTREME_REQUEST="aws4_request";
const AWS4_MAX_URL_SIZE= 512;
const AWS4_HTTP_REQ = "http://";
const AWS4_HTTPS_REQ= "https://";
const AWS4_MANDATORY_CRED_PARAMS = 4;
/**
* Method to verify the AWS signature based on given full URL address.
*
* @param string $pUrl
* @param array $awsKeyPairs identity, shared secret key pairs
* @return AWS error code
*/
public static function verifyAwsUrlSignature($pUrl,
$awsKeyPairs) {
// Perform basic validation
if($pUrl==NULL) {
return self::AWS4_ERROR_NULL_INPUT;
}
if (2*self::AWS4_MAX_URL_SIZE < strlen($pUrl)) {
return self::AWS4_ERROR_INPUT_URL_TOO_BIG;
}
if(stripos($pUrl, self::AWS4_HTTP_REQ)!=0 || stripos($pUrl, self::AWS4_HTTPS_REQ)!=0) {
return self::AWS4_ERROR_INVALID_PROTOCOL;
}
$urlParams = parse_url($pUrl);
if (!isset($urlParams['query'])) {
return self::AWS4_ERROR_MISSING_QUERY;
}
$queryParams = explode("&", $urlParams['query']);
foreach($queryParams AS $el) {
$arr = explode("=", $el);
$q[$arr[0]] = $arr[1];
}
$valResult = self::validateQueryParms($q);
if (self::AWS4_ERROR_NONE != $valResult) {
return $valResult;
}
// Done with the basic validations.
$date = $q['X-Amz-Date'];
$sign = $q['X-Amz-Signature'];
$credentVal = rawurldecode($q['X-Amz-Credential']);
ksort($q);
// Remove the signature from the list of parameters over
// which the signature will be recomputed.
unset($q['X-Amz-Signature']);
$credentAttrs = explode("/", $credentVal);
$pKey = $credentAttrs[0];
if (self::AWS4_MAX_URL_SIZE < strlen($pKey)) {
return self::AWS4_ERROR_INPUT_KEY_TOO_BIG;
}
if(self::AWS4_MANDATORY_CRED_PARAMS > count($credentAttrs)) {
return self::AWS4_ERROR_MISSING_CRED_PARAMS;
}
if (!isset($awsKeyPairs[$pKey])) {
return self::AWS4_ERROR_UNKNOWN_IDENTITY;
}
$scope = $credentAttrs[1]."/".$credentAttrs[2]."/"
.$credentAttrs[3]."/".$credentAttrs[4];
$port = $urlParams['port'];
$host = strtolower($urlParams['host']);
if($port && (($urlParams['scheme']=='https' && $port !=
443)||($urlParams['scheme']=='http' && $port != 80))) {
$host .= ':'.$port;
}
$canonical_request = self::getCanonicalFFECPContent($q,
$host, $urlParams['path']);
$stringToSign = "AWS4-HMAC-SHA256\n{$date}\n{$scope}\n" .
hash('sha256', $canonical_request);
$signingKey = self::getSigningKey($credentAttrs[1], $credentAttrs[2],
$credentAttrs[3], $awsKeyPairs[$pKey]);
$mySign = hash_hmac('sha256', $stringToSign, $signingKey);
if (strcmp($mySign,$sign)){
return self::AWS4_ERROR_INVALID_SIGNATURE;
}
return self::AWS4_ERROR_NONE;
}
/**
* Method to verify that the query parameters contain the elements
* required in the response to the controller and the ones required to
* sign the request.
* @param array $qParams: an associative array in which the key of an
* entry is the name of a query parameter and the corresponding value
* is the value of that parameter.
* @return an AWS_ERROR code.
*/
private static function validateQueryParms($qParams) {
if (is_null($qParams)) {
return self::AWS4_ERROR_MISSING_QUERY;
}
if ((!isset($qParams['wlan'])) or (!isset($qParams['token']))
or (!isset($qParams['dest']))) {
return self::AWS4_ERROR_MISSING_QUERY_PARAMS;
}
if (!isset($qParams['X-Amz-Signature'])) {
return self::AWS4_ERROR_MISSING_QUERY_SIGNATURE;
}
if(!isset($qParams['X-Amz-Algorithm'])) {
return self::AWS4_ERROR_MISSING_QUERY_ALGORITHM;
}
if (!isset($qParams['X-Amz-Credential'])) {
return self::AWS4_ERROR_MISSING_QUERY_CREDENTIAL;
}
if (!isset($qParams['X-Amz-Date'])) {
return self::AWS4_ERROR_MISSING_QUERY_DATE;
}
if (!isset($qParams['X-Amz-Expires'])) {
return self::AWS4_ERROR_MISSING_QUERY_EXPIRES;
}
if (!isset($qParams['X-Amz-SignedHeaders'])) {
return self::AWS4_ERROR_MISSING_QUERY_SIGNED_HEADERS;
}
// The date & expires parameters exist in the request.
// Verify that the request is not stale or replayed.
$redirectedAt = DateTime::createFromFormat('Ymd?Gis?',
$qParams['X-Amz-Date'], new DateTimeZone("UTC"));
$expires = $qParams['X-Amz-Expires'];
$now = date_create();
$delta = $now->getTimestamp() - $redirectedAt->getTimestamp();
// The following gives some latitude for clocks that are not synched
if (($delta < -10) or ($delta > $expires)) {
print("<br>");
print(date("Y-m-d H:i:sZ", $now->getTimestamp()));
print("<br>");
print("Redirected at: ");
print(date("Y-m-d H:i:sZ", $redirectedAt->getTimestamp()));
print("<br>");
print($now->getTimeZone()->getName());
print("<br>");
print($redirectedAt->getTimeZone()->getName());
print("<br>");
print($expires);
print("<br>");
print($delta);
return self::AWS4_ERROR_STALE_REQUEST;
}
return self::AWS4_ERROR_NONE;
}
/**
* Method to generate the AWS signed URL address
* @param string $pUrl: the URL that need to be appended with AWS parameters
* @param string $identity: the AWS identity
* @param string $sharedSecret: the secret shared with the controller
* @param string $region: the region component of the scope
* @param string $service: the service component of the scope
* @param int $expires: number of seconds till presigned URL is untrusted.
* @return URL string with AWS parameters
*/
public static function createPresignedUrl(
$pUrl, $identity, $sharedSecret, $region,
$service, $expires) {
$urlParams = parse_url($pUrl);
$httpDate = gmdate('Ymd\THis\Z', time());
$scopeDate = substr($httpDate, 0, 8);
$scope = "{$scopeDate}/".$region."/".$service."/".self::AWS4_EXTREME_REQUEST;
$credential = $identity . '/' . $scope;
$duration = $expires;
//set the aws parameters
$awsParams = array(
'X-Amz-Date'=>$httpDate,
'X-Amz-Algorithm'=> 'AWS4-HMAC-SHA256',
'X-Amz-Credential'=> $credential,
'X-Amz-SignedHeaders' =>'host',
'X-Amz-Expires'=> $duration
);
parse_str($urlParams['query'],$q);
$q = array_merge($q, $awsParams);
ksort($q);
$port = $urlParams['port'];
$host = strtolower($urlParams['host']);
if($port && (($urlParams['scheme']=='https' && $port !=
443)||($urlParams['scheme']=='http' && $port != 80))) {
$host .= ':'.$port;
}
$canonical_request = self::getCanonicalFFECPContent($q,
$host, $urlParams['path'], true);
$stringToSign = "AWS4-HMAC-SHA256\n{$httpDate}\n{$scope}\n" .
hash('sha256', $canonical_request);
$signingKey = self::getSigningKey(
$scopeDate,
$region,
$service,
$sharedSecret
);
$q['X-Amz-Signature'] = hash_hmac('sha256', $stringToSign,
$signingKey);
$p = substr($pUrl, 0, strpos($pUrl,'?'));
$queryParams = array();
foreach($q AS $k=>$v) {
$queryParams[] = "$k=".rawurlencode($v);
}
$p .= '?'.implode('&', $queryParams);
return $p;
}
/**
* Method to generate the AWS signing key
* @param string $shortDate: short date format (20140611)
* @param string $region: Region name (us-east-1)
* @param string $service: Service name (s3)
* @param string $secretKey Secret Access Key
* @return string
*/
protected static function getSigningKey($shortDate, $region, $service, $secretKey) {
$dateKey = hash_hmac('sha256', $shortDate, 'AWS4' . $secretKey, true);
$regionKey = hash_hmac('sha256', $region, $dateKey, true);
$serviceKey = hash_hmac('sha256', $service, $regionKey, true);
return hash_hmac('sha256', self::AWS4_EXTREME_REQUEST, $serviceKey, true);
}
/**
* Create the canonical context for the AWS service
* @param array $queryHash the query parameter hash
* @param string $host host name or ip address for the target service
* @param string $path the service address for the target service
* @param boolean $encode determine if the query parameter need to be encoded or not.
* @return string the canonical content for the request
*/
protected static function getCanonicalFFECPContent($queryHash, $host, $path, $encode=false) {
$queryParams = array();
foreach($queryHash AS $k=>$v) {
if($encode) {$v = rawurlencode($v);}
$queryParams[] = "$k=$v";
}
$canonical_request = "GET\n"
.$path."\n"
.implode('&',$queryParams)."\n"
.'host:'.$host
."\n\nhost\nUNSIGNED-PAYLOAD";
return $canonical_request;
}
/**
* Create user readable error message
* @param integer $eid error code after verifying the AWS URL
* @return string the error message
*/
public static function getAwsError($eid) {
$forAws = " for Amazon Web Service request.";
SWITCH ($eid) {
case self::AWS4_ERROR_NULL_INPUT:
$res = "Empty input".$forAws;
break;
case self::AWS4_ERROR_INPUT_BUFFER_TOO_SMALL:
$res = "Input buffer is too small".$forAws;
break;
case self::AWS4_ERROR_INVALID_PROTOCOL:
$res = "Invalid protocol".$forAws;
break;
case self::AWS4_ERROR_INPUT_URL_TOO_BIG:
$res = "Input url is too big".$forAws;
break;
case self::AWS4_ERROR_INPUT_ID_TOO_BIG:
$res = "Input ID is too big".$forAws;
break;
case self::AWS4_ERROR_INVALID_REGION:
$res = "Invalid region".$forAws;
break;
case self::AWS4_ERROR_INVALID_SIGNATURE:
$res = "Invalid signature".$forAws;
break;
case self::AWS4_ERROR_MISSING_QUERY:
$res = "Missing all query parameters".$forAws;
break;
case self::AWS4_ERROR_MISSING_QUERY_DATE:
$res = "Missing query date".$forAws;
break;
case self::AWS4_ERROR_MISSING_QUERY_SIGNED_HEADERS:
$res = "Missing query signed headers".$forAws;
break;
case self::AWS4_ERROR_MISSING_QUERY_EXPIRES:
$res = "Missing query expires".$forAws;
break;
case self::AWS4_ERROR_MISSING_QUERY_SIGNATURE:
$res = "Missing query signature".$forAws;
break;
case self::AWS4_ERROR_MISSING_QUERY_CREDENTIAL:
$res = "Missing query credential".$forAws;
break;
case self::AWS4_ERROR_MISSING_QUERY_ALGORITHM:
$res = "Missing query algorithm".$forAws;
break;
case self::AWS4_ERROR_MISSING_QUERY_PARAMS:
$res = "Missing query parameter".$forAws;
break;
case self::AWS4_ERROR_MISSING_CRED_PARAMS:
$res = "Missing credential parameters".$forAws;
break;
case self::AWS4_ERROR_STALE_REQUEST:
$res = "Invalid request date".$forAws;
break;
case self::AWS4_ERROR_UNKNOWN_IDENTITY:
$res = "Unrecognized identity or identity without a shared secret.";
break;
default:
$res = "Successfully validated".$forAws;
break;
}
return $res;
}
/**
* Return the AWS validation error message
* @param string $pUrl
* @return string the error message
*/
public function getUrlValidationResult($pUrl) {
$eid = self::verifyAwsUrlSignature($pUrl);
return self::getAwsError($eid);
}
}
?>