diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bc511a4 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,32 @@ +# phpFreja + +Changelog for phpFreja. + +The format is based on [Keep a Changelog][keep-a-changelog] + + +## [Unreleased] +- Nothing right now + +## [1.0] (2012-05-02) + +### BREAKING CHANGES +- Renamed the main class from 'frejaeID' to new name 'phpFreja' +- Renamed the php file from 'frejaeID.php' to 'freja.php' +- Output from the checkSignatureRequest have changed + +### Changed +- Included a fork from [php-jws](https://github.com/Gamegos/php-jws) in repo + +### Added +- Added JWS validation as suggested by [#1](https://github.com/DSorlov/phpFrejaeid/issues/1) +- PEM-files added for JWS validation + +## [0.1] (2019-10-14) + +### Changed +- Initial releases + +[keep-a-changelog]: http://keepachangelog.com/en/1.0.0/ +[1.0]: https://github.com/DSorlov/phpFreja/releases/tag/v1.0 +[0.1]: https://github.com/DSorlov/phpFreja \ No newline at end of file diff --git a/README.md b/README.md index aa68834..d2f049b 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,17 @@ Simple PHP wrapper to talk to [Freja eID](https://frejaeid.com/en/developers-section/) [REST API](https://frejaeid.com/rest-api/Freja%20eID%20Relying%20Party%20Developers'%20Documentation.html) for use both in test and production enviroment. -- Supports validation of the JWS but requires external library for that part. +- Supports validation of the JWS but requires external library for that part (thanks to [php-jws](https://github.com/Gamegos/php-jws). - Supports both directed and inferred authentication, for use with qr-code and app. - Supports authentication and signature api but not the assertion service. +- Well behaved functions that do not throw (atleast not by design) but always return objects for simpler handling. - Not developed, supported or endorsed by Verisec. ## Example ### Init connection to API (test) ```PHP -$testAuth = new frejaeID(getcwd().'/testCertificate.pfx','SuperSecretPassword',false); +$testAuth = new phpFreja(getcwd().'/testCertificate.pfx','SuperSecretPassword',false); ``` ### Create URL for QR-Code ```PHP diff --git a/example.php b/example.php new file mode 100644 index 0000000..67e8890 --- /dev/null +++ b/example.php @@ -0,0 +1,11 @@ +initAuthentication(); + +echo var_dump($result); + +?> \ No newline at end of file diff --git a/freja.php b/freja.php new file mode 100644 index 0000000..41038bc --- /dev/null +++ b/freja.php @@ -0,0 +1,437 @@ +production = $production; + if ($production) { + $this->serviceUrl = 'https://services.prod.frejaeid.com'; + $this->resourceUrl = 'https://resources.prod.frejaeid.com'; + + if (!is_readable(__DIR__."/freja_prod.pem")) + throw new Exception('JWS Certificate file could not be found ('.__DIR__.'/freja_test.pem)'); + else + $this->jwsCert = file_get_contents(__DIR__."/freja_prod.pem"); + } else { + $this->serviceUrl = 'https://services.test.frejaeid.com'; + $this->resourceUrl = 'https://resources.test.frejaeid.com'; + + if (!is_readable(getcwd()."/freja_test.pem")) + throw new Exception('JWS Certificate file could not be found ('.__DIR__.'/freja_test.pem)'); + else + $this->jwsCert = file_get_contents(__DIR__."/freja_test.pem"); + } + + if (!is_readable($certificate)) + throw new Exception('Certificate file could not be found'); + + $this->certificate = $certificate; + $this->password = $password; + } + + public function createAuthQRCode($existingCode=NULL) { + + if ($this->IsNullOrEmptyString($existingCode)) { + $response = $this->initAuthentication(); + if (!$response->success) + return $response; + $existingCode = $response->authRef; + } + + $resultObject = $this->createSuccessObject(); + $resultObject->url = $this->resourceUrl . "/qrcode/generate?qrcodedata=frejaeid%3A%2F%2FbindUserToTransaction%3Fdimension%3D4x%3FtransactionReference%3D" . $existingCode; + $resultObject->authRef = $existingCode; + + return $resultObject; + } + + public function cancelAuthentication($authRef) { + $query = new \stdClass(); $query->authRef = $authRef; + + $apiPost = array( + "cancelAuthRequest" => base64_encode(json_encode($query)) + ); + + $apiPostQuery = http_build_query($apiPost); + $result = $this->apiRequest('/authentication/1.0/cancel',$apiPostQuery); + + if (!$result->success) + return $this->createErrorObject($result->code,$result->data); + + return $this->createSuccessObject(); + } + + public function checkAuthentication($authRef) { + $query = new \stdClass(); $query->authRef = $authRef; + + $apiPost = array( + "getOneAuthResultRequest" => base64_encode(json_encode($query)) + ); + + $apiPostQuery = http_build_query($apiPost); + $result = $this->apiRequest('/authentication/1.0/getOneResult',$apiPostQuery); + + if (!$result->success) + return $this->createErrorObject($result->code,$result->data); + + return $this->createSuccessObject($result->data); + } + + public function initAuthentication($userType="N/A",$userInfo="N/A",$authLevel="BASIC") { + + + $emailAttribute = new \stdClass(); $emailAttribute->attribute = "EMAIL_ADDRESS"; + $userAttribute = new \stdClass(); $userAttribute->attribute = "RELYING_PARTY_USER_ID"; + $basicAttribute = new \stdClass(); $basicAttribute->attribute = "BASIC_USER_INFO"; + $dobAttribute = new \stdClass(); $dobAttribute->attribute = "DATE_OF_BIRTH"; + $ssnAttribute = new \stdClass(); $ssnAttribute->attribute = "SSN"; + + $query = new \stdClass(); $query->attributesToReturn = array ( $emailAttribute ); + array_push($query->attributesToReturn, $userAttribute ); + + switch ($userType) { + case "N/A": + $query->userInfoType = "INFERRED"; + $query->userInfo = "N/A"; + break; + case "PHONE": + $query->userInfoType = "PHONE"; + $query->userInfo = $userInfo; + break; + case "EMAIL": + $query->userInfoType = "EMAIL"; + $query->userInfo = $userInfo; + break; + case "SSN": + $query->userInfoType = "SSN"; + $ssnUserinfo = new \stdClass(); + $ssnUserinfo->country = "SE"; + $ssnUserinfo->ssn = $userInfo; + $query->userInfo = base64_encode(json_encode($ssnUserinfo)); + break; + default: + throw new Exception('User type not N/A, EMAIL or PHONE'); + break; + } + switch ($authLevel) { + case "BASIC": + $query->minRegistrationLevel = "BASIC"; + break; + case "EXTENDED": + $query->minRegistrationLevel = "EXTENDED"; + array_push($query->attributesToReturn, $basicAttribute); + array_push($query->attributesToReturn, $dobAttribute); + array_push($query->attributesToReturn, $ssnAttribute); + break; + case "PLUS": + $query->minRegistrationLevel = "PLUS"; + array_push($query->attributesToReturn, $basicAttribute); + array_push($query->attributesToReturn, $dobAttribute); + array_push($query->attributesToReturn, $ssnAttribute); + break; + default: + throw new Exception('User type not BASIC, EXTENDED or PLUS'); + break; + } + + $apiPost = array( + "initAuthRequest" => base64_encode(json_encode($query)) + ); + $apiPostQuery = http_build_query($apiPost); + $result = $this->apiRequest('/authentication/1.0/initAuthentication',$apiPostQuery); + + if (!$result->success) + return $this->createErrorObject($result->code,$result->data); + + if (!isset($result->data->authRef)) + return $this->createErrorObject(400,"Missing authRef from API response."); + + return $this->createSuccessObject($result->data);; + } + + public function initSignatureRequest($userType,$userInfo,$agreementText,$agreementTitle,$authLevel="BASIC",$timeoutMinutes=2,$confidential=false,$pushTitle=NULL,$pushMessage=NULL,$binaryData=NULL) { + + $emailAttribute = new \stdClass(); $emailAttribute->attribute = "EMAIL_ADDRESS"; + $userAttribute = new \stdClass(); $userAttribute->attribute = "RELYING_PARTY_USER_ID"; + $basicAttribute = new \stdClass(); $basicAttribute->attribute = "BASIC_USER_INFO"; + $dobAttribute = new \stdClass(); $dobAttribute->attribute = "DATE_OF_BIRTH"; + $ssnAttribute = new \stdClass(); $ssnAttribute->attribute = "SSN"; + + $query = new \stdClass(); $query->attributesToReturn = array ( $emailAttribute ); + array_push($query->attributesToReturn, $userAttribute ); + + if ($this->IsNullOrEmptyString($agreementText) or $this->IsNullOrEmptyString($agreementTitle)) + throw new Exception('Agreement text and title must be specified'); + + switch ($userType) { + case "PHONE": + $query->userInfoType = "PHONE"; + $query->userInfo = $userInfo; + break; + case "EMAIL": + $query->userInfoType = "EMAIL"; + $query->userInfo = $userInfo; + break; + case "SSN": + $query->userInfoType = "SSN"; + $ssnUserinfo = new \stdClass(); + $ssnUserinfo->country = "SE"; + $ssnUserinfo->ssn = $userInfo; + $query->userInfo = base64_encode(json_encode($ssnUserinfo)); + break; + default: + throw new Exception('User type not EMAIL or PHONE'); + break; + } + switch ($authLevel) { + case "BASIC": + $query->minRegistrationLevel = "BASIC"; + break; + case "EXTENDED": + $query->minRegistrationLevel = "EXTENDED"; + array_push($query->attributesToReturn, $basicAttribute); + array_push($query->attributesToReturn, $dobAttribute); + array_push($query->attributesToReturn, $ssnAttribute); + break; + case "PLUS": + $query->minRegistrationLevel = "PLUS"; + array_push($query->attributesToReturn, $basicAttribute); + array_push($query->attributesToReturn, $dobAttribute); + array_push($query->attributesToReturn, $ssnAttribute); + break; + default: + throw new Exception('User type not BASIC, EXTENDED or PLUS'); + break; + } + + $query->title = $agreementTitle; + $query->confidential = $confidential; + $query->expiry = (time() + ($timeoutMinutes * 60))*1000; + + if (!$this->IsNullOrEmptyString($pushTitle)) { + $pushNotification = new \stdClass(); $pushNotification->title = $pushTitle; + $pushNotification->text = $pushMessage; + $query->pushNotification = $pushNotification; + } + + $dataToSign = new \stdClass(); $dataToSign->text = base64_encode($agreementText); + if ($this->IsNullOrEmptyString($binaryData)) { + $query->dataToSign = $dataToSign; + $query->dataToSignType = "SIMPLE_UTF8_TEXT"; + $query->signatureType = "SIMPLE"; + } else { + $dataToSign->binaryData = base64_encode($binaryData); + $query->dataToSign = $dataToSign; + $query->dataToSignType = "EXTENDED_UTF8_TEXT"; + $query->signatureType = "EXTENDED"; + } + + $apiPost = array( + "initSignRequest" => base64_encode(json_encode($query)) + ); + $apiPostQuery = http_build_query($apiPost); + $result = $this->apiRequest('/sign/1.0/initSignature',$apiPostQuery); + + if (!$result->success) + return $this->createErrorObject($result->code,$result->data); + + if (!isset($result->data->signRef)) + return $this->createErrorObject(400,"Missing signRef from API response."); + + return $this->createSuccessObject($result->data);; + } + + public function checkSignatureRequest($signRef) { + $query = new \stdClass(); $query->signRef = $signRef; + + $apiPost = array( + "getOneSignResultRequest" => base64_encode(json_encode($query)) + ); + + $apiPostQuery = http_build_query($apiPost); + $result = $this->apiRequest('/sign/1.0/getOneResult',$apiPostQuery); + + if (!$result->success) + return $this->createErrorObject($result->code,$result->data); + + if ($result->data->status!='APPROVED') + return $this->createSuccessObject($result->data); + + $jws = new \Gamegos\JWS\JWS(); + try + { + $result->data->details = json_decode(json_encode($jws->verify($result->data->details, $this->jwsCert))); // + $result->data->jwsMessage = "The signed information is valid"; + $result->data->jwsVerified = true; + } + catch (Exception $e) + { + try + { + $result->data->details = json_decode(json_encode($jws->decode($result->data->details))); + $result->data->jwsMessage = $e->getMessage(); + $result->data->jwsVerified = false; + } + catch (Exception $e) + { + return $this->createErrorObject("400","JWS decoding of the remote data failed"); + } + } + + $headers = $result->data->details->headers; + $payload = $result->data->details->payload; + + $userTicket = explode(".", $result->data->details->payload->signatureData->userSignature); + $userHeader = json_decode(base64_decode($userTicket[0])); + $userPayload = base64_decode($userTicket[1]); + $userSignature = $userTicket[2]; + + $result->data->details->payload->signatureData = new \stdClass(); + $result->data->details->payload->signatureData->kid = $userHeader->kid; + $result->data->details->payload->signatureData->alg = $userHeader->alg; + $result->data->details->payload->signatureData->content = $userPayload; + $result->data->details->payload->userInfo = json_decode($result->data->details->payload->userInfo); + + $result->data->details = new \stdClass(); + $result->data->details = $payload; + $result->data->details->x5t = $headers->x5t; + $result->data->details->alg = $headers->alg; + + return $this->createSuccessObject($result->data); + } + + public function cancelSignatureRequest($signRef) { + $query = new \stdClass(); $query->signRef = $signRef; + + $apiPost = array( + "cancelSignRequest" => base64_encode(json_encode($query)) + ); + + $apiPostQuery = http_build_query($apiPost); + $result = $this->apiRequest('/sign/1.0/cancel',$apiPostQuery); + if (!$result->success) + return $this->createErrorObject($result->code,$result->data); + + return $this->createSuccessObject(); + } + + private function apiRequest($apiUrl,$apiPostQuery){ + + $curl = curl_init(); + + $apiHeader = array(); + $apiHeader[] = 'Content-length: ' . strlen($apiPostQuery); + $apiHeader[] = 'Content-type: application/json'; + + // cURL Options + $options = array( + CURLOPT_URL => $this->serviceUrl . $apiUrl, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HEADER => false, + CURLINFO_HEADER_OUT => false, + CURLOPT_HTTPGET => false, + CURLOPT_POST => true, + CURLOPT_FOLLOWLOCATION => false, + CURLOPT_SSL_VERIFYHOST => false, // true in production will not work due to private certs at freja + CURLOPT_SSL_VERIFYPEER => false, // true in production will not work due to private certs at freja + CURLOPT_TIMEOUT => 30, + CURLOPT_MAXREDIRS => 2, + CURLOPT_HTTPHEADER => $apiHeader, + CURLOPT_USERAGENT => 'phpFreja/1.0', + CURLOPT_POSTFIELDS => $apiPostQuery, + CURLOPT_SSLCERTTYPE => 'P12', + CURLOPT_SSLCERT => $this->certificate, + CURLOPT_KEYPASSWD => $this->password + ); + + curl_setopt_array($curl, $options); + $http_output = curl_exec($curl); + $http_info = curl_getinfo($curl); + + if (curl_errno($curl)) { + $response->success = false; + $response->code = 500; + $response->data = curl_error($curl); + return $response; + } + + $response = new \stdClass(); + switch($http_info["http_code"]) { + case 200: + $remoteResponse = json_decode($http_output); + $response->success = true; + $response->code = 200; + $response->data = $remoteResponse; + break; + case 204: + $response->success = true; + $response->code = 200; + $response->data = ""; + break; + case 404: + case 410: + $response->success = false; + $response->code = 404; + $response->data = "Remote API reported the resource to be not found."; + break; + case 400: + $response->success = false; + $response->code = 400; + $response->data = "Remote API reported it cannot parse the request."; + break; + case 422: + $remoteResponse = json_decode($http_output); + $response->success = false; + $response->code = 400; + $response->data = "Remote API reported processing errors: ".$remoteResponse->message; + break; + case 500: + $response->success = false; + $response->code = 500; + $response->data = "Remote API reported a internal error."; + break; + default: + $response->success = false; + $response->code = 500; + $response->data = "A unknown status was reported: ".$remoteResponse->code; + $response->http_data = $http_output; + break; + } + + return $response; + } + + private function IsNullOrEmptyString($input){ + return (!isset($input) || trim($input)===''); + } + + private function createErrorObject($error_code,$error_message){ + $resultObject = new \stdClass(); + $resultObject->success = false; + $resultObject->code = $error_code; + $resultObject->message = $error_message; + return $resultObject; + } + + private function createSuccessObject($dataObject) { + if (!isset($dataObject)) { + $dataObject = new \stdClass(); + } + $dataObject->success = true; + return $dataObject; + } + + +} + +?> \ No newline at end of file diff --git a/freja_prod.pem b/freja_prod.pem new file mode 100644 index 0000000..cb2623b --- /dev/null +++ b/freja_prod.pem @@ -0,0 +1,28 @@ +-----BEGIN CERTIFICATE----- +MIIEvTCCAyWgAwIBAgIUZBsJTBnWAwJ2kWEgFlvLkadSONAwDQYJKoZIhvcNAQEL +BQAweTELMAkGA1UEBhMCU0UxFDASBgNVBGETCzU1OTExMC00ODA2MR0wGwYDVQQK +ExRWZXJpc2VjIEZyZWphIGVJRCBBQjETMBEGA1UECxMKUHJvZHVjdGlvbjEgMB4G +A1UEAxMXRnJlamEgZUlEIElzc3VpbmcgQ0EgdjEwHhcNMTcwODAyMTYyODIzWhcN +MjAwODAyMTYyODIzWjB6MSEwHwYDVQQDExhGcmVqYSBlSUQgSldTIFNpZ25pbmcg +djExFDASBgNVBGETCzU1OTExMC00ODA2MRMwEQYDVQQLEwpQcm9kdWN0aW9uMR0w +GwYDVQQKExRWZXJpc2VjIEZyZWphIGVJRCBBQjELMAkGA1UEBhMCU0UwggEiMA0G +CSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQC7y2YjMYwNq5j09dQQp293NdBskxEL +puPUEYE6DD0m3HvWZq3bJqaVuav9NSSXqevtuBm0BUpEFFDARief6bgozJY+WGkP +tURLjCoroHbkjA9jeX6Z1BpFdi/zOOlg4i19u0QxznBTTes41UT5uFwIrS2yq867 +o8kczUs6RCGdw30Ikysm3t/zWWjHu6y4BTkMWvxLMQZFpuAad/vEjG+y0/+3oxzl +3CH9HhwQtT4xPH3UpcFw4nKt6hTXQDNSQUEQTQbB86Z6sAEPxwnvL/SZS7cmARw6 +CeDX+fvJv6sXwBjsNGL7B3YMib/1rBPKE2jskqMrF1hYuqRd/xi1jjFRAgMBAAGj +gbswgbgwDgYDVR0PAQH/BAQDAgbAMAwGA1UdEwEB/wQCMAAwWAYIKwYBBQUHAQEE +TDBKMEgGCCsGAQUFBzAChjxodHRwczovL3d3dy5mcmVqYWVpZC5jb20vdGMvY2Vy +dHMvZnJlamFlaWRfaXNzdWluZ19jYV92MS5jZXIwHwYDVR0jBBgwFoAUED8kN9o6 +iEfwKOPN0xXwS6n2sVAwHQYDVR0OBBYEFJJt+ukaSQCnRFQpuEVrwG9c2EDNMA0G +CSqGSIb3DQEBCwUAA4IBgQAZiytgukQ4ka0VXnkDbtEiF8LluPz3pFIZrXJTllmF +EGYT3RSb4e52wKkEzPZG0z0JlpjeZHeU8LOyKDe3jqDMSc7N0t5mA25GgjNOGYme +JZYsFlZZrP6jmNTSfFJKpy3Uvoj7+CKt+0qei4CB/RPscRrGHDMyc8lLVH6Bh1oI +9NRMB1m23AWFEXEKtQJUMTBOcMVcUaHm2jjZvagLf/SJ+jU1VFc/OzJYud8IAL6J +EfWn4deY5qUEJTQrLskF2jyL/5VTHJsk8DC90wjt0lJFX7nKS/MqCr+0yEIHIwST +APa/7M16YKBkEdQidcu2uYp4GHZCcB72XDxXO8JtL62OPTS80HgA9kMb5MZdJeo2 +awGyCBVPbZXAgfypr6pGQafMFkZoBzp9N1z+YGEJqEAFgljS5vNtEUGsPiRe8DUP +A59tnAEF09W7HQDw3hSabyYNGuMndtV575CvyXFBOH4VM6bda+MC+8oy0SyubD/h +daqqd+KNF8QMZrDM6RqcWao= +-----END CERTIFICATE----- diff --git a/freja_test.pem b/freja_test.pem new file mode 100644 index 0000000..8ac1d77 --- /dev/null +++ b/freja_test.pem @@ -0,0 +1,24 @@ +-----BEGIN CERTIFICATE----- +MIIEETCCAvmgAwIBAgIUTeCJ0hz3mbtyONBEiap7su74LZwwDQYJKoZIhvcNAQEL +BQAwgYMxCzAJBgNVBAYTAlNFMRIwEAYDVQQHEwlTdG9ja2hvbG0xFDASBgNVBGET +CzU1OTExMC00ODA2MR0wGwYDVQQKExRWZXJpc2VjIEZyZWphIGVJRCBBQjENMAsG +A1UECxMEVGVzdDEcMBoGA1UEAxMTUlNBIFRFU1QgSXNzdWluZyBDQTAeFw0xNzA3 +MTIxNTIwMTNaFw0yMDA3MTIxNTIwMTNaMIGKMQswCQYDVQQGEwJTRTESMBAGA1UE +BxMJU3RvY2tob2xtMRQwEgYDVQRhEws1NTkxMTAtNDgwNjEdMBsGA1UEChMUVmVy +aXNlYyBGcmVqYSBlSUQgQUIxDTALBgNVBAsTBFRlc3QxIzAhBgNVBAMTGkZyZWph +IGVJRCBURVNUIE9yZyBTaWduaW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB +CgKCAQEAgMINs87TiouDPSSmpn05kZv9TN8XdopcHnElp6ElJLpQh3oYGIL4B71o +IgF3r8zRWq8kQoJlYMugmhsld0r0EsUJbsrcjBJ5CJ1WYZg1Vu8FpYLKoaFRI/qx +T6xCMvd238Q99Sdl6G6O9sQQoFq10EaYBa970Tl3nDziQQ6bbSNkZoOYIZoicx4+ +1XFsrGiru8o8QIyc3g0eSgrd3esbUkuk0eH65SeaaOCrsaCOpJUqEziD+el4R6d4 +0dTz/uxWmNpGKF4BmsNWeQi9b4gDYuFqNYhs7bnahvkK6LvtDThV79395px/oUz5 +BEDdVwjxPJzgaAuUHE+6A1dMapkjsQIDAQABo3QwcjAOBgNVHQ8BAf8EBAMCBsAw +DAYDVR0TAQH/BAIwADAfBgNVHSMEGDAWgBRqfIoPnXAOHNpfLaA8Jl+I6BW/nDAS +BgNVHSAECzAJMAcGBSoDBAUKMB0GA1UdDgQWBBT7j90x8xG2Sg2p7dCiEpsq3mo5 +PTANBgkqhkiG9w0BAQsFAAOCAQEAaKEIpRJvhXcN3MvP7MIMzzuKh2O8kRVRQAoK +Cj0K0R9tTUFS5Ang1fEGMxIfLBohOlRhXgKtqJuB33IKzjyA/1IBuRUg2bEyecBf +45IohG+vn4fAHWTJcwVChHWcOUH+Uv1g7NX593nugv0fFdPqt0JCnsFx2c/r9oym ++VPP7p04BbXzYUk+17qmFBP/yNlltjzfeVnIOk4HauR9i94FrfynuZLuItB6ySCV +mOlfA0r1pHv5sofBEirhwceIw1EtFqEDstI+7XZMXgDwSRYFc1pTjrWMaua2Uktm +JyWZPfIY69pi/z4u+uAnlPuQZnksaGdZiIcAyrt5IXpNCU5wyg== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/frejaeid.php b/frejaeid.php deleted file mode 100644 index 32bbd52..0000000 --- a/frejaeid.php +++ /dev/null @@ -1,311 +0,0 @@ -production = $production; - if ($production) { - $this->serviceUrl = 'https://services.prod.frejaeid.com'; - $this->resourceUrl = 'https://resources.prod.frejaeid.com'; - } else { - $this->serviceUrl = 'https://services.test.frejaeid.com'; - $this->resourceUrl = 'https://resources.test.frejaeid.com'; - } - - if (!is_readable($certificate)) - throw new Exception('Certificate file could not be found'); - - $this->certificate = $certificate; - $this->password = $password; - - if (!$this->IsNullOrEmptyString($jwtCert)) - if (!is_readable($jwtCert)) - throw new Exception('JWT Certificate file could not be found'); - else - $this->jwtCert = $jwtCert; - } - - private function IsNullOrEmptyString($input){ - return (!isset($input) || trim($input)===''); - } - - public function createAuthQRCode($existingCode=NULL) { - - if ($this->IsNullOrEmptyString($existingCode)) { - - $response = $this->initAuthentication(); - if (isset($response->authRef)) - $existingCode = $response->authRef; - else - throw new Exception('Could not create authentication reference'); - - } - - $qrObject = new \stdClass(); - $qrObject->url = $this->resourceUrl . "/qrcode/generate?qrcodedata=frejaeid%3A%2F%2FbindUserToTransaction%3Fdimension%3D4x%3FtransactionReference%3D" . $existingCode; - $qrObject->authRef = $existingCode; - - return $qrObject; - } - - public function cancelAuthentication($authRef) { - $query = new \stdClass(); $query->authRef = $authRef; - - $apiPost = array( - "cancelAuthRequest" => base64_encode(json_encode($query)) - ); - - $apiPostQuery = http_build_query($apiPost); - $result = $this->apiRequest('/authentication/1.0/cancel',$apiPostQuery); - return $result; - } - - public function checkAuthentication($authRef) { - $query = new \stdClass(); $query->authRef = $authRef; - - $apiPost = array( - "getOneAuthResultRequest" => base64_encode(json_encode($query)) - ); - - $apiPostQuery = http_build_query($apiPost); - $result = $this->apiRequest('/authentication/1.0/getOneResult',$apiPostQuery); - return $result; - } - - public function initAuthentication($userType="N/A",$userInfo="N/A",$authLevel="BASIC") { - - - $emailAttribute = new \stdClass(); $emailAttribute->attribute = "EMAIL_ADDRESS"; - $userAttribute = new \stdClass(); $userAttribute->attribute = "RELYING_PARTY_USER_ID"; - $basicAttribute = new \stdClass(); $basicAttribute->attribute = "BASIC_USER_INFO"; - $dobAttribute = new \stdClass(); $dobAttribute->attribute = "DATE_OF_BIRTH"; - $ssnAttribute = new \stdClass(); $ssnAttribute->attribute = "SSN"; - - $query = new \stdClass(); $query->attributesToReturn = array ( $emailAttribute ); - array_push($query->attributesToReturn, $userAttribute ); - - switch ($userType) { - case "N/A": - $query->userInfoType = "INFERRED"; - $query->userInfo = "N/A"; - break; - case "PHONE": - $query->userInfoType = "PHONE"; - $query->userInfo = $userInfo; - break; - case "EMAIL": - $query->userInfoType = "EMAIL"; - $query->userInfo = $userInfo; - break; - case "SSN": - $query->userInfoType = "SSN"; - $ssnUserinfo = new \stdClass(); - $ssnUserinfo->country = "SE"; - $ssnUserinfo->ssn = $userInfo; - $query->userInfo = base64_encode(json_encode($ssnUserinfo)); - break; - default: - throw new Exception('User type not N/A, EMAIL or PHONE'); - break; - } - switch ($authLevel) { - case "BASIC": - $query->minRegistrationLevel = "BASIC"; - break; - case "EXTENDED": - $query->minRegistrationLevel = "EXTENDED"; - array_push($query->attributesToReturn, $basicAttribute); - array_push($query->attributesToReturn, $dobAttribute); - array_push($query->attributesToReturn, $ssnAttribute); - break; - case "PLUS": - $query->minRegistrationLevel = "PLUS"; - array_push($query->attributesToReturn, $basicAttribute); - array_push($query->attributesToReturn, $dobAttribute); - array_push($query->attributesToReturn, $ssnAttribute); - break; - default: - throw new Exception('User type not BASIC, EXTENDED or PLUS'); - break; - } - - $apiPost = array( - "initAuthRequest" => base64_encode(json_encode($query)) - ); - $apiPostQuery = http_build_query($apiPost); - $result = $this->apiRequest('/authentication/1.0/initAuthentication',$apiPostQuery); - - return $result; - } - - public function initSignatureRequest($userType,$userInfo,$agreementText,$agreementTitle,$authLevel="BASIC",$timeoutMinutes=2,$confidential=false,$pushTitle=NULL,$pushMessage=NULL,$binaryData=NULL) { - - $emailAttribute = new \stdClass(); $emailAttribute->attribute = "EMAIL_ADDRESS"; - $userAttribute = new \stdClass(); $userAttribute->attribute = "RELYING_PARTY_USER_ID"; - $basicAttribute = new \stdClass(); $basicAttribute->attribute = "BASIC_USER_INFO"; - $dobAttribute = new \stdClass(); $dobAttribute->attribute = "DATE_OF_BIRTH"; - $ssnAttribute = new \stdClass(); $ssnAttribute->attribute = "SSN"; - - $query = new \stdClass(); $query->attributesToReturn = array ( $emailAttribute ); - array_push($query->attributesToReturn, $userAttribute ); - - if ($this->IsNullOrEmptyString($agreementText) or $this->IsNullOrEmptyString($agreementTitle)) - throw new Exception('Agreement text and title must be specified'); - - switch ($userType) { - case "PHONE": - $query->userInfoType = "PHONE"; - $query->userInfo = $userInfo; - break; - case "EMAIL": - $query->userInfoType = "EMAIL"; - $query->userInfo = $userInfo; - break; - case "SSN": - $query->userInfoType = "SSN"; - $ssnUserinfo = new \stdClass(); - $ssnUserinfo->country = "SE"; - $ssnUserinfo->ssn = $userInfo; - $query->userInfo = base64_encode(json_encode($ssnUserinfo)); - break; - default: - throw new Exception('User type not EMAIL or PHONE'); - break; - } - switch ($authLevel) { - case "BASIC": - $query->minRegistrationLevel = "BASIC"; - break; - case "EXTENDED": - $query->minRegistrationLevel = "EXTENDED"; - array_push($query->attributesToReturn, $basicAttribute); - array_push($query->attributesToReturn, $dobAttribute); - array_push($query->attributesToReturn, $ssnAttribute); - break; - case "PLUS": - $query->minRegistrationLevel = "PLUS"; - array_push($query->attributesToReturn, $basicAttribute); - array_push($query->attributesToReturn, $dobAttribute); - array_push($query->attributesToReturn, $ssnAttribute); - break; - default: - throw new Exception('User type not BASIC, EXTENDED or PLUS'); - break; - } - - $query->title = $agreementTitle; - $query->confidential = $confidential; - $query->expiry = (time() + ($timeoutMinutes * 60))*1000; - - if (!$this->IsNullOrEmptyString($pushTitle)) { - $pushNotification = new \stdClass(); $pushNotification->title = $pushTitle; - $pushNotification->text = $pushMessage; - $query->pushNotification = $pushNotification; - } - - $dataToSign = new \stdClass(); $dataToSign->text = base64_encode($agreementText); - if ($this->IsNullOrEmptyString($binaryData)) { - $query->dataToSign = $dataToSign; - $query->dataToSignType = "SIMPLE_UTF8_TEXT"; - $query->signatureType = "SIMPLE"; - } else { - $dataToSign->binaryData = base64_encode($binaryData); - $query->dataToSign = $dataToSign; - $query->dataToSignType = "EXTENDED_UTF8_TEXT"; - $query->signatureType = "EXTENDED"; - } - - $apiPost = array( - "initSignRequest" => base64_encode(json_encode($query)) - ); - $apiPostQuery = http_build_query($apiPost); - $result = $this->apiRequest('/sign/1.0/initSignature',$apiPostQuery); - - return $result; - } - - public function checkSignatureRequest($signRef) { - $query = new \stdClass(); $query->signRef = $signRef; - - $apiPost = array( - "getOneSignResultRequest" => base64_encode(json_encode($query)) - ); - - $apiPostQuery = http_build_query($apiPost); - $result = $this->apiRequest('/sign/1.0/getOneResult',$apiPostQuery); - return $result; - } - - public function cancelSignatureRequest($signRef) { - $query = new \stdClass(); $query->signRef = $signRef; - - $apiPost = array( - "cancelSignRequest" => base64_encode(json_encode($query)) - ); - - $apiPostQuery = http_build_query($apiPost); - $result = $this->apiRequest('/sign/1.0/cancel',$apiPostQuery); - return $result; - } - - public function verifyJWT($jwsString) { - if (!class_exists('Namshi\JOSE\SimpleJWS')) { - $jws = SimpleJWS::load($jwsString); - $public_key = openssl_pkey_get_public($this->jwtCert); - - if ($jws->isValid($public_key)) - return true; - else - return false; - } - } - - private function apiRequest($apiUrl,$apiPostQuery){ - - $curl = curl_init(); - - $apiHeader = array(); - $apiHeader[] = 'Content-length: ' . strlen($apiPostQuery); - $apiHeader[] = 'Content-type: application/json'; - - // cURL Options - $options = array( - CURLOPT_URL => $this->serviceUrl . $apiUrl, - CURLOPT_RETURNTRANSFER => true, - CURLOPT_HEADER => false, // true to show header information - CURLINFO_HEADER_OUT => false, - CURLOPT_HTTPGET => false, - CURLOPT_POST => true, - CURLOPT_FOLLOWLOCATION => false, - CURLOPT_FOLLOWLOCATION => true, - CURLOPT_SSL_VERIFYHOST => false, // true in production - CURLOPT_SSL_VERIFYPEER => false, // true in production - CURLOPT_TIMEOUT => 30, - CURLOPT_MAXREDIRS => 2, - CURLOPT_HTTPHEADER => $apiHeader, - CURLOPT_USERAGENT => 'phpFreja/0.1', - CURLOPT_POSTFIELDS => $apiPostQuery, - CURLOPT_SSLCERTTYPE => 'P12', - CURLOPT_SSLCERT => $this->certificate, - CURLOPT_KEYPASSWD => $this->password - ); - - curl_setopt_array($curl, $options); - $output = curl_exec($curl); - $info =curl_errno($curl)>0 ? array("curl_error_".curl_errno($curl)=>curl_error($curl)) : curl_getinfo($curl); - $json = json_decode($output); - - return $json; - } - - - } - -?> diff --git a/jws/Algorithm/AlgorithmInterface.php b/jws/Algorithm/AlgorithmInterface.php new file mode 100644 index 0000000..b4d2406 --- /dev/null +++ b/jws/Algorithm/AlgorithmInterface.php @@ -0,0 +1,20 @@ +hashAlgo = $hashAlgo; + } + + public function sign($key, $data) + { + return hash_hmac($this->hashAlgo, $data, $key, true); + } + + public function verify($key, $data, $signature) + { + return $this->sign($key, $data) === $signature; + } +} diff --git a/jws/Algorithm/NoneAlgorithm.php b/jws/Algorithm/NoneAlgorithm.php new file mode 100644 index 0000000..1ca6632 --- /dev/null +++ b/jws/Algorithm/NoneAlgorithm.php @@ -0,0 +1,17 @@ +sigAlgo = $sigAlgo; + } + + /** + * @param string $key PEM encoded private key + * @param mixed $data + * @return mixed + */ + public function sign($key, $data) + { + $result = openssl_sign($data, $signature, $key, $this->sigAlgo); + if (!$result) { + throw new \RuntimeException(openssl_error_string()); + } + + return $signature; + } + + /** + * @param string $key PEM encoded public key + * @param mixed $data + * @param mixed $signature + * @return boolean + */ + public function verify($key, $data, $signature) + { + return openssl_verify($data, $signature, $key, $this->sigAlgo) === 1; + } +} diff --git a/jws/Exception/InvalidSignatureException.php b/jws/Exception/InvalidSignatureException.php new file mode 100644 index 0000000..ab5a7d1 --- /dev/null +++ b/jws/Exception/InvalidSignatureException.php @@ -0,0 +1,7 @@ +registerAlgorithms($algorithms); + } else { + //built-in algorithms + $this->registerAlgorithm('HS256', new HMACAlgorithm('sha256')); + $this->registerAlgorithm('HS384', new HMACAlgorithm('sha384')); + $this->registerAlgorithm('HS512', new HMACAlgorithm('sha512')); + + $this->registerAlgorithm('RS256', new RSA_SSA_PKCSv15(OPENSSL_ALGO_SHA256)); + $this->registerAlgorithm('RS384', new RSA_SSA_PKCSv15(OPENSSL_ALGO_SHA384)); + $this->registerAlgorithm('RS512', new RSA_SSA_PKCSv15(OPENSSL_ALGO_SHA512)); + } + } + + /** + * @param AlgorithmInterface[] $algorithms + */ + public function registerAlgorithms(array $algorithms) + { + foreach ($algorithms as $name => $algorithm) { + $this->registerAlgorithm($name, $algorithm); + } + } + + /** + * @param string $name + * @param AlgorithmInterface $algorithm + */ + public function registerAlgorithm($name, AlgorithmInterface $algorithm) + { + $this->algorithms[$name] = $algorithm; + } + + /** + * @return array + */ + public function supportedAlgorithms() + { + return array_keys($this->algorithms); + } + + /** + * @param $name + * @return AlgorithmInterface + */ + private function _getAlgorithm($name) + { + if (!isset($this->algorithms[$name])) { + throw new UnsupportedAlgorithmException(sprintf("Signing algorithm '%s' is not supported", $name)); + } + + return $this->algorithms[$name]; + } + + /** + * @param array $headers + * @param mixed $payload + * @param $key + * @return string + */ + public function encode(array $headers, $payload, $key) + { + if (empty($headers['alg'])) { + throw new UnspecifiedAlgorithmException("'alg' header parameter is required."); + } + + $algorithm = $this->_getAlgorithm($headers['alg']); + + $headerComponent = Base64Url::encode(Json::encode($headers)); + $payloadComponent = Base64Url::encode(Json::encode($payload)); + + $dataToSign = $headerComponent.'.'.$payloadComponent; + $signature = Base64Url::encode($algorithm->sign($key, $dataToSign)); + + return $dataToSign.'.'.$signature; + } + + private function extractSignature($jwsString) + { + $p = strrpos($jwsString, '.'); + + return [substr($jwsString, 0, $p), substr($jwsString, $p + 1) ?: '']; + } + + /** + * Only decodes jws string and returns headers and payload. To verify signature use verify method. + * + * @throws MalformedSignatureException + * @param $jwsString + * @return array( + * 'headers' => array(), + * 'payload' => payload data + * ) + */ + public function decode($jwsString) + { + $components = explode('.', $jwsString); + if (count($components) !== 3) { + throw new MalformedSignatureException('JWS string must contain 3 dot separated component.'); + } + + try { + $headers = Json::decode(Base64Url::decode($components[0])); + $payload = Json::decode(Base64Url::decode($components[1])); + } catch (\InvalidArgumentException $e) { + throw new MalformedSignatureException("Cannot decode signature headers and/or payload"); + } + + return [ + 'headers' => $headers, + 'payload' => $payload + ]; + } + + /** + * @param $jwsString + * @param $key + * @param $expectedAlgorithm + * + * @throws UnspecifiedAlgorithmException + * @throws MalformedSignatureException + * @throws UnspecifiedAlgorithmException + * @throws InvalidSignatureException + * @throws UnexpectedAlgorithmException + * + * @return array( + * 'headers' => array(), + * 'payload' => mixed payload data + * ) + */ + public function verify($jwsString, $key, $expectedAlgorithm = null) + { + $jws = $this->decode($jwsString); + $headers = $jws['headers']; + + list($dataToSign, $signature) = $this->extractSignature($jwsString); + + if (empty($headers['alg'])) { + throw new UnspecifiedAlgorithmException("No algorithm information found in headers. alg header parameter is required."); + } + + if ($expectedAlgorithm !== null && strtolower($headers['alg']) !== strtolower($expectedAlgorithm)) { + throw new UnexpectedAlgorithmException(sprintf("Algorithm '%s' is not expected. Expected algorithm is '%s'", + $headers['alg'], $expectedAlgorithm)); + } + + $algorithm = $this->_getAlgorithm($headers['alg']); + if (!$algorithm->verify($key, $dataToSign, Base64Url::decode($signature))) { + throw new InvalidSignatureException("Invalid signature"); + } + + return $jws; + } +} diff --git a/jws/Util/Base64Url.php b/jws/Util/Base64Url.php new file mode 100644 index 0000000..acc845e --- /dev/null +++ b/jws/Util/Base64Url.php @@ -0,0 +1,25 @@ +