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); } } ?>