├── src └── Trianglman │ └── Sqrl │ ├── Tests │ ├── Resources │ │ ├── bad.json │ │ ├── unittest.txt │ │ ├── onlyReq.json │ │ ├── unittest.json │ │ └── allOptional.json │ ├── TestScenario.php │ ├── Ed25519 │ │ └── CryptoTest.php │ ├── SodiumNonceValidatorTest.php │ ├── SqrlConfigurationTest.php │ ├── SqrlStoreStatelessAbstractTest.php │ ├── SqrlGenerateTest.php │ ├── RequestHandlerScenario.php │ └── SqrlValidateTest.php │ ├── SodiumNonceValidator.php │ ├── NonceValidatorInterface.php │ ├── SqrlException.php │ ├── Ed25519 │ ├── CryptoInterface.php │ └── Crypto.php │ ├── Traits │ ├── Base64Url.php │ └── SqrlUrlGenerator.php │ ├── SqrlGenerateInterface.php │ ├── SqrlValidateInterface.php │ ├── SqrlGenerate.php │ ├── SqrlValidate.php │ ├── SqrlStoreInterface.php │ ├── SqrlStoreStatelessAbstract.php │ ├── SqrlRequestHandlerInterface.php │ ├── SqrlConfiguration.php │ └── SqrlRequestHandler.php ├── assets ├── sqrl_logo.xcf ├── sqrl_php_logo_150px.png ├── sqrl_php_logo_draft.png └── sqrl_php_logo_draft.png.xmp ├── .gitignore ├── .travis.yml ├── .idea └── inspectionProfiles │ └── Project_Default.xml ├── examples ├── server │ ├── config │ │ └── sqrlconfig.json │ ├── web │ │ ├── login │ │ │ ├── logout.php │ │ │ ├── isNonceValidated.php │ │ │ └── sqrlauth.php │ │ ├── sqrlImg.php │ │ ├── account.php │ │ └── index.php │ └── includes │ │ └── ExampleStatefulStorage.php └── config.sample.json ├── composer.json ├── phpunit.xml.dist ├── tools ├── sign.php ├── createServerResponseFile.php └── createClientResponseFile.php ├── LICENSE └── README.md /src/Trianglman/Sqrl/Tests/Resources/bad.json: -------------------------------------------------------------------------------- 1 | ["lbah" -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/Tests/Resources/unittest.txt: -------------------------------------------------------------------------------- 1 | JSON = false 2 | -------------------------------------------------------------------------------- /assets/sqrl_logo.xcf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trianglman/sqrl/HEAD/assets/sqrl_logo.xcf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /netbeans/ 2 | /vendor/ 3 | /vagrant/ 4 | /.idea/ 5 | composer.lock 6 | composer.phar 7 | -------------------------------------------------------------------------------- /assets/sqrl_php_logo_150px.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trianglman/sqrl/HEAD/assets/sqrl_php_logo_150px.png -------------------------------------------------------------------------------- /assets/sqrl_php_logo_draft.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trianglman/sqrl/HEAD/assets/sqrl_php_logo_draft.png -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.2 5 | 6 | before_script: 7 | - sudo apt-get update 8 | - composer install 9 | -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/Tests/Resources/onlyReq.json: -------------------------------------------------------------------------------- 1 | { 2 | "accepted_versions": [1], 3 | "key_domain":"domain.com", 4 | "authentication_path":"login/sqrlauth.php" 5 | } 6 | -------------------------------------------------------------------------------- /.idea/inspectionProfiles/Project_Default.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/Tests/Resources/unittest.json: -------------------------------------------------------------------------------- 1 | { 2 | "accepted_versions": [1], 3 | "secure":1, 4 | "key_domain":"domain.com", 5 | "authentication_path":"login/sqrlauth.php", 6 | "friendly_name":"Example Server", 7 | 8 | "height":"30", 9 | "padding":"1", 10 | "nonce_salt":"testdata" 11 | } 12 | -------------------------------------------------------------------------------- /examples/server/config/sqrlconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "accepted_versions": [1], 3 | "secure":0, 4 | "key_domain":"localhost", 5 | "authentication_path":"login/sqrlauth.php", 6 | "friendly_name":"Example SQRL Server", 7 | "allow_anonymous_accounts":1, 8 | 9 | "nonce_max_age":"5", 10 | 11 | "height":"150", 12 | "padding":"10", 13 | "nonce_salt":"replace this with some unique random data" 14 | } 15 | -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/Tests/Resources/allOptional.json: -------------------------------------------------------------------------------- 1 | { 2 | "accepted_versions": [1], 3 | "secure":1, 4 | "key_domain":"otherdomain.com", 5 | "authentication_path":"sqrl.php", 6 | "allow_anonymous_accounts":1, 7 | 8 | "dsn":"mysql:host=localhost;dbname=sqrl", 9 | "username":"foo", 10 | "password":"bar", 11 | "nonce_table":"sqrl_nonce", 12 | "pubkey_table":"sqrl_pubkey", 13 | 14 | "nonce_max_age":9, 15 | 16 | "height":250, 17 | "padding":5, 18 | "nonce_salt":"gibberish data" 19 | } 20 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trianglman/sqrl", 3 | "description": "PHP Server side implementation of a SQRL generator/listener", 4 | "authors": [ 5 | { 6 | "name": "johnj", 7 | "email": "johnj.a.judy@gmail.com" 8 | } 9 | ], 10 | "autoload": { 11 | "psr-0": { 12 | "Trianglman\\Sqrl": "src/" 13 | } 14 | }, 15 | "require": { 16 | "php": ">=7.2.0", 17 | "endroid/qr-code": "~1.1.3" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "~7.2.4" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /examples/config.sample.json: -------------------------------------------------------------------------------- 1 | { 2 | "accepted_versions": [1], 3 | "secure":1, 4 | "key_domain":"domain.com", 5 | "authentication_path":"/login/sqrlauth.php", 6 | "friendly_name":"Example Server", 7 | "allow_anonymous_accounts":0, 8 | 9 | "dsn":"mysql:host=localhost;dbname=sqrl", 10 | "username":"foo", 11 | "password":"bar", 12 | "nonce_table":"sqrl_nonce", 13 | "pubkey_table":"sqrl_pubkey", 14 | 15 | "nonce_max_age":"5", 16 | 17 | "height":"300", 18 | "padding":"10", 19 | "nonce_salt":"replace this with some unique random data" 20 | } 21 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 13 | 14 | 15 | ./src/Trianglman/Sqrl/Tests 16 | 17 | 18 | 19 | 20 | 21 | ./ 22 | 23 | ./src/Trianglman/Sqrl/Tests/Resources 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tools/sign.php: -------------------------------------------------------------------------------- 1 | publickey($sk); 17 | 18 | $sig = $obj->signature($m, $sk, $pk); 19 | 20 | echo 'Message: "'.$m."\"\n"; 21 | echo 'Public Key(base64url): '.base64UrlEncode($pk)."\n"; 22 | echo 'Signature(base64Url) : '.base64UrlEncode($sig)."\n"; 23 | echo 'Verifies? '.($obj->checkvalid($sig, $m, $pk)?'yes':'no')."\n"; 24 | 25 | 26 | 27 | function base64UrlEncode($string) 28 | { 29 | $base64 = base64_encode($string); 30 | $urlencode = str_replace(array('+','/'), array('-','_'), $base64); 31 | $urlencode = trim($urlencode, '='); 32 | return $urlencode; 33 | } 34 | -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/Tests/TestScenario.php: -------------------------------------------------------------------------------- 1 | test = $test; 16 | } 17 | 18 | /** 19 | * Sets up the data that would be set on the server end before the scenario happens 20 | * 21 | * @param callable $run 22 | * 23 | * @return TestScenario 24 | */ 25 | public function given(callable $run): TestScenario 26 | { 27 | $run(); 28 | return $this; 29 | } 30 | 31 | /** 32 | * Describes the scenario happening 33 | * 34 | * @param callable $run 35 | * 36 | * @return TestScenario 37 | */ 38 | public function when(callable $run): TestScenario 39 | { 40 | $run(); 41 | return $this; 42 | } 43 | 44 | /** 45 | * @param callable $run 46 | */ 47 | public function then(callable $run): void 48 | { 49 | $run(); 50 | } 51 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 John Judy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /tools/createServerResponseFile.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /tools/createClientResponseFile.php: -------------------------------------------------------------------------------- 1 | load(__DIR__.'/../config/sqrlconfig.json'); 35 | $store = new ExampleStatefulStorage(new \PDO('mysql:host=localhost;dbname=sqrl', 'example', 'bar'),$_SERVER['REMOTE_ADDR'],$_SESSION); 36 | $generator = new \Trianglman\Sqrl\SqrlGenerate($config,$store); 37 | 38 | header('Content-Type: image/png'); 39 | $generator->render(null); -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/Ed25519/CryptoInterface.php: -------------------------------------------------------------------------------- 1 | 0){ 55 | $string = str_pad($string, 4-($len%4), '='); 56 | } 57 | $base64 = str_replace(array('-','_'), array('+','/'), $string); 58 | return base64_decode($base64); 59 | } 60 | } -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/SqrlGenerateInterface.php: -------------------------------------------------------------------------------- 1 | getSecure() ? 's' : '').'qrl://'.$config->getDomain(); 55 | if (strpos($config->getDomain(), '/') !== false) { 56 | $extension = strlen($config->getDomain())-strpos($config->getDomain(), '/'); 57 | $url.= substr($this->generateQry($config->getAuthenticationPath(), $nut), $extension).'&x='.$extension; 58 | } else { 59 | $url.= $this->generateQry($config->getAuthenticationPath(), $nut); 60 | } 61 | return $url; 62 | } 63 | } -------------------------------------------------------------------------------- /examples/server/web/account.php: -------------------------------------------------------------------------------- 1 | load(__DIR__.'/../config/sqrlconfig.json'); 33 | $store = new \Trianglman\Sqrl\SqrlStore($config); 34 | 35 | if (isset($_SESSION['publicKey'])) { 36 | $acccount = $store->retrieveAuthenticationRecord( 37 | $_SESSION['publicKey'], 38 | array(SqrlStoreInterface::SUK, SqrlStoreInterface::VUK) 39 | ); 40 | } 41 | if (empty($account)) { 42 | header('Location: /index.php',true,303);//send the user back to the index page to get a new nonce 43 | } 44 | ?> 45 | 46 | 47 | 48 | 49 | 50 | SQRL Account 51 | 52 | 53 |

You have successfully signed in using SQRL

54 | 55 | 61 |

62 | Logout 63 |

64 | 65 | 66 | -------------------------------------------------------------------------------- /examples/server/web/index.php: -------------------------------------------------------------------------------- 1 | load(__DIR__.'/../config/sqrlconfig.json'); 34 | $store = new ExampleStatefulStorage(new \PDO('mysql:host=localhost;dbname=sqrl', 'example', 'bar'),$_SERVER['REMOTE_ADDR'],$_SESSION); 35 | $generator = new \Trianglman\Sqrl\SqrlGenerate($config,$store); 36 | 37 | $nonce = $generator->getNonce(); 38 | $sqrlUrl = $generator->getUrl(); 39 | ?> 40 | 41 | 42 | 43 | 44 | SQRL Example Server 45 | 46 | 47 |

Welcome to the SQRL PHP Example Server

48 | 49 |

50 | This server should enable you to walk through a number of test scenarios using the SQRL protocol. 51 |

52 |

53 | Please use the below link/QR code to sign in and either create a new account or view your already entered account information. 54 |

55 |

56 | 57 | SQRL QR Code 58 |
59 |
60 | Verify Login 61 |

62 | 63 | 64 | -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/SqrlValidateInterface.php: -------------------------------------------------------------------------------- 1 | load(__DIR__.'/../../config/sqrlconfig.json'); 32 | $db = new \PDO($config->getDsn(),$config->getUsername(),$config->getPassword()); 33 | $store = new \Trianglman\Sqrl\SqrlStore($config); 34 | $store->setDatabaseConnection($db); 35 | 36 | $validated = false; 37 | if (isset($_SESSION['nonce'])) { 38 | $validated = (int)$store->retrieveNutRecord( 39 | $_SESSION['nonce'], 40 | array(\Trianglman\Sqrl\SqrlStoreInterface::VERIFIED) 41 | ) > 0; 42 | if ($validated) { 43 | //TODO: create a utility function in SqrlStore that will do this work for the developer 44 | $SQL = "SELECT related_public_key FROM sqrl_nonce n JOIN sqrl_nonce_relationship r ON r.new_nonce = n.nonce WHERE r.old_nonce = ?"; 45 | $stmt = $db->prepare($SQL); 46 | $stmt->execute(array($_SESSION['nonce'])); 47 | $result = $stmt->fetchColumn(0); 48 | //Update the session with a user identifier instead of the nonce 49 | $_SESSION['publicKey'] = $result[0]; 50 | unset($_SESSION['nonce']); 51 | unset($_SESSION['generatedTime']); 52 | header('Location: /account.php',true,303); 53 | } 54 | } else { 55 | header('Location: /index.php',true,303);//send the user back to the index page to get a new nonce 56 | } 57 | 58 | 59 | ?> 60 | 61 | 62 | 63 | Verifying Login... 64 | 65 | 66 | 67 | 68 | 69 |

70 | 71 | Your log in has not been validated. This page will refresh in 5 seconds. Click here to check again. 72 | 73 |

74 | 75 | -------------------------------------------------------------------------------- /examples/server/web/login/sqrlauth.php: -------------------------------------------------------------------------------- 1 | load(__DIR__.'/../../config/sqrlconfig.json'); 33 | $conn = new \PDO('mysql:host=localhost;dbname=sqrl', 'example', 'bar'); 34 | $store = new ExampleStatefulStorage($conn,$_SERVER['REMOTE_ADDR']); 35 | $generator = new \Trianglman\Sqrl\SqrlGenerate($config,$store); 36 | if(extension_loaded("ellipticCurveSignature")) { 37 | $sigValidator = new \Trianglman\Sqrl\EcEd25519NonceValidator(); 38 | } else { 39 | $sigValidator = new \Trianglman\Sqrl\Ed25519NonceValidator(); 40 | } 41 | $validator = new \Trianglman\Sqrl\SqrlValidate($config,$sigValidator,$store); 42 | $handler = new \Trianglman\Sqrl\SqrlRequestHandler($config,$validator,$store,$generator); 43 | if (!empty($_POST)) {//this is only necessary for early clients that were not setting the Content-Type header properly 44 | $post = $_POST; 45 | } else { 46 | $post = array(); 47 | parse_str(file_get_contents('php://input'), $post); 48 | } 49 | $handler->parseRequest($_GET, $post,$_SERVER); 50 | $resp = $handler->getResponseMessage(); 51 | echo $resp; 52 | 53 | //This is extra logging code that is only being used while testing clients 54 | //Production servers should not do this 55 | $request = json_encode(array('get'=>$_GET,'post'=>$hardPost,'ip'=>$_SERVER['REMOTE_ADDR'],'uri'=>$_SERVER['REQUEST_URI'])); 56 | $decodedClient = $handler->base64URLDecode($hardPost['client']); 57 | $reqHeaders = json_encode(apache_request_headers()); 58 | $response = $handler->base64URLDecode($resp)."\r\nerror=".$handler->error; 59 | $sql = 'INSERT INTO nutTransactions (nut,request, clientData, responseData,reqHeaders) VALUES (:n,:req,:cli,:resp,:hdr)'; 60 | $stmt = $conn->prepare($sql); 61 | $stmt->bindParam(':n', $_GET['nut']); 62 | $stmt->bindParam(':req', $request); 63 | $stmt->bindParam(':cli', $decodedClient); 64 | $stmt->bindParam(':resp', $response); 65 | $stmt->bindParam(':hdr', $reqHeaders); 66 | $stmt->execute(); -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/Tests/Ed25519/CryptoTest.php: -------------------------------------------------------------------------------- 1 | publickey($sk); 44 | $this->assertEquals($skConcat, bin2hex($sk.$pk)); 45 | $this->assertEquals($pktest, bin2hex($pk)); 46 | } 47 | 48 | /** 49 | * @dataProvider publicKeyProvider 50 | */ 51 | public function testSigns($skConcat, $pktest, $binM, $sigConcat, $sk) 52 | { 53 | $obj = new Crypto(); 54 | $sig = $obj->signature($binM, $sk, hex2bin($pktest)); 55 | $this->assertEquals(hex2bin(substr($sigConcat, 0, 128)), $sig); 56 | } 57 | 58 | /** 59 | * @dataProvider publicKeyProvider 60 | * @throws \Exception 61 | */ 62 | public function testVerify($skConcat, $pktest, $binM, $sigConcat, $sk) 63 | { 64 | $sig = hex2bin(substr($sigConcat, 0, 128)); 65 | $obj = new Crypto(); 66 | $this->assertTrue($obj->checkvalid($sig, $binM, hex2bin($pktest))); 67 | } 68 | 69 | public function publicKeyProvider() 70 | { 71 | $this->markTestSkipped('This is not going to be used in PHP 7.2; Sodium is in core.'); 72 | if (version_compare(phpversion(), '5.4.0', '<')) { 73 | return array(array(0,0,0,0,0)); 74 | } 75 | 76 | $testData = file_get_contents(dirname(__FILE__).'/../Resources/sign.input'); 77 | //use only a subset of the total data because of how long it takes to run each test 78 | $fullDataSet = explode("\n", $testData); 79 | $startofTestSet = rand(0, count($fullDataSet)-10); 80 | $dataRows = array_slice($fullDataSet, $startofTestSet, 10); 81 | $array = array(); 82 | foreach ($dataRows as $set) { 83 | list($skConcat, $pktest, $m, $sigConcat) = explode(':', $set); 84 | $sk = hex2bin(substr($skConcat, 0, 64)); 85 | $binM = hex2bin($m); 86 | $array[] = array($skConcat, $pktest, $binM, $sigConcat, $sk); 87 | } 88 | 89 | return $array; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/Tests/SodiumNonceValidatorTest.php: -------------------------------------------------------------------------------- 1 | markTestSkipped('sodium_crypto_sign_open not supported'); 42 | } 43 | $this->validator = new SodiumNonceValidator(); 44 | } 45 | 46 | /** 47 | * Test data taken from https://www.grc.com/dev/sqrl/Four-phase-update.txt 48 | */ 49 | public function testValidatesTrue() 50 | { 51 | $idk = base64_decode('3DoDRDKwrLAQmpa/6YNQFwq0wZyN5uEChbGYzRGs+jM=='); 52 | //$urlencode = str_replace(array('+','/'), array('-','_'), $base64); 53 | $ids = base64_decode( 54 | 'viqGSMvzIf7y8OHGec4X9zC4IQUQQbbAIWujvPp4uVtsv5sHNfxZCjZh1UoJIVPgJLHXOr3Z+Cu8tUKqPJVjAw=' 55 | ); 56 | $msg = 'dmVyPTENCmNtZD1xdWVyeQ0KaWRrPTNEb0RSREt3ckxBUW1wYV82WU5RRndxMHdaeU41dUVDaGJHWXpSR3Mtak0NCm9wdD1zdWsNCn' 57 | .'BpZGs9MGVnMUd0ZVlMNnN3Sy13RnJhR0NQNnJUOE9GbF9DbEpCcXRybjc2ZFgtaw0Kc3FybDovL3d3dy5zdGV2ZS9zcXJsP251dD1pbV' 58 | .'RUUE1FVV9WM3VUamc3MldDMmNnJnNmbj1SMUpE'; 59 | $this->assertTrue($this->validator->validateSignature($msg, $ids, $idk), 'Signature failed to validate'); 60 | } 61 | 62 | /** 63 | * Modified two characters of the above message 64 | * @depends testValidatesTrue 65 | */ 66 | public function testValidatesFalse() 67 | { 68 | $idk = base64_decode('3DoDRDKwrLAQmpa/6YNQFwq0wZyN5uEChbGYzRGs+jM=='); 69 | $ids = base64_decode( 70 | 'viqGSMvzIf7y8OHGec4X9zC4IQUQQbbAIWujvPp4uVtsv5sHNfxZCjZh1UoJIVPgJLHXOr3Z+Cu8tUKqPJVjAw=' 71 | ); 72 | $msg = 'dmVyPTENCmNtZD1xdWVyeiiKaWRrPTNEb0RSREt3ckxBUW1wYV82WU5RRndxMHdaeU41dUVDaGJHWXpSR3Mtak0NCm9wdD1zdWsNCn' 73 | .'BpZGs9MGVnMUd0ZVlMNnN3Sy13RnJhR0NQNnJUOE9GbF9DbEpCcXRybjc2ZFgtaw0Kc3FybDovL3d3dy5zdGV2ZS9zcXJsP251dD1pbV' 74 | .'RUUE1FVV9WM3VUamc3MldDMmNnJnNmbj1SMUpE'; 75 | $this->assertFalse($this->validator->validateSignature($msg, $ids, $idk), 'Signature incorrectly validated'); 76 | } 77 | } -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/Tests/SqrlConfigurationTest.php: -------------------------------------------------------------------------------- 1 | load(__DIR__.'/Resources/onlyReq.json'); 40 | 41 | $this->assertEquals([1], $obj->getAcceptedVersions()); 42 | $this->assertEquals('domain.com', $obj->getDomain()); 43 | $this->assertEquals('login/sqrlauth.php', $obj->getAuthenticationPath()); 44 | 45 | //check defaults are unchanged 46 | $this->assertFalse($obj->getSecure()); 47 | $this->assertFalse($obj->getAnonAllowed()); 48 | $this->assertEquals(5, $obj->getNonceMaxAge()); 49 | $this->assertEquals(300, $obj->getQrHeight()); 50 | $this->assertEquals(10, $obj->getQrPadding()); 51 | } 52 | 53 | public function testLoadsFullJsonConfig() 54 | { 55 | $obj = new SqrlConfiguration(); 56 | $obj->load(__DIR__.'/Resources/allOptional.json'); 57 | 58 | $this->assertEquals([1], $obj->getAcceptedVersions()); 59 | $this->assertEquals('otherdomain.com', $obj->getDomain()); 60 | $this->assertEquals('sqrl.php', $obj->getAuthenticationPath()); 61 | $this->assertTrue($obj->getSecure()); 62 | $this->assertTrue($obj->getAnonAllowed()); 63 | $this->assertEquals(9, $obj->getNonceMaxAge()); 64 | $this->assertEquals(250, $obj->getQrHeight()); 65 | $this->assertEquals(5, $obj->getQrPadding()); 66 | $this->assertEquals('gibberish data', $obj->getNonceSalt()); 67 | } 68 | 69 | /** 70 | * @expectedException \InvalidArgumentException 71 | * @expectedExceptionMessage Configuration data could not be parsed. 72 | */ 73 | public function testExceptionOnFileNotExisting() 74 | { 75 | $obj = new SqrlConfiguration(); 76 | $obj->load(__DIR__.'/Resources/file_does_not_exist.json'); 77 | } 78 | 79 | /** 80 | * @expectedException \InvalidArgumentException 81 | * @expectedExceptionMessage Configuration data could not be parsed. 82 | */ 83 | public function testExceptionOnInvalidJsonFormat() 84 | { 85 | $obj = new SqrlConfiguration(); 86 | $obj->load(__DIR__.'/Resources/bad.json'); 87 | } 88 | 89 | public function testSetAcceptedVersionsWithSingleValue() 90 | { 91 | $obj = new SqrlConfiguration(); 92 | $obj->setAcceptedVersions(1); 93 | $this->assertEquals(array(1),$obj->getAcceptedVersions()); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | Build Status 5 |

6 | 7 | # sqrl 8 | 9 | PHP Server side implementation of a SQRL generator/listener 10 | 11 | This project is in *pre-alpha* until there is a defined reference implementation. 12 | 13 | Follow the conversation at [grc sqrl newsgroup](https://www.grc.com/groups/sqrl) for updates on the 14 | standard. 15 | 16 | 17 | ## Software Requirements 18 | 19 | - Composer - http://getcomposer.org 20 | - Endroid/qrcode Loaded automatically by Composer - https://github.com/endroid/QrCode 21 | 22 | ## Purpose 23 | 24 | The goal of this software is to provide a simple PHP implementation of Steve 25 | Gibson's SQRL authentication proposal. This library will allow any site using it 26 | to generate the QR code with a nonce, validate a signed nonce, and store the 27 | public key for connection to a site account. 28 | 29 | ## Installation 30 | 31 | ### Composer 32 | 33 | 1. Download the [`composer.phar`](https://getcomposer.org/composer.phar) executable or use the installer. 34 | 35 | ``` sh 36 | $ curl -sS https://getcomposer.org/installer | php 37 | ``` 38 | 39 | 2. Create a composer.json defining your dependencies. Note that this example is 40 | a short version for applications that are not meant to be published as packages 41 | themselves. To create libraries/packages please read the 42 | [documentation](http://getcomposer.org/doc/02-libraries.md). 43 | 44 | ``` json 45 | "require": { 46 | "trianglman/sqrl": "dev-master" 47 | } 48 | ``` 49 | 50 | 3. Run Composer: `php composer.phar update` 51 | 52 | ### Configuration 53 | 54 | If you want to have the library automatically store generated nonces and validated 55 | public keys, first generate the database tables based on the supplied 56 | [ExampleStatefulStorage.php](examples/server/includes/ExampleStatefulStorage.php), then create a JSON config file based on the sample 57 | provided in sqrl/config.sample.json. You can then configure the 58 | generator or validator by calling the appropriate `configure($filepath);` 59 | method. 60 | 61 | If you would rather manage storage of this information in your own tables, you can 62 | configure the generator manually: 63 | 64 | ```php 65 | $generator = new \Trianglman\Sqrl\SqrlGenerate(); 66 | //whether SQRL responses should come back over SSL (sqrl://) 67 | $generator->setSecure(true); 68 | //the domain sqrl clients should generate their key off of 69 | $generator->setKeyDomain('www.example.com'); 70 | //the path to the SQRL authentication script relative to the key domain 71 | $generator->setAuthenticationPath('sqrl/login.php'); 72 | 73 | //The above would generate a SQRL URL pointing to 74 | //sqrl://www.example.com/sqrl/login.php 75 | //... 76 | ``` 77 | 78 | You can also configure the size of the QR code generated and the amount of 79 | padding between the image edge and the start of the code, as well as supply 80 | your own salt for the nonce: 81 | 82 | ```php 83 | //... 84 | $generator->setHeight(300); 85 | $generator->setPadding(10); 86 | $generator->setSalt('foo'); 87 | //... 88 | ``` 89 | 90 | ### Usage 91 | 92 | **Generate a nonce** 93 | ```php 94 | //Initialize the generator 95 | $generator = new \Trianglman\Sqrl\SqrlGenerate(); 96 | $generator->configure('/path/to/config'); 97 | 98 | //output the QR file to stdout 99 | $generator->render(); 100 | 101 | //get the nonce for other uses, i.e. link, etc. 102 | $nonce = $generator->getNonce(); 103 | ``` 104 | 105 | **Verify a user's input** 106 | ```php 107 | //initialize the validator 108 | $validator = new \Trianglman\Sqrl\SqrlValidate(); 109 | $validator->configure('/path/to/config'); 110 | $validator->setValidator(new \Trianglman\Sqrl\ed25519\Crypto()); 111 | 112 | //initialize the request handler 113 | $requestResponse = new \Trianglman\Sqrl\SqrlRequestHandler($validator); 114 | $requestResponse->parseRequest($_GET, $_POST, $_SERVER); 115 | 116 | //check validation 117 | $requestResponse = $obj->getResponseMessage(); 118 | $requestResponseCode = $obj->getResponseCode(); 119 | 120 | //OR 121 | 122 | //Let the request handler also handle the response 123 | $reqHandler->sendResponse(); 124 | ``` 125 | 126 | # Resources 127 | 128 | * [GRC.com - SQRL Explainted](https://www.grc.com/sqrl/SQRL_Explained.pdf) (pdf) 129 | * [GRC.com - SQRL Semantincs](https://www.grc.com/sqrl/semantics.htm) 130 | * [GRC.com - SQRL Protocol](https://www.grc.com/sqrl/protocol.htm) -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/SqrlGenerate.php: -------------------------------------------------------------------------------- 1 | configuration = $config; 57 | $this->store = $storage; 58 | } 59 | 60 | /** 61 | * Returns the generated nonce 62 | * 63 | * @param int $action [Optional] The type of action this nonce is being generated for 64 | * @param string $key [Optional] The public key associated with the nonce 65 | * @param string $previousNonce [Optional] The previous nonce in the transaction that should be associated to this nonce 66 | * 67 | * @return string The one time use string for the QR link 68 | */ 69 | public function getNonce(int $action = 0, string $key = '', string $previousNonce = ''): string 70 | { 71 | if (!!($action&SqrlRequestHandler::CLIENT_FAILURE)) { 72 | $this->nonce = 'failnut'; 73 | } 74 | if (empty($this->nonce)) { 75 | if ($this->store instanceof SqrlStoreStatelessAbstract) { 76 | $this->nonce = $this->store->generateNut($action, $key, $previousNonce); 77 | return $this->nonce; 78 | } 79 | if ($action === 0) { 80 | $check = $this->store->getSessionNonce(); 81 | if (!empty($check)) { 82 | $this->nonce = $check; 83 | return $this->nonce; 84 | } 85 | } 86 | $this->generateNonce($action, $key,$previousNonce); 87 | } 88 | 89 | return $this->nonce; 90 | } 91 | 92 | public function generateQry(): string 93 | { 94 | return $this->traitGenerateQry($this->configuration->getAuthenticationPath(), $this->getNonce()); 95 | } 96 | 97 | public function getUrl(): string 98 | { 99 | return $this->generateUrl($this->configuration, $this->getNonce()); 100 | } 101 | 102 | public function render(?string $outputFile) 103 | { 104 | $qrCode = new QrCode(); 105 | $qrCode->setText($this->getUrl()); 106 | $qrCode->setSize($this->configuration->getQrHeight()); 107 | $qrCode->setPadding($this->configuration->getQrPadding()); 108 | $qrCode->render($outputFile); 109 | } 110 | 111 | /** 112 | * Generates a random, one time use key to be used in the sqrl validation 113 | * 114 | * The implementation of this may get more complicated depending on the 115 | * requirements detailed in any reference implementation. Users wanting to 116 | * make this library more (or less) secure should override this function 117 | * to strengthen (or weaken) the randomness of the generation. 118 | * 119 | * @param int $action [Optional] The type of action this nonce is being generated for 120 | * @param string $key [Optional] The public key associated with the nonce 121 | * @param string $previousNonce [Optional] The previous nonce in the transaction that should be associated to this nonce 122 | * 123 | * @return string 124 | */ 125 | protected function generateNonce($action = 0, $key = '', $previousNonce='') 126 | { 127 | $this->nonce = hash_hmac('sha256', uniqid('', true), $this->configuration->getNonceSalt()); 128 | $this->store->storeNonce($this->nonce, $action, $key,$previousNonce); 129 | return $this->nonce; 130 | } 131 | 132 | } 133 | -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/SqrlValidate.php: -------------------------------------------------------------------------------- 1 | configuration = $config; 67 | $this->validator = $validator; 68 | $this->store = $storage; 69 | } 70 | 71 | /** 72 | * Validates the returned server value 73 | * 74 | * @param string|array $server The returned server value 75 | * @param string $nut The nut from the request 76 | * @param bool $secure Whether the request was secure 77 | * 78 | * @return boolean 79 | */ 80 | public function validateServer($server, string $nut, bool $secure): bool 81 | { 82 | if (is_string($server)) { 83 | return $server === $this->generateUrl($this->configuration, $nut) && 84 | $secure === $this->configuration->getSecure(); 85 | } else { 86 | if (!isset($server['ver']) || 87 | !isset($server['nut']) || 88 | !isset($server['tif']) || 89 | !isset($server['qry']) 90 | ) { 91 | return false; 92 | } 93 | $nutInfo = $this->store->getNutDetails($nut); 94 | return $server['ver'] === implode(',', $this->configuration->getAcceptedVersions()) && 95 | $server['nut'] === $nut && 96 | (!is_array($nutInfo) || hexdec($server['tif']) === $nutInfo['tif']) && 97 | $server['qry'] === $this->generateQry($this->configuration->getAuthenticationPath(), $nut) && 98 | $secure === $this->configuration->getSecure(); 99 | } 100 | } 101 | 102 | /** 103 | * Validates a supplied nut 104 | * 105 | * @param string $nut 106 | * @param string $signingKey The key used to sign the current request 107 | * 108 | * @return int One of the nut class constants 109 | */ 110 | public function validateNut(string $nut, string $signingKey = null): int 111 | { 112 | $nutInfo = $this->store->getNutDetails($nut); 113 | $maxAge = '-'.$this->configuration->getNonceMaxAge().' minutes'; 114 | if (!is_array($nutInfo)) { 115 | return self::INVALID_NUT; 116 | } elseif ($nutInfo['createdDate']->format('U') < strtotime($maxAge)) { 117 | return self::EXPIRED_NUT; 118 | } elseif (!is_null($signingKey) && 119 | !empty($nutInfo['originalKey']) && 120 | $nutInfo['originalKey'] !== $signingKey 121 | ) { 122 | return self::KEY_MISMATCH; 123 | } else { 124 | return self::VALID_NUT; 125 | } 126 | } 127 | 128 | /** 129 | * Validates the message signature 130 | * 131 | * @param string $orig 132 | * @param string $key 133 | * @param string $sig 134 | * 135 | * @return boolean 136 | */ 137 | public function validateSignature(string $orig, string $key, string $sig): bool 138 | { 139 | return $this->validator->validateSignature($orig, $sig, $key); 140 | } 141 | 142 | /** 143 | * Verifies the original nut's IP matches the current IP 144 | * 145 | * @param string $nut 146 | * @param string $ip 147 | * 148 | * @return boolean 149 | */ 150 | public function nutIPMatches(string $nut, string $ip): bool 151 | { 152 | $nutInfo = $this->store->getNutDetails($nut); 153 | return is_array($nutInfo) && $nutInfo['nutIP'] === $ip; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/SqrlStoreInterface.php: -------------------------------------------------------------------------------- 1 | int The tif stored with the nut (0 for first request nuts) 59 | * 'originalKey'=> string The key associated with the nut, if any 60 | * 'originalNut'=> string The nut that came before this one in the transaction, if any 61 | * 'createdDate'=> \DateTime The time the nut was created 62 | * 'nutIP'=> string the IP address that requested the nut 63 | * 'sessionId'=> string the session ID for the nut [this is only required in stateless nuts] 64 | */ 65 | public function getNutDetails($nut); 66 | 67 | /** 68 | * Checks the status of an identity key 69 | * 70 | * @param string $key 71 | * 72 | * @return int One of the class key status constants 73 | */ 74 | public function checkIdentityKey($key); 75 | 76 | /** 77 | * Activates a session 78 | * 79 | * @param string $requestNut The nut of the current request that is being logged in 80 | * 81 | * @return void 82 | */ 83 | public function logSessionIn($requestNut); 84 | 85 | /** 86 | * Stores a new identity key along with the Identity Lock information 87 | * 88 | * @param string $key 89 | * @param string $suk 90 | * @param string $vuk 91 | * 92 | * @return void 93 | */ 94 | public function createIdentity($key,$suk,$vuk); 95 | 96 | /** 97 | * Flags a session as no longer valid. 98 | * 99 | * This should either immediatly destroy the session, or mark the session 100 | * in such a way that it will be destroyed the next time it is accessed. 101 | * 102 | * @param string $requestNut The nut of the curret request related to the session 103 | * to be destroyed 104 | * 105 | * @return void 106 | */ 107 | public function endSession($requestNut); 108 | 109 | /** 110 | * Locks an authentication key against further use until a successful unlock 111 | * 112 | * @param string $key The authentication key to lock 113 | * 114 | * @return void 115 | */ 116 | public function lockIdentityKey($key); 117 | 118 | /** 119 | * Unlocks an authentication key allowing future authentication 120 | * 121 | * @param string $key The authentication key to lock 122 | * 123 | * @return void 124 | */ 125 | public function unlockIdentityKey($key); 126 | 127 | /** 128 | * Gets an identity's SUK value in order for the client to use the Identity Unlock protocol 129 | * 130 | * @param string $key The identity key 131 | * 132 | * @return string The SUK value 133 | */ 134 | public function getIdentitySUK($key); 135 | 136 | /** 137 | * Gets an identity's VUK value in order for the client to use the Identity Unlock protocol 138 | * 139 | * @param string $key The identity key 140 | * 141 | * @return string The VUK value 142 | */ 143 | public function getIdentityVUK($key); 144 | 145 | /** 146 | * Updates a user's key information after an identity update action 147 | * 148 | * @param string $oldKey The key getting new information 149 | * @param string $newKey The authentication key replacing the old key 150 | * @param string $newSuk The replacement SUK 151 | * @param string $newVuk The replacement VUK 152 | * 153 | * @return void 154 | */ 155 | public function updateIdentityKey($oldKey, $newKey, $newSuk, $newVuk); 156 | 157 | /** 158 | * Gets the current active nonce for the user's session if there is any 159 | * 160 | * @return string 161 | */ 162 | public function getSessionNonce(); 163 | } 164 | -------------------------------------------------------------------------------- /examples/server/includes/ExampleStatefulStorage.php: -------------------------------------------------------------------------------- 1 | session = &$session; 48 | $this->conn = $conn; 49 | $this->reqIp = $reqIp; 50 | } 51 | 52 | public function checkIdentityKey($key) 53 | { 54 | $sql = "SELECT disabled FROM sqrl_pubkey WHERE public_key = ?"; 55 | $stmt = $this->conn->prepare($sql); 56 | $stmt->execute(array($key)); 57 | $result = $stmt->fetchColumn(); 58 | if ($result === false) { 59 | return self::IDENTITY_UNKNOWN; 60 | } else { 61 | return $result === '1'?self::IDENTITY_LOCKED:self::IDENTITY_ACTIVE; 62 | } 63 | } 64 | 65 | public function createIdentity($key, $suk, $vuk) 66 | { 67 | $sql = "INSERT INTO sqrl_pubkey (public_key,suk,vuk) VALUES (?,?,?)"; 68 | $stmt = $this->conn->prepare($sql); 69 | $stmt->execute(array($key,$suk,$vuk)); 70 | } 71 | 72 | public function endSession($requestNut) 73 | { 74 | $sql = 'UPDATE sqrl_nonce SET kill_session = 1 WHERE nonce = ?'; 75 | $stmt = $this->conn->prepare($sql); 76 | $stmt->execute(array($requestNut)); 77 | } 78 | 79 | public function getIdentitySUK($key) 80 | { 81 | $sql = 'SELECT suk FROM sqrl_pubkey WHERE public_key = ?'; 82 | $stmt = $this->conn->prepare($sql); 83 | $stmt->execute(array($key)); 84 | return $stmt->fetchColumn(); 85 | } 86 | 87 | public function getIdentityVUK($key) 88 | { 89 | $sql = 'SELECT vuk FROM sqrl_pubkey WHERE public_key = ?'; 90 | $stmt = $this->conn->prepare($sql); 91 | $stmt->execute(array($key)); 92 | return $stmt->fetchColumn(); 93 | } 94 | 95 | public function getNutDetails($nut) 96 | { 97 | $sql = 'SELECT action,related_public_key,orig_nonce,created,ip ' 98 | . 'FROM sqrl_nonce WHERE nonce = ?'; 99 | $stmt = $this->conn->prepare($sql); 100 | $stmt->execute(array($nut)); 101 | $result = $stmt->fetch(\PDO::FETCH_ASSOC); 102 | if (empty($result)) { 103 | return null; 104 | } else { 105 | return array ( 106 | 'tif'=> $result['action'], 107 | 'originalKey'=> $result['related_public_key'], 108 | 'originalNut'=> $result['orig_nonce'], 109 | 'createdDate'=> new \DateTime($result['created']), 110 | 'nutIP'=> long2ip($result['ip']) 111 | ); 112 | } 113 | } 114 | 115 | public function getSessionNonce() 116 | { 117 | return isset($this->session['sqrl_nut'])?$this->session['sqrl_nut']:''; 118 | } 119 | 120 | public function lockIdentityKey($key) 121 | { 122 | $sql = 'UPDATE sqrl_pubkey SET disabled = 1 WHERE public_key = ?'; 123 | $stmt = $this->conn->prepare($sql); 124 | $stmt->execute(array($key)); 125 | } 126 | 127 | public function logSessionIn($requestNut) 128 | { 129 | $sql = 'UPDATE sqrl_nonce SET verified = 1 WHERE nonce = ? OR orig_nonce = ?'; 130 | $stmt = $this->conn->prepare($sql); 131 | $stmt->execute(array($requestNut,$requestNut)); 132 | } 133 | 134 | public function storeNonce($nonce, $action, $key = '', $previousNonce = '') 135 | { 136 | $sql = 'INSERT INTO sqrl_nonce (nonce,ip,action,related_public_key) ' 137 | . 'VALUES (?,?,?,?)'; 138 | $longIp = ip2long($this->reqIp); 139 | $stmt = $this->conn->prepare($sql); 140 | $stmt->execute(array($nonce,$longIp,$action,$key)); 141 | if (empty($previousNonce)) { 142 | if (empty($this->session['sqrl_nut'])) { 143 | $this->session['sqrl_nut'] = $nonce; 144 | } 145 | } 146 | } 147 | 148 | public function unlockIdentityKey($key) 149 | { 150 | $sql = 'UPDATE sqrl_pubkey SET disabled = 0 WHERE public_key = ?'; 151 | $stmt = $this->conn->prepare($sql); 152 | $stmt->execute(array($key)); 153 | } 154 | 155 | public function updateIdentityKey($oldKey, $newKey, $newSuk, $newVuk) 156 | { 157 | $sql = 'UPDATE sqrl_pubkey SET public_key = ?, vuk = ?, suk = ? WHERE public_key = ?'; 158 | $stmt = $this->conn->prepare($sql); 159 | $stmt->execute(array($newKey, $newSuk, $newVuk, $oldKey)); 160 | } 161 | } 162 | 163 | /** 164 | * Database schema for this file: 165 | * 166 | * CREATE TABLE `sqrl_nonce` ( 167 | * `id` INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY, 168 | * `nonce` CHAR(64) NOT NULL, 169 | * `created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, 170 | * `ip` INT UNSIGNED NOT NULL, 171 | * `action` INT UNSIGNED NOT NULL, 172 | * `related_public_key` CHAR(44) DEFAULT NULL, 173 | * `verified` TINYINT NOT NULL DEFAULT 0, 174 | * `kill_session` TINYINT NOT NULL DEFAULT 0, 175 | * `orig_nonce` CHAR(64) DEFAULT NULL, 176 | * UNIQUE (`nonce`) 177 | * ); 178 | * 179 | * CREATE TABLE `sqrl_pubkey` ( 180 | * `id` INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY, 181 | * `public_key` CHAR(44) NOT NULL, 182 | * `vuk` CHAR(44) NOT NULL, 183 | * `suk` CHAR(44) NOT NULL, 184 | * `disabled` TINYINT NOT NULL DEFAULT 0, 185 | * UNIQUE (`public_key`) 186 | * ); 187 | */ -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/Tests/SqrlStoreStatelessAbstractTest.php: -------------------------------------------------------------------------------- 1 | testStub = $this->getMockBuilder('\Trianglman\Sqrl\SqrlStoreStatelessAbstract') 47 | ->disableOriginalConstructor() 48 | ->disableArgumentCloning() 49 | ->getMockForAbstractClass(); 50 | $this->testStub->expects($this->any())->method('getCurrentSessionId') 51 | ->will($this->returnValue('currentSessionID of long length')); 52 | $this->testStub->expects($this->any())->method('getIP') 53 | ->will($this->returnValue('192.168.0.105')); 54 | $this->testStub->setNonceSalt('123456789'); 55 | } 56 | 57 | public function testGeneratesAndReadsNewNut() 58 | { 59 | $sessionData = array(); 60 | $this->testStub->expects($this->exactly(3))->method('getSessionInfo') 61 | ->with($this->equalTo('currentSessionID of long length')) 62 | ->will($this->returnCallback(function() use (&$sessionData) { 63 | return $sessionData; 64 | })); 65 | $this->testStub->expects($this->once())->method('setSessionValue') 66 | ->with( 67 | $this->equalTo('currentSessionID of long length'), 68 | $this->equalTo('sqrl_nuts'), 69 | $this->anything() 70 | ); 71 | $nut = $this->testStub->generateNut(); 72 | 73 | $sessionData['sqrl_nuts']=$nut; 74 | 75 | $info = $this->testStub->getNutDetails($nut); 76 | 77 | $this->assertTrue(is_array($info)); 78 | $this->assertEquals(0,$info['tif']); 79 | $this->assertEquals('',$info['originalKey']); 80 | $this->assertEquals($nut,$info['originalNut']); 81 | $this->assertInstanceOf('\DateTime',$info['createdDate']); 82 | $this->assertEquals('192.168.0.105',$info['nutIP']); 83 | $this->assertEquals('currentSessionID of long length',$info['sessionId']); 84 | return $nut; 85 | } 86 | 87 | /** 88 | * 89 | * @depends testGeneratesAndReadsNewNut 90 | */ 91 | public function testLogsSessionIn($nut) 92 | { 93 | $this->testStub->expects($this->once())->method('getSessionInfo') 94 | ->with($this->equalTo('currentSessionID of long length')) 95 | ->will($this->returnValue(array('sqrl_nuts'=>$nut))); 96 | $this->testStub->expects($this->once())->method('setSessionValue') 97 | ->with( 98 | $this->equalTo('currentSessionID of long length'), 99 | $this->equalTo('sqrl_authenticated'), 100 | $this->equalTo('1') 101 | ); 102 | $this->testStub->logSessionIn($nut); 103 | } 104 | 105 | /** 106 | * 107 | * @depends testGeneratesAndReadsNewNut 108 | */ 109 | public function testGeneratesAndReadsSecondLoopNut($nut) 110 | { 111 | $sessionData = array('sqrl_nuts'=>$nut); 112 | $this->testStub->expects($this->any())->method('getSessionInfo') 113 | ->with($this->equalTo('currentSessionID of long length')) 114 | ->will($this->returnCallback(function() use (&$sessionData) { 115 | return $sessionData; 116 | })); 117 | $this->testStub->expects($this->exactly(2))->method('setSessionValue') 118 | ->with( 119 | $this->equalTo('currentSessionID of long length'), 120 | $this->anything(), 121 | $this->anything() 122 | ) 123 | ->will($this->returnCallback(function($sesId, $sessKey, $value) use ($nut) { 124 | if ($sessKey === 'sqrl_nuts') { 125 | $this->assertTrue(strpos($value, $nut.';')===0); 126 | } elseif ($sessKey === 'sqrl_key') { 127 | $this->assertEquals('some valid key',$value); 128 | } else { 129 | $this->assertFalse(true,$sessKey.' not an expected key.'); 130 | } 131 | })); 132 | $newNut = $this->testStub->generateNut(0x5,'some valid key',$nut); 133 | 134 | $sessionData['sqrl_nuts']=$nut.';'.$newNut; 135 | $sessionData['sqrl_key']='some valid key'; 136 | 137 | $info = $this->testStub->getNutDetails($newNut); 138 | 139 | $this->assertTrue(is_array($info)); 140 | $this->assertEquals(0x5,$info['tif']); 141 | $this->assertEquals('some valid key',$info['originalKey']); 142 | $this->assertEquals($nut,$info['originalNut']); 143 | $this->assertInstanceOf('\DateTime',$info['createdDate']); 144 | $this->assertEquals('192.168.0.105',$info['nutIP']); 145 | $this->assertEquals('currentSessionID of long length',$info['sessionId']); 146 | return $newNut; 147 | } 148 | 149 | /** 150 | * 151 | * @depends testGeneratesAndReadsNewNut 152 | */ 153 | public function testGeneratesAndReadsNewNutDoesntCreateNewIfOneExists($nut) 154 | { 155 | $this->testStub->expects($this->any())->method('getSessionInfo') 156 | ->with($this->equalTo('currentSessionID of long length')) 157 | ->will($this->returnValue(array('sqrl_nuts'=>$nut))); 158 | $this->assertEquals($nut, $this->testStub->generateNut()); 159 | } 160 | 161 | public function testRejectsBadNut() 162 | { 163 | $this->testStub->expects($this->never())->method('getSessionInfo'); 164 | $this->assertNull($this->testStub->getNutDetails('gibberishnonsense')); 165 | } 166 | 167 | /** 168 | * 169 | * @depends testGeneratesAndReadsNewNut 170 | */ 171 | public function testRejectsUnknownNut($nut) 172 | { 173 | $this->testStub->expects($this->any())->method('getSessionInfo') 174 | ->with($this->equalTo('currentSessionID of long length')) 175 | ->will($this->returnValue(null)); 176 | $this->assertNull($this->testStub->getNutDetails($nut)); 177 | } 178 | /** 179 | * 180 | * @depends testGeneratesAndReadsNewNut 181 | */ 182 | public function testRejectsNutNotFromSession($nut) 183 | { 184 | $this->testStub->expects($this->any())->method('getSessionInfo') 185 | ->with($this->equalTo('currentSessionID of long length')) 186 | ->will($this->returnValue(array('sqrl_nuts'=>'someothernut'))); 187 | $this->assertNull($this->testStub->getNutDetails($nut)); 188 | } 189 | 190 | /** 191 | * 192 | * @depends testGeneratesAndReadsNewNut 193 | */ 194 | public function testRejectsNutNotNewestInSession($nut) 195 | { 196 | $this->testStub->expects($this->any())->method('getSessionInfo') 197 | ->with($this->equalTo('currentSessionID of long length')) 198 | ->will($this->returnValue(array('sqrl_nuts'=>$nut.';someothernut'))); 199 | $this->assertNull($this->testStub->getNutDetails($nut)); 200 | } 201 | /** 202 | * 203 | * @depends testGeneratesAndReadsNewNut 204 | * @expectedException \InvalidArgumentException 205 | * @expectedExceptionMessage Old nut was not found. 206 | */ 207 | public function testGeneratesAndReadsSecondLoopNutBadOldNut($nut) 208 | { 209 | $sessionData = array('sqrl_nuts'=>'someothernut'); 210 | $this->testStub->expects($this->any())->method('getSessionInfo') 211 | ->with($this->equalTo('currentSessionID of long length')) 212 | ->will($this->returnCallback(function() use (&$sessionData) { 213 | return $sessionData; 214 | })); 215 | $this->testStub->expects($this->never())->method('setSessionValue'); 216 | $this->testStub->generateNut(0x5,'some valid key',$nut); 217 | } 218 | 219 | } 220 | -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/SqrlStoreStatelessAbstract.php: -------------------------------------------------------------------------------- 1 | nonceSalt = $salt; 57 | } 58 | 59 | /** 60 | * Creates a nut from the supplied data 61 | * 62 | * Takes the nut data, compacts it into a usable format with date, 63 | * session and random info data, and encrypts it into a usable nut 64 | * 65 | * @param int $tif The action associated with the nut 66 | * @param string $key The authentication key the nut is for 67 | * @param string $oldnut The previous nut in this transacion. 68 | * Information from this nut will be used to help store the new nut in 69 | * the right session. 70 | * 71 | * @return string 72 | * 73 | * @throws \InvalidArgumentException 74 | */ 75 | public function generateNut($tif=0, $key='', $oldnut='') 76 | { 77 | $created = dechex(time());//8 char 78 | $rnd = openssl_random_pseudo_bytes(4);//4 char 79 | $ip = dechex(ip2long($this->getIp()));//8 char 80 | $check = 'sqrl';//to make sure it decrypted 81 | if (!empty($oldnut)) { 82 | $nutInfo = $this->getNutDetails($oldnut); 83 | if (is_array($nutInfo)) { 84 | $sessionId = $nutInfo['sessionId'];//128 char max 85 | } else { 86 | throw new \InvalidArgumentException('Old nut was not found.'); 87 | } 88 | } else { 89 | $sessionId = $this->getCurrentSessionId();//128 char max 90 | $sessionData = $this->getSessionInfo($sessionId); 91 | if (is_array($sessionData) && isset($sessionData['sqrl_nuts'])) { 92 | $currentNuts = explode(';',$sessionData['sqrl_nuts']); 93 | return $currentNuts[0]; 94 | } 95 | } 96 | $nut = $rnd.$created.$ip.str_pad(dechex($tif), 2, '0', STR_PAD_LEFT).$sessionId.$check;//154 characters 97 | 98 | $encNut = $this->base64UrlEncode(openssl_encrypt($nut, 'aes128', $this->nonceSalt,0,$this->iv)); 99 | 100 | $this->addSessionNut($encNut,$sessionId); 101 | if (!empty($key) && isset($sessionId)) { 102 | $this->setSessionValue($sessionId,'sqrl_key', $key); 103 | } 104 | 105 | return $encNut; 106 | } 107 | 108 | /** 109 | * Retrieves information about the supplied nut 110 | * 111 | * @param string $nut The nonce to retrieve information on 112 | * 113 | * @return array: 114 | * 'tif'=> int The tif stored with the nut (0 for first request nuts) 115 | * 'originalKey'=> string The key associated with the nut, if any 116 | * 'originalNut'=> string The nut that came before this one in the transaction, if any 117 | * 'createdDate'=> \DateTime The time the nut was created 118 | * 'nutIP'=> string the IP address that requested the nut 119 | * 'sessionId'=> string the session ID for the nut [this is only required in stateless nuts] 120 | */ 121 | public function getNutDetails($nut,$debug =false) 122 | { 123 | $decNut = openssl_decrypt($this->base64URLDecode($nut), 'aes128', $this->nonceSalt,0,$this->iv); 124 | if ('sqrl' !== substr($decNut, -4)) { 125 | return null;//this nut was not encrypted with our key 126 | } 127 | $timestamp = hexdec(substr($decNut, 4, 8)); 128 | $ip = long2ip(hexdec(substr($decNut, 12, 8))); 129 | $tif = hexdec(substr($decNut, 20, 2)); 130 | $sessionId = substr($decNut, 22,strlen($decNut)-26); 131 | 132 | $sessionInfo = $this->getSessionInfo($sessionId); 133 | if (!is_array($sessionInfo)) { 134 | return null;//there's not a session that matches this nut, either it was lost or never existed 135 | } 136 | $currentNuts = isset($sessionInfo['sqrl_nuts'])?explode(';',$sessionInfo['sqrl_nuts']):array(); 137 | if (!in_array($nut,$currentNuts)) { 138 | return null;//this session never had this nut, somehow... 139 | } 140 | if ($currentNuts[count($currentNuts)-1]!==$nut) { 141 | return null;//someone is trying to resign an older nut 142 | } 143 | 144 | return array( 145 | 'tif'=> $tif, 146 | 'originalKey'=> isset($sessionInfo['sqrl_key'])?$sessionInfo['sqrl_key']:'', 147 | 'originalNut'=> $currentNuts[0], 148 | 'createdDate'=> new \DateTime('@'.$timestamp), 149 | 'nutIP'=> $ip, 150 | 'sessionId'=> $sessionId 151 | ); 152 | } 153 | 154 | /** 155 | * Adds a nut to the user's current session 156 | * 157 | * @param string $newNut 158 | * 159 | * @return void 160 | */ 161 | protected function addSessionNut($newNut,$sessionId) 162 | { 163 | $sessionInfo = $this->getSessionInfo($sessionId); 164 | $currentNuts = isset($sessionInfo['sqrl_nuts'])?explode(';',$sessionInfo['sqrl_nuts']):array(); 165 | $currentNuts[] = $newNut; 166 | $this->setSessionValue($sessionId,'sqrl_nuts', implode(';',$currentNuts)); 167 | } 168 | 169 | public function logSessionIn($requestNut) 170 | { 171 | $nutInfo = $this->getNutDetails($requestNut); 172 | if (is_array($nutInfo)) { 173 | $this->setSessionValue($nutInfo['sessionId'],'sqrl_authenticated', '1'); 174 | } 175 | } 176 | 177 | 178 | /** 179 | * Gets the session information that matches the supplied session ID 180 | * 181 | * @param string $sessionId 182 | * 183 | * @return array 184 | */ 185 | protected abstract function getSessionInfo($sessionId); 186 | 187 | /** 188 | * Gets the user's current session ID 189 | * 190 | * @return string 191 | */ 192 | protected abstract function getCurrentSessionId(); 193 | 194 | /** 195 | * Gets the user's IP address 196 | * 197 | * @return string 198 | */ 199 | protected abstract function getIp(); 200 | 201 | /** 202 | * Sets or updates a value in the user session 203 | * 204 | * @param string $sessionId 205 | * @param string $key 206 | * @param string $value 207 | * 208 | * @return void 209 | */ 210 | protected abstract function setSessionValue($sessionId,$key,$value); 211 | 212 | /** 213 | * Base 64 URL encodes a string 214 | * 215 | * Basically the same as base64 encoding, but replacing "+" with "-" and 216 | * "/" with "_" to make it safe to include in a URL 217 | * 218 | * Optionally removes trailing "=" padding characters. 219 | * 220 | * @param string $string The string to encode 221 | * @param bool $stripEquals [Optional] Whether to strip the "=" off of the end 222 | * 223 | * @return string 224 | */ 225 | protected function base64UrlEncode($string, $stripEquals=true) 226 | { 227 | $base64 = base64_encode($string); 228 | $urlencode = str_replace(array('+','/'), array('-','_'), $base64); 229 | if($stripEquals){ 230 | $urlencode = trim($urlencode, '='); 231 | } 232 | return $urlencode; 233 | } 234 | 235 | /** 236 | * Base 64 URL decodes a string 237 | * 238 | * Basically the same as base64 decoding, but replacing URL safe "-" with "+" 239 | * and "_" with "/". Automatically detects if the trailing "=" padding has 240 | * been removed. 241 | * 242 | * @param string $string 243 | * @return string 244 | */ 245 | protected function base64URLDecode($string) 246 | { 247 | $len = strlen($string); 248 | if($len%4 > 0){ 249 | $string = str_pad($string, 4-($len%4), '='); 250 | } 251 | $base64 = str_replace(array('-','_'), array('+','/'), $string); 252 | return base64_decode($base64); 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/SqrlRequestHandlerInterface.php: -------------------------------------------------------------------------------- 1 | b = 256; 55 | $this->q = "57896044618658097711785492504343953926634992332820282019728792003956564819949"; //bcsub(bcpow(2, 255),19); 56 | $this->l = "7237005577332262213973186563042994240857116359379907606001950938285454250989"; //bcadd(bcpow(2,252),27742317777372353535851937790883648493); 57 | $this->d = "-4513249062541557337682894930092624173785641285191125241628941591882900924598840740"; //bcmul(-121665,$this->inv(121666)); 58 | $this->I = "19681161376707505956807079304988542015446066515923890162744021073123829784752"; //$this->expmod(2, bcdiv((bcsub($this->q,1)),4),$this->q); 59 | $this->By = "46316835694926478169428394003475163141307993866256225615783033603165251855960"; //bcmul(4,$this->inv(5)); 60 | $this->Bx = "15112221349535400772501151409588531511454012693041857206046113283949847762202"; //$this->xrecover($this->By); 61 | $this->B = array( 62 | "15112221349535400772501151409588531511454012693041857206046113283949847762202", 63 | "46316835694926478169428394003475163141307993866256225615783033603165251855960" 64 | ); //array(bcmod($this->Bx,$this->q),bcmod($this->By,$this->q)); 65 | } 66 | 67 | protected function H($m) 68 | { 69 | return hash('sha512', $m, true); 70 | } 71 | 72 | //((n % M) + M) % M //python modulus craziness 73 | protected function pymod($x, $m) 74 | { 75 | $mod = bcmod($x, $m); 76 | if ($mod[0] === '-') { 77 | $mod = bcadd($mod, $m); 78 | } 79 | 80 | return $mod; 81 | } 82 | 83 | protected function expmod($b, $e, $m) 84 | { 85 | //if($e==0){return 1;} 86 | $t = bcpowmod($b, $e, $m); 87 | if ($t[0] === '-') { 88 | $t = bcadd($t, $m); 89 | } 90 | 91 | return $t; 92 | } 93 | 94 | protected function inv($x) 95 | { 96 | return $this->expmod($x, bcsub($this->q, 2), $this->q); 97 | } 98 | 99 | protected function xrecover($y) 100 | { 101 | $y2 = bcpow($y, 2); 102 | $xx = bcmul(bcsub($y2, 1), $this->inv(bcadd(bcmul($this->d, $y2), 1))); 103 | $x = $this->expmod($xx, bcdiv(bcadd($this->q, 3), 8, 0), $this->q); 104 | if ($this->pymod(bcsub(bcpow($x, 2), $xx), $this->q) != 0) { 105 | $x = $this->pymod(bcmul($x, $this->I), $this->q); 106 | } 107 | if (substr($x, -1)%2 != 0) { 108 | $x = bcsub($this->q, $x); 109 | } 110 | 111 | return $x; 112 | } 113 | 114 | protected function edwards($P, $Q) 115 | { 116 | list($x1, $y1) = $P; 117 | list($x2, $y2) = $Q; 118 | $xmul = bcmul($x1, $x2); 119 | $ymul = bcmul($y1, $y2); 120 | $com = bcmul($this->d, bcmul($xmul, $ymul)); 121 | $x3 = bcmul(bcadd(bcmul($x1, $y2), bcmul($x2, $y1)), $this->inv(bcadd(1, $com))); 122 | $y3 = bcmul(bcadd($ymul, $xmul), $this->inv(bcsub(1, $com))); 123 | 124 | return array($this->pymod($x3, $this->q), $this->pymod($y3, $this->q)); 125 | } 126 | 127 | protected function scalarmult($P, $e) 128 | { 129 | if ($e == 0) { 130 | return array(0, 1); 131 | } 132 | $Q = $this->scalarmult($P, bcdiv($e, 2, 0)); 133 | $Q = $this->edwards($Q, $Q); 134 | if (substr($e, -1)%2 == 1) { 135 | $Q = $this->edwards($Q, $P); 136 | } 137 | 138 | return $Q; 139 | } 140 | 141 | protected function scalarloop($P, $e) 142 | { 143 | $temp = array(); 144 | $loopE = $e; 145 | while ($loopE > 0) { 146 | array_unshift($temp, $loopE); 147 | $loopE = bcdiv($loopE, 2, 0); 148 | } 149 | $Q = array(); 150 | foreach ($temp as $e) { 151 | if ($e == 1) { 152 | $Q = $this->edwards(array(0, 1), $P); 153 | } elseif (substr($e, -1)%2 == 1) { 154 | $Q = $this->edwards($this->edwards($Q, $Q), $P); 155 | } else { 156 | $Q = $this->edwards($Q, $Q); 157 | } 158 | } 159 | 160 | return $Q; 161 | } 162 | 163 | protected function bitsToString($bits) 164 | { 165 | $string = ''; 166 | for ($i = 0; $i < $this->b/8; $i++) { 167 | $sum = 0; 168 | for ($j = 0; $j < 8; $j++) { 169 | $bit = $bits[$i*8+$j]; 170 | $sum += (int) $bit << $j; 171 | } 172 | $string .= chr($sum); 173 | } 174 | 175 | return $string; 176 | } 177 | 178 | protected function dec2bin_i($decimal_i) 179 | { 180 | $binary_i = ''; 181 | do { 182 | $binary_i = substr($decimal_i, -1)%2 .$binary_i; 183 | $decimal_i = bcdiv($decimal_i, '2', 0); 184 | } while (bccomp($decimal_i, '0')); 185 | 186 | return ($binary_i); 187 | } 188 | 189 | protected function encodeint($y) 190 | { 191 | $bits = substr(str_pad(strrev($this->dec2bin_i($y)), $this->b, '0', STR_PAD_RIGHT), 0, $this->b); 192 | 193 | return $this->bitsToString($bits); 194 | } 195 | 196 | protected function encodepoint($P) 197 | { 198 | list($x, $y) = $P; 199 | $bits = substr(str_pad(strrev($this->dec2bin_i($y)), $this->b-1, '0', STR_PAD_RIGHT), 0, $this->b-1); 200 | $bits .= (substr($x, -1)%2 == 1 ? '1' : '0'); 201 | 202 | return $this->bitsToString($bits); 203 | } 204 | 205 | protected function bit($h, $i) 206 | { 207 | return (ord($h[(int) bcdiv($i, 8, 0)]) >> substr($i, -3)%8) & 1; 208 | } 209 | 210 | /** 211 | * Generates the public key of a given private key 212 | * 213 | * @param string $sk the secret key 214 | * 215 | * @return string 216 | */ 217 | public function publickey($sk) 218 | { 219 | $h = $this->H($sk); 220 | $sum = 0; 221 | for ($i = 3; $i < $this->b-2; $i++) { 222 | $sum = bcadd($sum, bcmul(bcpow(2, $i), $this->bit($h, $i))); 223 | } 224 | $a = bcadd(bcpow(2, $this->b-2), $sum); 225 | $A = $this->scalarmult($this->B, $a); 226 | $data = $this->encodepoint($A); 227 | 228 | return $data; 229 | } 230 | 231 | protected function Hint($m) 232 | { 233 | $h = $this->H($m); 234 | $sum = 0; 235 | for ($i = 0; $i < $this->b*2; $i++) { 236 | $sum = bcadd($sum, bcmul(bcpow(2, $i), $this->bit($h, $i))); 237 | } 238 | 239 | return $sum; 240 | } 241 | 242 | public function signature($m, $sk, $pk) 243 | { 244 | $h = $this->H($sk); 245 | $a = bcpow(2, (bcsub($this->b, 2))); 246 | for ($i = 3; $i < $this->b-2; $i++) { 247 | $a = bcadd($a, bcmul(bcpow(2, $i), $this->bit($h, $i))); 248 | } 249 | $r = $this->Hint(substr($h, $this->b/8, ($this->b/4-$this->b/8)).$m); 250 | $R = $this->scalarmult($this->B, $r); 251 | $encR = $this->encodepoint($R); 252 | $S = $this->pymod(bcadd($r, bcmul($this->Hint($encR.$pk.$m), $a)), $this->l); 253 | 254 | return $encR.$this->encodeint($S); 255 | } 256 | 257 | protected function isoncurve($P) 258 | { 259 | list($x, $y) = $P; 260 | $x2 = bcpow($x, 2); 261 | $y2 = bcpow($y, 2); 262 | 263 | return $this->pymod(bcsub(bcsub(bcsub($y2, $x2), 1), bcmul($this->d, bcmul($x2, $y2))), $this->q) == 0; 264 | } 265 | 266 | protected function decodeint($s) 267 | { 268 | $sum = 0; 269 | for ($i = 0; $i < $this->b; $i++) { 270 | $sum = bcadd($sum, bcmul(bcpow(2, $i), $this->bit($s, $i))); 271 | } 272 | 273 | return $sum; 274 | } 275 | 276 | /* 277 | * def decodepoint(s): 278 | y = sum(2**i * bit(s,i) for i in range(0,b-1)) 279 | x = xrecover(y) 280 | if x & 1 != bit(s,b-1): x = q-x 281 | P = [x,y] 282 | if not isoncurve(P): raise Exception("decoding point that is not on curve") 283 | return P 284 | 285 | */ 286 | protected function decodepoint($s) 287 | { 288 | $y = 0; 289 | for ($i = 0; $i < $this->b-1; $i++) { 290 | $y = bcadd($y, bcmul(bcpow(2, $i), $this->bit($s, $i))); 291 | } 292 | $x = $this->xrecover($y); 293 | if (substr($x, -1)%2 != $this->bit($s, $this->b-1)) { 294 | $x = bcsub($this->q, $x); 295 | } 296 | $P = array($x, $y); 297 | if (!$this->isoncurve($P)) { 298 | throw new \Exception("Decoding point that is not on curve"); 299 | } 300 | 301 | return $P; 302 | } 303 | 304 | public function checkvalid($s, $m, $pk) 305 | { 306 | if (strlen($s) != $this->b/4) { 307 | throw new \Exception('Signature length is wrong'); 308 | } 309 | if (strlen($pk) != $this->b/8) { 310 | throw new \Exception('Public key length is wrong: '.strlen($pk)); 311 | } 312 | $R = $this->decodepoint(substr($s, 0, $this->b/8)); 313 | try { 314 | $A = $this->decodepoint($pk); 315 | } catch (\Exception $e) { 316 | return false; 317 | } 318 | $S = $this->decodeint(substr($s, $this->b/8, $this->b/4)); 319 | $h = $this->Hint($this->encodepoint($R).$pk.$m); 320 | 321 | return $this->scalarmult($this->B, $S) == $this->edwards($R, $this->scalarmult($A, $h)); 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/SqrlConfiguration.php: -------------------------------------------------------------------------------- 1 | loadConfigFromJSON($filePath); 120 | } catch (\Exception $ex) { 121 | throw new \InvalidArgumentException('Configuration data could not be parsed.', 1, $ex); 122 | } 123 | } 124 | 125 | protected function loadConfigFromJSON(string $filePath): void 126 | { 127 | if (!file_exists($filePath)) { 128 | throw new \InvalidArgumentException('Configuration file not found'); 129 | } 130 | $data = file_get_contents($filePath); 131 | $decoded = json_decode($data); 132 | if (is_null($decoded)) { 133 | throw new \InvalidArgumentException('Configuration data could not be parsed. Is it JSON formatted?'); 134 | } 135 | if (is_array($decoded->accepted_versions)) { 136 | $this->setAcceptedVersions($decoded->accepted_versions); 137 | } 138 | $this->setSecure(!empty($decoded->secure) && (int)$decoded->secure > 0); 139 | $this->setDomain($decoded->key_domain ?? ''); 140 | $this->setAuthenticationPath($decoded->authentication_path ?? ''); 141 | $this->setAnonAllowed( 142 | !empty($decoded->allow_anonymous_accounts) && (int)$decoded->allow_anonymous_accounts > 0 143 | ); 144 | if (!empty($decoded->nonce_max_age)) { 145 | $this->setNonceMaxAge($decoded->nonce_max_age); 146 | } 147 | if (!empty($decoded->height)) { 148 | $this->setQrHeight($decoded->height); 149 | } 150 | if (!empty($decoded->padding)) { 151 | $this->setQrPadding($decoded->padding); 152 | } 153 | $this->setNonceSalt(!empty($decoded->nonce_salt)?$decoded->nonce_salt:''); 154 | } 155 | 156 | /** 157 | * Gets the versions this SQRL server supports 158 | * 159 | * @return array 160 | */ 161 | public function getAcceptedVersions(): array 162 | { 163 | return $this->acceptedVersions; 164 | } 165 | 166 | /** 167 | * Gets whether responses to the server should be secure 168 | * 169 | * @return boolean 170 | */ 171 | public function getSecure(): bool 172 | { 173 | return $this->secure; 174 | } 175 | 176 | /** 177 | * Gets the domain clients should generate a key for 178 | * 179 | * @return string 180 | */ 181 | public function getDomain(): string 182 | { 183 | return $this->domain; 184 | } 185 | 186 | /** 187 | * Gets the path to the authentication script 188 | * 189 | * @return string 190 | */ 191 | public function getAuthenticationPath(): string 192 | { 193 | return $this->authenticationPath; 194 | } 195 | 196 | /** 197 | * Gets whether users are allowed to generate anonymous accounts 198 | * 199 | * @return boolean 200 | */ 201 | public function getAnonAllowed(): bool 202 | { 203 | return $this->anonAllowed; 204 | } 205 | 206 | /** 207 | * Gets the time in minutes that a nonce is considered valid 208 | * 209 | * @return int 210 | */ 211 | public function getNonceMaxAge(): int 212 | { 213 | return $this->nonceMaxAge; 214 | } 215 | 216 | /** 217 | * Gets the height, in pixels, of a generated QR code 218 | * 219 | * @return int 220 | */ 221 | public function getQrHeight(): int 222 | { 223 | return $this->qrHeight; 224 | } 225 | 226 | /** 227 | * Gets the padding, in pixels, around a generated QR code 228 | * 229 | * @return int 230 | */ 231 | public function getQrPadding(): int 232 | { 233 | return $this->qrPadding; 234 | } 235 | 236 | /** 237 | * Gets the random string used to salt generated nonces 238 | * 239 | * @return string 240 | */ 241 | public function getNonceSalt(): string 242 | { 243 | return $this->nonceSalt; 244 | } 245 | 246 | /** 247 | * Sets the versions this SQRL server supports 248 | * 249 | * @param mixed $acceptedVersions 250 | * 251 | * @return SqrlConfiguration 252 | */ 253 | public function setAcceptedVersions($acceptedVersions): SqrlConfiguration 254 | { 255 | if (is_array($acceptedVersions)) { 256 | $this->acceptedVersions = $acceptedVersions; 257 | } else { 258 | $this->acceptedVersions = [$acceptedVersions]; 259 | } 260 | return $this; 261 | } 262 | 263 | /** 264 | * Sets whether responses to the server should be secure 265 | * 266 | * @param boolean $secure 267 | * 268 | * @return SqrlConfiguration 269 | */ 270 | public function setSecure(bool $secure): SqrlConfiguration 271 | { 272 | $this->secure = $secure; 273 | return $this; 274 | } 275 | 276 | /** 277 | * Sets the domain clients should generate a key for 278 | * 279 | * @param string $domain 280 | * 281 | * @return SqrlConfiguration 282 | */ 283 | public function setDomain(string $domain): SqrlConfiguration 284 | { 285 | $this->domain = $domain; 286 | return $this; 287 | } 288 | 289 | /** 290 | * Sets the path to the authentication script 291 | * 292 | * @param string $authenticationPath 293 | * 294 | * @return SqrlConfiguration 295 | */ 296 | public function setAuthenticationPath(string $authenticationPath): SqrlConfiguration 297 | { 298 | $this->authenticationPath = $authenticationPath; 299 | return $this; 300 | } 301 | 302 | /** 303 | * Sets whether users are allowed to generate anonymous accounts 304 | * 305 | * @param boolean $anonAllowed 306 | * 307 | * @return SqrlConfiguration 308 | */ 309 | public function setAnonAllowed(bool $anonAllowed): SqrlConfiguration 310 | { 311 | $this->anonAllowed = (bool)$anonAllowed; 312 | return $this; 313 | } 314 | 315 | /** 316 | * Sets the time in minutes that a nonce is considered valid 317 | * 318 | * @param int $nonceMaxAge 319 | * 320 | * @return SqrlConfiguration 321 | */ 322 | public function setNonceMaxAge(int $nonceMaxAge): SqrlConfiguration 323 | { 324 | $this->nonceMaxAge = $nonceMaxAge; 325 | return $this; 326 | } 327 | 328 | /** 329 | * Sets the height, in pixels, of a generated QR code 330 | * 331 | * @param int $qrHeight 332 | * 333 | * @return SqrlConfiguration 334 | */ 335 | public function setQrHeight(int $qrHeight): SqrlConfiguration 336 | { 337 | $this->qrHeight = $qrHeight; 338 | return $this; 339 | } 340 | 341 | /** 342 | * Sets the padding, in pixels, around a generated QR code 343 | * 344 | * @param int $qrPadding 345 | * 346 | * @return SqrlConfiguration 347 | */ 348 | public function setQrPadding(int $qrPadding): SqrlConfiguration 349 | { 350 | $this->qrPadding = $qrPadding; 351 | return $this; 352 | } 353 | 354 | /** 355 | * Sets the random string used to salt generated nonces 356 | * 357 | * @param string $nonceSalt 358 | * 359 | * @return SqrlConfiguration 360 | */ 361 | public function setNonceSalt(string $nonceSalt): SqrlConfiguration 362 | { 363 | $this->nonceSalt = $nonceSalt; 364 | return $this; 365 | } 366 | 367 | } -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/Tests/SqrlGenerateTest.php: -------------------------------------------------------------------------------- 1 | config = $this->getMockBuilder(SqrlConfiguration::class)->getMock(); 54 | $this->config->expects($this->any())->method('getNonceSalt') 55 | ->will($this->returnValue('randomsalt')); 56 | $this->storage = $this->getMockBuilder(SqrlStoreInterface::class)->getMock(); 57 | } 58 | 59 | /** 60 | * Tests the getNonce() function when called with no arguments 61 | * 62 | * This should check the storage for an existing nonce, find nothing, 63 | * generate a completely random nonce, and send it to the storage for stateful 64 | * saving 65 | */ 66 | public function testGeneratesStatefulNonceInitialRequest() 67 | { 68 | $this->storage->expects($this->once()) 69 | ->method('getSessionNonce') 70 | ->will($this->returnValue(null)); 71 | $this->storage->expects($this->once()) 72 | ->method('storeNonce') 73 | ->with($this->anything(),$this->equalTo(0),$this->equalTo(''),$this->equalTo('')) 74 | ->will($this->returnCallback(function($nut) { 75 | $this->assertRegExp('/[a-z0-9]{64}/',$nut,'Nut is not properly formatted'); 76 | })); 77 | $obj = new SqrlGenerate($this->config,$this->storage); 78 | $obj->getNonce(); 79 | } 80 | 81 | /** 82 | * Tests the getNonce() function when called with no arguments where storage is 83 | * semi-stateless 84 | * 85 | * This should check the storage for an existing nonce, find nothing, and 86 | * request a semi-stateless nut from storage 87 | */ 88 | public function testGeneratesStatelessNonceInitialRequest() 89 | { 90 | /** @var MockObject|SqrlStoreStatelessAbstract $storage */ 91 | $storage = $this->getMockBuilder(SqrlStoreStatelessAbstract::class) 92 | ->disableOriginalConstructor() 93 | ->setMethods(array('generateNut')) 94 | ->getMockForAbstractClass(); 95 | $storage->expects($this->once())->method('generateNut') 96 | ->with($this->equalTo(0),$this->equalTo(''),$this->equalTo('')) 97 | ->will($this->returnValue('semi-stateless nut')); 98 | 99 | $obj = new SqrlGenerate($this->config,$storage); 100 | 101 | $this->assertEquals('semi-stateless nut',$obj->getNonce()); 102 | } 103 | 104 | /** 105 | * Tests the getNonce() function when called with no arguments where storage 106 | * already holds an active nonce 107 | * 108 | * This should check the storage for an existing nonce and find it 109 | */ 110 | public function testReloadsActiveNonce() 111 | { 112 | $this->storage->expects($this->once()) 113 | ->method('getSessionNonce') 114 | ->will($this->returnValue('stored nut')); 115 | $this->storage->expects($this->never())->method('storeNonce'); 116 | $obj = new SqrlGenerate($this->config,$this->storage); 117 | $this->assertEquals('stored nut', $obj->getNonce()); 118 | } 119 | 120 | /** 121 | * Tests the getNonce() function when called with previous nut state arguments 122 | * 123 | * This should generate a completely random nonce and store it and the previous 124 | * state information 125 | * @depends testGeneratesStatefulNonceInitialRequest 126 | */ 127 | public function testGeneratesStatefulNonceSecondLoop() 128 | { 129 | $this->storage->expects($this->never()) 130 | ->method('getSessionNonce') 131 | ->will($this->returnValue(null)); 132 | $this->storage->expects($this->once()) 133 | ->method('storeNonce') 134 | ->with($this->anything(),$this->equalTo(5),$this->equalTo('validkey'),$this->equalTo('previousNut')) 135 | ->will($this->returnCallback(function($nut) { 136 | $this->assertRegExp('/[a-z0-9]{64}/',$nut,'Nut is not properly formatted'); 137 | })); 138 | $obj = new SqrlGenerate($this->config,$this->storage); 139 | $obj->getNonce(5,'validkey','previousNut'); 140 | } 141 | 142 | /** 143 | * Tests the getNonce() function when called with no arguments where storage is 144 | * semi-stateless when called with previous nut state arguments 145 | * 146 | * This should request a semi-stateless nut from storage including the previous 147 | * state information 148 | * 149 | * @depends testGeneratesStatelessNonceInitialRequest 150 | */ 151 | public function testGeneratesStatelessNonceSecondLoop() 152 | { 153 | /** @var MockObject|SqrlStoreStatelessAbstract $storage */ 154 | $storage = $this->getMockBuilder(SqrlStoreStatelessAbstract::class) 155 | ->disableOriginalConstructor() 156 | ->setMethods(array('generateNut')) 157 | ->getMockForAbstractClass(); 158 | $storage->expects($this->once())->method('generateNut') 159 | ->with($this->equalTo(5),$this->equalTo('validkey'),$this->equalTo('previousNut')) 160 | ->will($this->returnValue('semi-stateless nut')); 161 | 162 | $obj = new SqrlGenerate($this->config,$storage); 163 | $this->assertEquals('semi-stateless nut',$obj->getNonce(5,'validkey','previousNut')); 164 | } 165 | 166 | /** 167 | * @depends testReloadsActiveNonce 168 | */ 169 | public function testGeneratesQryNoQueryString() 170 | { 171 | $this->storage->expects($this->once()) 172 | ->method('getSessionNonce') 173 | ->will($this->returnValue('storednut')); 174 | $this->config->expects($this->any())->method('getAuthenticationPath') 175 | ->will($this->returnValue('/sqrl')); 176 | 177 | $obj = new SqrlGenerate($this->config,$this->storage); 178 | $this->assertEquals('/sqrl?nut=storednut', $obj->generateQry()); 179 | } 180 | 181 | /** 182 | * @depends testReloadsActiveNonce 183 | */ 184 | public function testGeneratesQryWithQueryString() 185 | { 186 | $this->storage->expects($this->once()) 187 | ->method('getSessionNonce') 188 | ->will($this->returnValue('storednut')); 189 | $this->config->expects($this->any())->method('getAuthenticationPath') 190 | ->will($this->returnValue('/sqrl?foo=bar')); 191 | 192 | $obj = new SqrlGenerate($this->config,$this->storage); 193 | $this->assertEquals('/sqrl?foo=bar&nut=storednut', $obj->generateQry()); 194 | } 195 | 196 | /** 197 | * @depends testGeneratesQryNoQueryString 198 | */ 199 | public function testGeneratesUrl() 200 | { 201 | $this->storage->expects($this->once()) 202 | ->method('getSessionNonce') 203 | ->will($this->returnValue('storednut')); 204 | $this->config->expects($this->any())->method('getDomain') 205 | ->will($this->returnValue('example.com')); 206 | $this->config->expects($this->any())->method('getAuthenticationPath') 207 | ->will($this->returnValue('/sqrl')); 208 | $this->config->expects($this->any())->method('getSecure') 209 | ->will($this->returnValue(true)); 210 | 211 | $obj = new SqrlGenerate($this->config,$this->storage); 212 | $this->assertEquals('sqrl://example.com/sqrl?nut=storednut', $obj->getUrl()); 213 | } 214 | 215 | /** 216 | * @depends testGeneratesQryNoQueryString 217 | */ 218 | public function testGeneratesUrlIncludingExtendedDomain() 219 | { 220 | $this->storage->expects($this->once()) 221 | ->method('getSessionNonce') 222 | ->will($this->returnValue('storednut')); 223 | $this->config->expects($this->any())->method('getDomain') 224 | ->will($this->returnValue('example.com/~user')); 225 | $this->config->expects($this->any())->method('getAuthenticationPath') 226 | ->will($this->returnValue('/~user/sqrl')); 227 | $this->config->expects($this->any())->method('getSecure') 228 | ->will($this->returnValue(true)); 229 | 230 | $obj = new SqrlGenerate($this->config,$this->storage); 231 | $this->assertEquals('sqrl://example.com/~user/sqrl?nut=storednut&x=6', $obj->getUrl()); 232 | } 233 | 234 | 235 | /** 236 | * @depends testGeneratesUrl 237 | * @throws \Endroid\QrCode\Exceptions\ImageFunctionUnknownException 238 | */ 239 | public function testRenders() 240 | { 241 | $this->storage->expects($this->once())->method('getSessionNonce') 242 | ->will($this->returnValue('storednut')); 243 | $this->config->expects($this->any())->method('getDomain') 244 | ->will($this->returnValue('example.com')); 245 | $this->config->expects($this->any())->method('getAuthenticationPath') 246 | ->will($this->returnValue('/sqrl')); 247 | $this->config->expects($this->any())->method('getSecure') 248 | ->will($this->returnValue(true)); 249 | $this->config->expects($this->any())->method('getQrHeight') 250 | ->will($this->returnValue(30)); 251 | $this->config->expects($this->any())->method('getQrPadding') 252 | ->will($this->returnValue(1)); 253 | 254 | $obj = new SQRLGenerate($this->config,$this->storage); 255 | $expected = new QrCode(); 256 | $expected->setText('sqrl://example.com/sqrl?nut=storednut'); 257 | $expected->setSize(30); 258 | $expected->setPadding(1); 259 | $expected->render(dirname(__FILE__).'/expected.png'); 260 | $obj->render(dirname(__FILE__).'/test.png'); 261 | $this->assertEquals( 262 | file_get_contents(dirname(__FILE__).'/expected.png'), 263 | file_get_contents(dirname(__FILE__).'/test.png') 264 | ); 265 | unlink(dirname(__FILE__).'/expected.png'); 266 | unlink(dirname(__FILE__).'/test.png'); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/SqrlRequestHandler.php: -------------------------------------------------------------------------------- 1 | config = $config; 77 | $this->validator = $val; 78 | $this->sqrlGenerator = $gen; 79 | $this->store = $store; 80 | } 81 | 82 | /** 83 | * Parses a user request 84 | * 85 | * This will determine what type of request is being performed and set values 86 | * up for use in validation and creating the response. 87 | * 88 | * @param array $get The user's GET request 89 | * @param array $post The user's POST body 90 | * @param array $server Server level variables (the _SERVER array) 91 | * 92 | * @throws \Exception 93 | * @throws SqrlException 94 | * 95 | * @return void 96 | */ 97 | public function parseRequest($get, $post, $server) 98 | { 99 | //check that all the right pieces exist 100 | if (isset($post['client']) && isset($post['server']) && isset($post['ids']) && isset($get['nut'])) { 101 | $serverInfo = $this->parseServer($post['server']); 102 | $clientInfo = $this->parseClient($post['client']); 103 | $this->requestNut = $get['nut']; 104 | if (empty($serverInfo) || empty($clientInfo) || !isset($clientInfo['ver'])) { 105 | $this->tif|= (self::COMMAND_FAILED|self::CLIENT_FAILURE); 106 | return; 107 | } 108 | if (!$this->validator->validateServer($serverInfo,$this->requestNut,isset($server['HTTPS'])?$server['HTTPS']:false)) { 109 | $this->tif|= (self::COMMAND_FAILED|self::CLIENT_FAILURE); 110 | return; 111 | } 112 | $nutStatus = $this->validator->validateNut($this->requestNut,isset($clientInfo['idk'])?$clientInfo['idk']:null); 113 | if ($nutStatus !== \Trianglman\Sqrl\SqrlValidateInterface::VALID_NUT) { 114 | if ($nutStatus === \Trianglman\Sqrl\SqrlValidateInterface::EXPIRED_NUT) { 115 | $this->authenticationKey = $clientInfo['idk']; 116 | $this->tif|= (self::COMMAND_FAILED|self::TRANSIENT_ERROR); 117 | } elseif ($nutStatus === SqrlValidateInterface::KEY_MISMATCH) { 118 | $this->tif|= (self::COMMAND_FAILED|self::CLIENT_FAILURE|self::BAD_ID_ASSOCIATION); 119 | } else { 120 | $this->tif|= (self::COMMAND_FAILED|self::CLIENT_FAILURE); 121 | } 122 | return; 123 | } else { 124 | $this->tif|= $this->validator->nutIPMatches($get['nut'],$server['REMOTE_ADDR'])?self::IP_MATCH:0; 125 | } 126 | if (!$this->validateSignatures($post, $clientInfo)) { 127 | $this->tif|= (self::COMMAND_FAILED|self::CLIENT_FAILURE); 128 | return; 129 | } 130 | $this->authenticationKey = $clientInfo['idk']; 131 | if (isset($clientInfo['vuk'])) { 132 | $this->clientSUK = isset($clientInfo['suk'])?$clientInfo['suk']:''; 133 | $this->clientVUK = $clientInfo['vuk']; 134 | } 135 | if (isset($clientInfo['pidk'])) { 136 | $this->previousIdKey = $clientInfo['pidk']; 137 | } 138 | $this->actions = $clientInfo['actions']; 139 | $this->clientOptions = isset($clientInfo['options'])?$clientInfo['options']:array(); 140 | return; 141 | } 142 | $this->tif = (self::COMMAND_FAILED|self::CLIENT_FAILURE); 143 | return; 144 | } 145 | 146 | private function validateSignatures($post,$clientInfo) 147 | { 148 | if (!$this->validator->validateSignature( 149 | $post['client'].$post['server'], 150 | $clientInfo['idk'], 151 | $this->base64URLDecode($post['ids']) 152 | )) { 153 | return false; 154 | } 155 | if (isset($post['urs']) && isset($clientInfo['vuk']) && !isset($clientInfo['pidk']) && !$this->validator->validateSignature( 156 | $post['client'].$post['server'], 157 | $clientInfo['vuk'], 158 | $this->base64URLDecode($post['urs']) 159 | )) { 160 | return false; 161 | } 162 | if (isset($post['urs']) && isset($clientInfo['vuk']) && isset($clientInfo['pidk']) && !$this->validator->validateSignature( 163 | $post['client'].$post['server'], 164 | $this->store->getIdentityVUK($clientInfo['pidk']), 165 | $this->base64URLDecode($post['urs']) 166 | )) { 167 | return false; 168 | } 169 | if (isset($post['pids']) && isset($clientInfo['pidk']) && !$this->validator->validateSignature( 170 | $post['client'].$post['server'], 171 | $clientInfo['pidk'], 172 | $this->base64URLDecode($post['pids']) 173 | )) { 174 | return false; 175 | } 176 | return true; 177 | } 178 | 179 | /** 180 | * Takes a (base64Url decoded) client value string and breaks it into its individual values 181 | * @param string $clientInput 182 | * @return void 183 | */ 184 | protected function parseClient($clientInput) 185 | { 186 | $inputAsArray = explode("\n", $this->base64URLDecode($clientInput)); 187 | $return = array(); 188 | foreach (array_filter($inputAsArray) as $individualInputs) { 189 | if (strpos($individualInputs, '=') === false) { 190 | continue; 191 | } 192 | list($key,$val) = explode("=", $individualInputs); 193 | $val = trim($val);//strip off the \r 194 | switch (trim($key)){ 195 | case 'ver': 196 | $return['ver']=$val; 197 | break; 198 | case 'cmd': 199 | $return['actions'] = explode('~',$val); 200 | break; 201 | case 'idk': 202 | $return['idk']=$this->base64URLDecode($val); 203 | break; 204 | case 'pidk': 205 | $return['pidk']=$this->base64URLDecode($val); 206 | break; 207 | case 'vuk': 208 | $return['vuk']=$this->base64URLDecode($val); 209 | break; 210 | case 'suk': 211 | $return['suk']=$this->base64URLDecode($val); 212 | break; 213 | case 'opt': 214 | $return['options'] = explode('~',$val); 215 | break; 216 | } 217 | } 218 | return $return; 219 | } 220 | 221 | protected function parseServer($serverData) 222 | { 223 | $decoded = $this->base64URLDecode($serverData); 224 | if (substr($decoded,0,7)==='sqrl://' || substr($decoded,0,6)==='qrl://'){ 225 | return $decoded; 226 | } else { 227 | $serverValues = explode("\r\n", $decoded); 228 | $parsedResult = array(); 229 | foreach ($serverValues as $value) { 230 | $splitStop = strpos($value, '='); 231 | $key = substr($value, 0, $splitStop); 232 | $val = substr($value, $splitStop+1); 233 | $parsedResult[$key]=$val; 234 | } 235 | return $parsedResult; 236 | } 237 | } 238 | 239 | /** 240 | * Gets the text message to be returned to the SQRL client 241 | * 242 | * @throws \Exception 243 | * @throws SqrlException 244 | * @return string 245 | */ 246 | public function getResponseMessage() 247 | { 248 | foreach ($this->actions as $action) { 249 | if ($this->tif&self::COMMAND_FAILED) { 250 | break; 251 | } 252 | $this->$action(); 253 | } 254 | return $this->formatResponse($this->tif); 255 | } 256 | 257 | protected function query() 258 | { 259 | $identityStatus = $this->store->checkIdentityKey($this->authenticationKey); 260 | if ($identityStatus === SqrlStoreInterface::IDENTITY_ACTIVE) { 261 | $this->tif|= self::ID_MATCH; 262 | } elseif (!empty($this->previousIdKey)) { 263 | if ($this->store->checkIdentityKey($this->previousIdKey) === SqrlStoreInterface::IDENTITY_ACTIVE) { 264 | $this->tif|= self::PREVIOUS_ID_MATCH; 265 | } 266 | } elseif ($identityStatus === SqrlStoreInterface::IDENTITY_UNKNOWN) { 267 | if (!$this->config->getAnonAllowed()) {//notify the client that anonymous authentication is not allowed in this transaction 268 | $this->tif|= self::FUNCTION_NOT_SUPPORTED|self::COMMAND_FAILED; 269 | } 270 | } elseif ($identityStatus === SqrlStoreInterface::IDENTITY_LOCKED) { 271 | $this->tif|= self::ID_MATCH|self::SQRL_DISABLED; 272 | } 273 | } 274 | 275 | protected function ident() 276 | { 277 | $identityStatus = $this->store->checkIdentityKey($this->authenticationKey); 278 | if ($identityStatus === SqrlStoreInterface::IDENTITY_ACTIVE) { 279 | $this->store->logSessionIn($this->requestNut); 280 | $this->tif|= self::ID_MATCH; 281 | } elseif ($identityStatus === SqrlStoreInterface::IDENTITY_UNKNOWN) { 282 | $this->identUnknownIdentity(); 283 | } elseif ($identityStatus === SqrlStoreInterface::IDENTITY_LOCKED) { 284 | if (empty($this->clientSUK) || $this->clientVUK !== $this->store->getIdentityVUK($this->authenticationKey)) { 285 | $this->tif|= (self::CLIENT_FAILURE|self::COMMAND_FAILED); 286 | } else { 287 | $this->store->unlockIdentityKey($this->authenticationKey); 288 | $this->store->logSessionIn($this->requestNut); 289 | $this->tif|= self::ID_MATCH; 290 | } 291 | } 292 | } 293 | 294 | private function identUnknownIdentity() 295 | { 296 | if (!empty($this->previousIdKey) && 297 | $this->store->checkIdentityKey($this->previousIdKey) !== SqrlStoreInterface::IDENTITY_UNKNOWN) { 298 | if (empty($this->clientSUK) || empty($this->clientVUK)) { 299 | $this->tif|= (self::CLIENT_FAILURE|self::COMMAND_FAILED); 300 | } else { 301 | $this->store->updateIdentityKey($this->previousIdKey,$this->authenticationKey,$this->clientSUK,$this->clientVUK); 302 | $this->store->logSessionIn($this->requestNut); 303 | $this->tif|= self::ID_MATCH|self::PREVIOUS_ID_MATCH; 304 | } 305 | return; 306 | } 307 | if (!$this->config->getAnonAllowed()) {//notify the client that anonymous authentication is not allowed in this transaction 308 | $this->tif|= (self::FUNCTION_NOT_SUPPORTED|self::COMMAND_FAILED); 309 | } elseif (empty($this->clientSUK)) { 310 | $this->tif|= (self::CLIENT_FAILURE|self::COMMAND_FAILED); 311 | } else { 312 | $this->store->createIdentity($this->authenticationKey,$this->clientSUK,$this->clientVUK); 313 | $this->store->logSessionIn($this->requestNut); 314 | $this->tif|= self::ID_MATCH; 315 | } 316 | } 317 | 318 | protected function lock() 319 | { 320 | $identityStatus = $this->store->checkIdentityKey($this->authenticationKey); 321 | if ($identityStatus !== SqrlStoreInterface::IDENTITY_UNKNOWN) { 322 | $this->store->lockIdentityKey($this->authenticationKey); 323 | $this->store->endSession($this->requestNut); 324 | $this->tif|= (self::ID_MATCH|self::SQRL_DISABLED); 325 | } else { 326 | $this->tif|= (self::COMMAND_FAILED|self::CLIENT_FAILURE); 327 | } 328 | } 329 | 330 | /** 331 | * Gets the numeric HTTP code to return to the SQRL client 332 | * 333 | * Currently the spec only uses the 200 code and any error message is in the 334 | * test message response 335 | * 336 | * @return int 337 | */ 338 | public function getResponseCode() 339 | { 340 | return $this->responseCode; 341 | } 342 | 343 | /** 344 | * A helper function to send the response message and code to the SQRL client 345 | * 346 | * @return void 347 | */ 348 | public function sendResponse() 349 | { 350 | echo $this->getResponseMessage(); 351 | } 352 | 353 | /** 354 | * Formats a response to send back to a client 355 | * 356 | * @param int $code The TIF code to send back to the user 357 | * 358 | * @return string 359 | */ 360 | protected function formatResponse($code) 361 | { 362 | $resp = 'ver='.implode(',',$this->config->getAcceptedVersions())."\r\n" 363 | ."nut=".$this->sqrlGenerator->getNonce($code, $this->authenticationKey, $this->requestNut)."\r\n" 364 | .'tif='. strtoupper(dechex($code))."\r\n" 365 | ."qry=".$this->sqrlGenerator->generateQry(); 366 | if (!empty($this->ask)) { 367 | $resp.= "\r\nask=".$this->ask; 368 | } 369 | if (($this->tif&self::SQRL_DISABLED && !in_array('lock', $this->actions))) { 370 | $resp.= "\r\nsuk=".$this->base64UrlEncode($this->store->getIdentitySUK($this->authenticationKey)); 371 | } elseif ($this->tif&self::PREVIOUS_ID_MATCH && !in_array('ident', $this->actions)) { 372 | $resp.= "\r\nsuk=".$this->base64UrlEncode($this->store->getIdentitySUK($this->previousIdKey)); 373 | } 374 | return $this->base64UrlEncode($resp); 375 | } 376 | 377 | /** 378 | * Base 64 URL encodes a string 379 | * 380 | * Basically the same as base64 encoding, but replacing "+" with "-" and 381 | * "/" with "_" to make it safe to include in a URL 382 | * 383 | * Optionally removes trailing "=" padding characters. 384 | * 385 | * @param string $string The string to encode 386 | * @param type $stripEquals [Optional] Whether to strip the "=" off of the end 387 | * 388 | * @return string 389 | */ 390 | protected function base64UrlEncode($string, $stripEquals=true) 391 | { 392 | $base64 = base64_encode($string); 393 | $urlencode = str_replace(array('+','/'), array('-','_'), $base64); 394 | if($stripEquals){ 395 | $urlencode = trim($urlencode, '='); 396 | } 397 | return $urlencode; 398 | } 399 | 400 | /** 401 | * Base 64 URL decodes a string 402 | * 403 | * Basically the same as base64 decoding, but replacing URL safe "-" with "+" 404 | * and "_" with "/". Automatically detects if the trailing "=" padding has 405 | * been removed. 406 | * 407 | * @param type $string 408 | * @return type 409 | */ 410 | protected function base64URLDecode($string) 411 | { 412 | $len = strlen($string); 413 | if($len%4 > 0){ 414 | $string = str_pad($string, 4-($len%4), '='); 415 | } 416 | $base64 = str_replace(array('-','_'), array('+','/'), $string); 417 | return base64_decode($base64); 418 | } 419 | } 420 | -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/Tests/RequestHandlerScenario.php: -------------------------------------------------------------------------------- 1 | generator = $this->test->getMockBuilder(SqrlGenerateInterface::class)->getMock(); 52 | $this->validator = $this->test->getMockBuilder(SqrlValidateInterface::class)->getMock(); 53 | $this->storage = $this->test->getMockBuilder(SqrlStoreInterface::class)->getMock(); 54 | 55 | $this->config = $this->test->getMockBuilder(SqrlConfiguration::class)->getMock(); 56 | $this->config->expects($this->test->any()) 57 | ->method('getAcceptedVersions') 58 | ->will($this->test->returnValue(array('1'))); 59 | $this->handler = new SqrlRequestHandler($this->config,$this->validator,$this->storage,$this->generator); 60 | } 61 | 62 | /* GIVENS */ 63 | 64 | /** 65 | * Sets the response the server last sent 66 | * 67 | * This can be formatted as either a URL string if the server's last response was the initial SQRL QR 68 | * or an array of key=>value parameters that were the last reply in an authentication chain 69 | * 70 | * @param array|string $originalReply 71 | */ 72 | public function serverLastSent($originalReply): void 73 | { 74 | $this->serverOriginalReply = $originalReply; 75 | } 76 | 77 | /** 78 | * Sets what the server knows about a key 79 | * 80 | * @param string $key 81 | * @param int $keyStatus Must be a SqrlStoreInterface::IDENTITY_* value 82 | */ 83 | public function serverKnowsKey(string $key, int $keyStatus) 84 | { 85 | $this->serverKeys[$key]=$keyStatus; 86 | } 87 | 88 | /** 89 | * Sets what the server knows about a nut 90 | * @param string $nut 91 | * @param int $nutStatus Must be a SqrlValidateInterface constant 92 | * @param null|string $associatedKey 93 | */ 94 | public function serverKnowsNut(string $nut, int $nutStatus, ?string $associatedKey = null) 95 | { 96 | $this->nut = ['nut'=>$nut, 'status'=>$nutStatus, 'oldKey'=>$associatedKey]; 97 | } 98 | 99 | /** 100 | * Sets what IP the server thinks the nut is connected to 101 | * 102 | * @param string $ip 103 | */ 104 | public function nutConnectedToIp(string $ip) 105 | { 106 | $this->originalIp = $ip; 107 | } 108 | 109 | /** 110 | * Sets whether the server has been configured to accept new account creations 111 | * @param bool $accept 112 | */ 113 | public function serverAcceptsNewAccounts(bool $accept = true): void 114 | { 115 | $this->config->expects($this->test->any())->method('getAnonAllowed')->will($this->test->returnValue($accept)); 116 | } 117 | 118 | public function serverKnowsSuk(string $idk, string $suk) 119 | { 120 | $this->storage->expects($this->test->any()) 121 | ->method('getIdentitySUK') 122 | ->with($this->test->equalTo($idk)) 123 | ->will($this->test->returnValue($suk)); 124 | } 125 | 126 | public function serverKnowsVuk(string $idk, string $vuk) 127 | { 128 | $this->storage->expects($this->test->any()) 129 | ->method('getIdentityVUK') 130 | ->with($this->test->equalTo($idk)) 131 | ->will($this->test->returnValue($vuk)); 132 | } 133 | 134 | /* WHENS */ 135 | /** 136 | * Sets what nut the client sends in the GET parameters 137 | * 138 | * @param string $nut 139 | */ 140 | public function clientSendsNut(string $nut) 141 | { 142 | $this->clientNut = $nut; 143 | } 144 | 145 | /** 146 | * Says that the client sent back the same server value the server last sent 147 | */ 148 | public function clientSendsOriginalServer() 149 | { 150 | $this->clientServerParam = $this->serverOriginalReply; 151 | } 152 | 153 | /** 154 | * Sets what the client says the server last sent 155 | * 156 | * @param string $server 157 | */ 158 | public function clientSendsServerParam(string $server) 159 | { 160 | $this->clientServerParam = $server; 161 | } 162 | 163 | /** 164 | * Sets the parameters the client is sending in their request 165 | * 166 | * @param array $requestParams 167 | */ 168 | public function clientSendsRequest(array $requestParams) 169 | { 170 | $this->clientRequest = $requestParams; 171 | } 172 | 173 | /** 174 | * Sets a signature the client sends 175 | * 176 | * @param string $key Should match a key in the client request 177 | * @param string $signature 178 | * @param bool $valid 179 | */ 180 | public function clientSendsSignature(string $key, string $signature, bool $valid = true) 181 | { 182 | $this->clientSignatures[$key] = ['sig'=>$signature, 'valid'=>$valid]; 183 | } 184 | 185 | /** 186 | * Sets what IP the client request is from 187 | * 188 | * @param string $ip 189 | */ 190 | public function clientRequestsFromIp(string $ip) 191 | { 192 | $this->clientIp = $ip; 193 | } 194 | 195 | public function clientRequestIsSecure(bool $secure = true) 196 | { 197 | $this->clientSecure = $secure; 198 | } 199 | 200 | /* THENS */ 201 | 202 | /** 203 | * Set the TIF the server should respond with 204 | * 205 | * @param int $tif 206 | */ 207 | public function expectTif(int $tif) 208 | { 209 | $this->expectedTif = $tif; 210 | } 211 | 212 | /** 213 | * Set the new nut the server should respond with 214 | * 215 | * @param string $nut 216 | */ 217 | public function expectNewNut(string $nut) 218 | { 219 | $this->expectedNut = $nut; 220 | } 221 | 222 | /** 223 | * Set the new nut the server should respond with to the fail state nut 224 | */ 225 | public function expectFailedNut() 226 | { 227 | $this->expectedNut = 'failnut'; 228 | } 229 | 230 | /** 231 | * Sets whether the validator should say the server parameter is valid 232 | * 233 | * @param bool $isValid 234 | */ 235 | public function expectServerParamValid(bool $isValid = true) 236 | { 237 | $this->serverParamValid = $isValid; 238 | } 239 | 240 | /** 241 | * Sets additional parameters that should be expeccted in the server's response (ask, sin, etc.) 242 | * @param array $params 243 | */ 244 | public function expectAdditionalResponseParams(array $params) 245 | { 246 | $this->additionalExpectedResponseParams = $params; 247 | } 248 | 249 | /** 250 | * Finalizes configuration and checks that the server response matches the expected response 251 | * 252 | * @throws SqrlException 253 | */ 254 | public function checkResponse() 255 | { 256 | $this->setUpValidator(); 257 | $this->setUpStorage(); 258 | $this->setUpGenerator(); 259 | $this->sendClientRequest(); 260 | $this->validateResponse(); 261 | } 262 | 263 | /* Helpers */ 264 | 265 | protected function convertServerReplyToString(): string 266 | { 267 | if (is_string($this->serverOriginalReply)) { 268 | return $this->serverOriginalReply; 269 | } 270 | return $this->paramArrayToSqrlString($this->serverOriginalReply); 271 | } 272 | 273 | protected function paramArrayToSqrlString(array $params): string 274 | { 275 | $combinedData = array_map(function ($key, $val) { 276 | return $key.'='.$val; 277 | }, array_keys($params), $params); 278 | return implode("\r\n", $combinedData); 279 | } 280 | 281 | private function getClientString() 282 | { 283 | $encodedFields = ['idk', 'pidk', 'suk', 'vuk']; 284 | $result = []; 285 | foreach ($this->clientRequest as $key=>$value) { 286 | if (in_array($key, $encodedFields)) { 287 | $result[$key] = $this->base64UrlEncode($value); 288 | } else { 289 | $result[$key] = $value; 290 | } 291 | } 292 | return $this->paramArrayToSqrlString($result); 293 | } 294 | 295 | protected function setUpValidator(): void 296 | { 297 | $this->validator->expects($this->test->any()) 298 | ->method('validateServer') 299 | ->with( 300 | $this->test->equalTo($this->clientServerParam), 301 | $this->test->equalTo($this->clientNut), 302 | $this->test->equalTo($this->clientSecure) 303 | ) 304 | ->will($this->test->returnValue($this->serverParamValid)); 305 | 306 | if (!empty($this->nut)) { 307 | $this->validator->expects($this->test->any()) 308 | ->method('validateNut') 309 | ->with($this->test->equalTo($this->clientNut), $this->test->equalTo($this->getClientKey('idk'))) 310 | ->will($this->test->returnValue($this->nut['status'])); 311 | } 312 | 313 | $request = $this->base64UrlEncode($this->getClientString()) 314 | .$this->base64UrlEncode($this->convertServerReplyToString()); 315 | $this->validator->expects($this->test->any()) 316 | ->method('validateSignature') 317 | ->with($this->test->equalTo($request), $this->test->anything(), $this->test->anything()) 318 | ->will($this->test->returnCallback( 319 | function ($message, $key, $sig) { 320 | $this->test->assertTrue(isset($this->clientSignatures[$key]), 'Key not found'); 321 | $this->test->assertEquals($this->clientSignatures[$key]['sig'], $sig); 322 | return $this->clientSignatures[$key]['valid']; 323 | } 324 | )); 325 | 326 | $this->validator->expects($this->test->any()) 327 | ->method('nutIPMatches') 328 | ->with($this->test->equalTo($this->clientNut), $this->test->equalTo($this->clientIp)) 329 | ->will($this->test->returnValue($this->clientIp === $this->originalIp)); 330 | } 331 | 332 | protected function getClientKey(string $keyId): ?string 333 | { 334 | if (isset($this->clientRequest[$keyId])) { 335 | return $this->clientRequest[$keyId]; 336 | } 337 | return null; 338 | } 339 | 340 | protected function setUpStorage(): void 341 | { 342 | $this->storage->expects($this->test->any()) 343 | ->method('checkIdentityKey') 344 | ->with($this->test->anything()) 345 | ->will($this->test->returnCallback( 346 | function ($key) { 347 | $this->test->assertTrue(isset($this->serverKeys[$key]), 'Key not found'); 348 | return $this->serverKeys[$key]; 349 | } 350 | )); 351 | } 352 | 353 | protected function setUpGenerator(): void 354 | { 355 | if (!empty($this->nut)) { 356 | $this->generator->expects($this->test->any()) 357 | ->method('getNonce') 358 | ->with( 359 | $this->test->equalTo($this->expectedTif), 360 | $this->expectedNut === 'failnut' ? 361 | $this->test->anything() ://when the nut is for a failure response, it doesn't get tied to a key 362 | $this->test->equalTo($this->getClientKey('idk')), 363 | $this->test->equalTo($this->nut['nut']) 364 | )->will($this->test->returnValue($this->expectedNut)); 365 | $this->generator->expects($this->test->any()) 366 | ->method('generateQry') 367 | ->will($this->test->returnValue('sqrl?nut=' . $this->expectedNut)); 368 | } else { 369 | $this->generator->expects($this->test->any()) 370 | ->method('getNonce') 371 | ->with( 372 | $this->test->equalTo($this->expectedTif), 373 | $this->test->equalTo(''), 374 | $this->test->equalTo('') 375 | )->will($this->test->returnValue($this->expectedNut)); 376 | $this->generator->expects($this->test->any()) 377 | ->method('generateQry') 378 | ->will($this->test->returnValue('sqrl?nut=' . $this->expectedNut)); 379 | } 380 | } 381 | 382 | /** 383 | * @throws SqrlException 384 | */ 385 | protected function sendClientRequest(): void 386 | { 387 | $body = [ 388 | 'server' => $this->base64UrlEncode( 389 | is_array($this->clientServerParam) ? 390 | $this->paramArrayToSqrlString($this->clientServerParam) : 391 | $this->clientServerParam 392 | ), 393 | 'client' => $this->base64UrlEncode($this->getClientString()) 394 | ]; 395 | $keyPosMap = ['idk'=>'ids', 'pidk'=>'pids', 'vuk'=>'urs']; 396 | foreach ($this->clientSignatures as $key=>$sigData) { 397 | $param = $keyPosMap[$this->findClientKeyPos($key)]; 398 | $body[$param] = $this->base64UrlEncode($sigData['sig']); 399 | } 400 | 401 | $this->handler->parseRequest( 402 | ['nut' => $this->clientNut], 403 | $body, 404 | ['REMOTE_ADDR' => $this->clientIp, 'HTTPS' => $this->clientSecure ? '1' : '0'] 405 | ); 406 | } 407 | 408 | protected function findClientKeyPos($key): string 409 | { 410 | $pos = array_search($key, $this->clientRequest); 411 | if ($pos === false) { 412 | return 'vuk'; 413 | } 414 | return $pos; 415 | } 416 | 417 | /** 418 | * @param array $expectedResponse 419 | * @throws SqrlException 420 | */ 421 | protected function validateResponse() 422 | { 423 | $expectedResponse = array_merge([ 424 | 'ver'=>'1', 425 | 'nut'=>$this->expectedNut, 426 | 'tif'=>strtoupper(dechex( $this->expectedTif)), 427 | 'qry'=>'sqrl?nut='.$this->expectedNut 428 | ], $this->additionalExpectedResponseParams); 429 | $this->test->assertEquals( 430 | $this->paramArrayToSqrlString($expectedResponse), 431 | $this->base64UrlDecode($this->handler->getResponseMessage()) 432 | ); 433 | } 434 | 435 | public function expectLogin(bool $shouldHappen = true): void 436 | { 437 | $this->storage->expects($shouldHappen ? $this->test->once() : $this->test->never()) 438 | ->method('logSessionIn') 439 | ->with($this->test->equalTo($this->nut['nut'])); 440 | } 441 | 442 | public function expectRegistration(string $idk, string $suk, string $vuk): void 443 | { 444 | $this->storage->expects($this->test->once()) 445 | ->method('createIdentity') 446 | ->with($this->test->equalTo($idk), $this->test->equalTo($suk), $this->test->equalTo($vuk)); 447 | } 448 | 449 | public function expectNoRegistration() 450 | { 451 | $this->storage->expects($this->test->never())->method('createIdentity'); 452 | } 453 | 454 | public function expectLock(string $idk): void 455 | { 456 | $this->storage->expects($this->test->once())->method('lockIdentityKey')->with($this->test->equalTo($idk)); 457 | } 458 | 459 | public function expectLogout(string $nut) 460 | { 461 | $this->storage->expects($this->test->once())->method('endSession')->with($this->test->equalTo($nut)); 462 | } 463 | 464 | public function expectUnlock(string $idk) 465 | { 466 | $this->storage->expects($this->test->once())->method('unlockIdentityKey')->with($this->test->equalTo($idk)); 467 | } 468 | 469 | public function expectNoUnlock() 470 | { 471 | $this->storage->expects($this->test->never())->method('unlockIdentityKey'); 472 | } 473 | 474 | public function expectIdentityKeyUpdate(string $oldIdk, string $newIdk, string $newSuk, string $newVuk) 475 | { 476 | $this->storage->expects($this->test->once()) 477 | ->method('updateIdentityKey') 478 | ->with( 479 | $this->test->equalTo($oldIdk), 480 | $this->test->equalTo($newIdk), 481 | $this->test->equalTo($newSuk), 482 | $this->test->equalTo($newVuk) 483 | ); 484 | } 485 | 486 | public function expectNoIdentityKeyUpdate() 487 | { 488 | $this->storage->expects($this->test->never())->method('updateIdentityKey'); 489 | } 490 | } -------------------------------------------------------------------------------- /src/Trianglman/Sqrl/Tests/SqrlValidateTest.php: -------------------------------------------------------------------------------- 1 | config = $this->getMockBuilder(SqrlConfiguration::class)->getMock(); 64 | $this->val = $this->getMockBuilder(NonceValidatorInterface::class)->getMock(); 65 | $this->storage = $this->getMockBuilder(SqrlStoreInterface::class)->getMock(); 66 | 67 | $this->obj = new SqrlValidate($this->config,$this->val,$this->storage); 68 | } 69 | 70 | public function testValidatesServerFromUrl() 71 | { 72 | $this->config->expects($this->any())->method('getDomain') 73 | ->will($this->returnValue('example.com')); 74 | $this->config->expects($this->any())->method('getAuthenticationPath') 75 | ->will($this->returnValue('/sqrl')); 76 | $this->config->expects($this->any())->method('getSecure') 77 | ->will($this->returnValue(true)); 78 | 79 | $this->assertTrue($this->obj->validateServer('sqrl://example.com/sqrl?nut=1234', '1234', true)); 80 | } 81 | 82 | public function testValidatesServerFromUrlWithExtendedDomain() 83 | { 84 | $this->config->expects($this->any())->method('getDomain') 85 | ->will($this->returnValue('example.com/~user')); 86 | $this->config->expects($this->any())->method('getAuthenticationPath') 87 | ->will($this->returnValue('/~user/sqrl')); 88 | $this->config->expects($this->any())->method('getSecure') 89 | ->will($this->returnValue(true)); 90 | 91 | $this->assertTrue($this->obj->validateServer('sqrl://example.com/~user/sqrl?nut=1234&x=6', '1234', true)); 92 | } 93 | 94 | public function testValidatesServerFromUrlInvalidSecurity() 95 | { 96 | $this->config->expects($this->any())->method('getDomain') 97 | ->will($this->returnValue('example.com')); 98 | $this->config->expects($this->any())->method('getAuthenticationPath') 99 | ->will($this->returnValue('/sqrl')); 100 | $this->config->expects($this->any())->method('getSecure') 101 | ->will($this->returnValue(true)); 102 | 103 | 104 | $this->assertFalse($this->obj->validateServer('sqrl://example.com/sqrl?nut=1234', '1234', false)); 105 | } 106 | 107 | public function testValidatesServerFromUrlInvalidProtocol() 108 | { 109 | $this->config->expects($this->any())->method('getDomain') 110 | ->will($this->returnValue('example.com')); 111 | $this->config->expects($this->any())->method('getAuthenticationPath') 112 | ->will($this->returnValue('/sqrl')); 113 | $this->config->expects($this->any())->method('getSecure') 114 | ->will($this->returnValue(true)); 115 | 116 | $this->assertFalse($this->obj->validateServer('qrl://example.com/sqrl?nut=1234', '1234', true)); 117 | } 118 | 119 | public function testValidatesServerFromUrlInvalidDomain() 120 | { 121 | $this->config->expects($this->any())->method('getDomain') 122 | ->will($this->returnValue('example.com')); 123 | $this->config->expects($this->any())->method('getAuthenticationPath') 124 | ->will($this->returnValue('/sqrl')); 125 | $this->config->expects($this->any())->method('getSecure') 126 | ->will($this->returnValue(true)); 127 | 128 | $this->assertFalse($this->obj->validateServer('sqrl://fakeexample.com/sqrl?nut=1234', '1234', true)); 129 | } 130 | 131 | public function testValidatesServerFromUrlInvalidAuthPath() 132 | { 133 | $this->config->expects($this->any())->method('getDomain') 134 | ->will($this->returnValue('example.com')); 135 | $this->config->expects($this->any())->method('getAuthenticationPath') 136 | ->will($this->returnValue('/sqrl')); 137 | $this->config->expects($this->any())->method('getSecure') 138 | ->will($this->returnValue(true)); 139 | 140 | $this->assertFalse($this->obj->validateServer('sqrl://example.com/notsqrl?nut=1234', '1234', true)); 141 | } 142 | 143 | public function testValidatesServerFromUrlNutDoesntMatch() 144 | { 145 | $this->config->expects($this->any())->method('getDomain') 146 | ->will($this->returnValue('example.com')); 147 | $this->config->expects($this->any())->method('getAuthenticationPath') 148 | ->will($this->returnValue('/sqrl')); 149 | $this->config->expects($this->any())->method('getSecure') 150 | ->will($this->returnValue(true)); 151 | 152 | $this->assertFalse($this->obj->validateServer('sqrl://example.com/sqrl?nut=1234', '1235', true)); 153 | } 154 | 155 | public function testValidatesServerFromArray() 156 | { 157 | $this->config->expects($this->any())->method('getAcceptedVersions') 158 | ->will($this->returnValue([1])); 159 | $this->config->expects($this->any())->method('getAuthenticationPath') 160 | ->will($this->returnValue('/sqrl')); 161 | $this->config->expects($this->any())->method('getSecure') 162 | ->will($this->returnValue(true)); 163 | $this->storage->expects($this->any())->method('getNutDetails') 164 | ->with($this->equalTo('newNut')) 165 | ->will($this->returnValue([ 166 | 'tif' => 0xD, 167 | 'originalKey' => 'some key', 168 | 'originalNut' => 'someNut', 169 | 'createdDate' => new \DateTime(), 170 | 'nutIP' => '192.168.0.105' 171 | ])); 172 | 173 | $server = [ 174 | 'ver' => '1', 175 | 'nut' => 'newNut', 176 | 'tif' => 'D', 177 | 'qry' => '/sqrl?nut=newNut', 178 | 'suk' => $this->base64UrlEncode('validSUK') 179 | ]; 180 | $this->assertTrue($this->obj->validateServer($server, 'newNut', true)); 181 | } 182 | 183 | public function testValidatesServerFromArrayMissingRequiredFields() 184 | { 185 | $server = [ 186 | 'ver' => '1', 187 | 'nut' => 'newNut', 188 | 'qry' => '/sqrl?nut=newNut', 189 | 'suk' => $this->base64UrlEncode('validSUK') 190 | ]; 191 | $this->assertFalse($this->obj->validateServer($server, 'newNut', true)); 192 | } 193 | 194 | public function testValidatesServerFromArrayInvalidVersion() 195 | { 196 | $this->config->expects($this->any())->method('getAcceptedVersions') 197 | ->will($this->returnValue([1])); 198 | $this->config->expects($this->any())->method('getAuthenticationPath') 199 | ->will($this->returnValue('/sqrl')); 200 | $this->config->expects($this->any())->method('getSecure') 201 | ->will($this->returnValue(true)); 202 | $this->storage->expects($this->any())->method('getNutDetails') 203 | ->with($this->equalTo('newNut')) 204 | ->will($this->returnValue([ 205 | 'tif' => 0x5, 206 | 'originalKey' => 'some key', 207 | 'originalNut' => 'someNut', 208 | 'createdDate' => new \DateTime(), 209 | 'nutIP' => '192.168.0.105' 210 | ])); 211 | 212 | $server = [ 213 | 'ver' => '666', 214 | 'nut' => 'newNut', 215 | 'tif' => '5', 216 | 'qry' => '/sqrl?nut=newNut', 217 | 'suk' => $this->base64UrlEncode('validSUK') 218 | ]; 219 | $this->assertFalse($this->obj->validateServer($server, 'newNut', true)); 220 | } 221 | 222 | public function testValidatesServerFromArrayInvalidTif() 223 | { 224 | $this->config->expects($this->any())->method('getAcceptedVersions') 225 | ->will($this->returnValue([1])); 226 | $this->config->expects($this->any())->method('getAuthenticationPath') 227 | ->will($this->returnValue('/sqrl')); 228 | $this->config->expects($this->any())->method('getSecure') 229 | ->will($this->returnValue(true)); 230 | $this->storage->expects($this->any())->method('getNutDetails') 231 | ->with($this->equalTo('newNut')) 232 | ->will($this->returnValue([ 233 | 'tif' => 0x5, 234 | 'originalKey' => 'some key', 235 | 'originalNut' => 'someNut', 236 | 'createdDate' => new \DateTime(), 237 | 'nutIP' => '192.168.0.105' 238 | ])); 239 | 240 | $server = [ 241 | 'ver' => '666', 242 | 'nut' => 'newNut', 243 | 'tif' => '20', 244 | 'qry' => '/sqrl?nut=newNut', 245 | 'suk' => $this->base64UrlEncode('validSUK') 246 | ]; 247 | $this->assertFalse($this->obj->validateServer($server, 'newNut', true)); 248 | } 249 | 250 | public function testValidatesServerFromArrayInvalidQry() 251 | { 252 | $this->config->expects($this->any())->method('getAcceptedVersions') 253 | ->will($this->returnValue([1])); 254 | $this->config->expects($this->any())->method('getAuthenticationPath') 255 | ->will($this->returnValue('/sqrl')); 256 | $this->config->expects($this->any())->method('getSecure') 257 | ->will($this->returnValue(true)); 258 | $this->storage->expects($this->any())->method('getNutDetails') 259 | ->with($this->equalTo('newNut')) 260 | ->will($this->returnValue([ 261 | 'tif' => 0x5, 262 | 'originalKey' => 'some key', 263 | 'originalNut' => 'someNut', 264 | 'createdDate' => new \DateTime(), 265 | 'nutIP' => '192.168.0.105' 266 | ])); 267 | 268 | $server = [ 269 | 'ver' => '1', 270 | 'nut' => 'newNut', 271 | 'tif' => '5', 272 | 'qry' => '/notsqrl?nut=newNut', 273 | 'suk' => $this->base64UrlEncode('validSUK') 274 | ]; 275 | $this->assertFalse($this->obj->validateServer($server, 'newNut', true)); 276 | } 277 | 278 | public function testValidatesServerFromArraySecurityDowngrade() 279 | { 280 | $this->config->expects($this->any())->method('getAcceptedVersions') 281 | ->will($this->returnValue([1])); 282 | $this->config->expects($this->any())->method('getAuthenticationPath') 283 | ->will($this->returnValue('/sqrl')); 284 | $this->config->expects($this->any())->method('getSecure') 285 | ->will($this->returnValue(true)); 286 | $this->storage->expects($this->any())->method('getNutDetails') 287 | ->with($this->equalTo('newNut')) 288 | ->will($this->returnValue([ 289 | 'tif' => 0x5, 290 | 'originalKey' => 'some key', 291 | 'originalNut' => 'someNut', 292 | 'createdDate' => new \DateTime(), 293 | 'nutIP' => '192.168.0.105' 294 | ])); 295 | 296 | $server = [ 297 | 'ver' => '1', 298 | 'nut' => 'newNut', 299 | 'tif' => '5', 300 | 'qry' => '/sqrl?nut=newNut', 301 | 'suk' => $this->base64UrlEncode('validSUK') 302 | ]; 303 | $this->assertFalse($this->obj->validateServer($server, 'newNut', false)); 304 | } 305 | 306 | public function testValidatesGoodNut() 307 | { 308 | $this->config->expects($this->any())->method('getNonceMaxAge') 309 | ->will($this->returnValue(5)); 310 | $this->storage->expects($this->any())->method('getNutDetails') 311 | ->with($this->equalTo('1234')) 312 | ->will($this->returnValue([ 313 | 'tif' => 0x5, 314 | 'originalKey' => 'some key', 315 | 'originalNut' => 'someNut', 316 | 'createdDate' => new \DateTime(), 317 | 'nutIP' => '192.168.0.105' 318 | ])); 319 | $this->assertEquals(SqrlValidateInterface::VALID_NUT, $this->obj->validateNut('1234')); 320 | } 321 | 322 | public function testValidatesExpiredNut() 323 | { 324 | $this->config->expects($this->any())->method('getNonceMaxAge') 325 | ->will($this->returnValue(5)); 326 | $this->storage->expects($this->any())->method('getNutDetails') 327 | ->with($this->equalTo('old1234')) 328 | ->will($this->returnValue([ 329 | 'tif' => 0x5, 330 | 'originalKey' => 'some key', 331 | 'originalNut' => 'someNut', 332 | 'createdDate' => new \DateTime('-6 minutes'), 333 | 'nutIP' => '192.168.0.105' 334 | ])); 335 | $this->assertEquals(SqrlValidateInterface::EXPIRED_NUT, $this->obj->validateNut('old1234')); 336 | } 337 | 338 | public function testValidatesUnknownNut() 339 | { 340 | $this->config->expects($this->any())->method('getNonceMaxAge') 341 | ->will($this->returnValue(5)); 342 | $this->storage->expects($this->any())->method('getNutDetails') 343 | ->with($this->equalTo('you know nothing')) 344 | ->will($this->returnValue(null)); 345 | $this->assertEquals(SqrlValidateInterface::INVALID_NUT, $this->obj->validateNut('you know nothing')); 346 | } 347 | 348 | public function testValidatesGoodNutMatchingKey() 349 | { 350 | $this->config->expects($this->any())->method('getNonceMaxAge') 351 | ->will($this->returnValue(5)); 352 | $this->storage->expects($this->any())->method('getNutDetails') 353 | ->with($this->equalTo('1234')) 354 | ->will($this->returnValue([ 355 | 'tif' => 0x5, 356 | 'originalKey' => 'some key', 357 | 'originalNut' => 'someNut', 358 | 'createdDate' => new \DateTime(), 359 | 'nutIP' => '192.168.0.105' 360 | ])); 361 | $this->assertEquals(SqrlValidateInterface::VALID_NUT, $this->obj->validateNut('1234','some key')); 362 | } 363 | 364 | public function testValidatesGoodNutMismatchKey() 365 | { 366 | $this->config->expects($this->any())->method('getNonceMaxAge') 367 | ->will($this->returnValue(5)); 368 | $this->storage->expects($this->any())->method('getNutDetails') 369 | ->with($this->equalTo('1234')) 370 | ->will($this->returnValue([ 371 | 'tif' => 0x5, 372 | 'originalKey' => 'some key', 373 | 'originalNut' => 'someNut', 374 | 'createdDate' => new \DateTime(), 375 | 'nutIP' => '192.168.0.105' 376 | ])); 377 | $this->assertEquals(SqrlValidateInterface::KEY_MISMATCH, $this->obj->validateNut('1234','different key')); 378 | } 379 | 380 | public function testValidatesSignature() 381 | { 382 | $this->val->expects($this->any())->method('validateSignature') 383 | ->with( 384 | $this->equalTo('original message'), 385 | $this->equalTo('signature'), 386 | $this->equalTo('good key') 387 | )->will($this->returnValue(true)); 388 | 389 | $this->assertTrue($this->obj->validateSignature('original message', 'good key', 'signature')); 390 | } 391 | 392 | public function testValidatesBadSignature() 393 | { 394 | $this->val->expects($this->any())->method('validateSignature') 395 | ->with( 396 | $this->equalTo('original message'), 397 | $this->equalTo('bad signature'), 398 | $this->equalTo('some key') 399 | )->will($this->returnValue(false)); 400 | 401 | 402 | $this->assertFalse($this->obj->validateSignature('original message', 'some key', 'bad signature')); 403 | } 404 | 405 | public function testChecksNutIPMatch() 406 | { 407 | $this->storage->expects($this->any())->method('getNutDetails') 408 | ->with($this->equalTo('anut')) 409 | ->will($this->returnValue([ 410 | 'tif' => 0x5, 411 | 'originalKey' => 'some key', 412 | 'originalNut' => 'someNut', 413 | 'createdDate' => new \DateTime(), 414 | 'nutIP' => '192.168.0.105' 415 | ])); 416 | 417 | $this->assertTrue($this->obj->nutIPMatches('anut', '192.168.0.105')); 418 | } 419 | 420 | public function testChecksNutIPMismatch() 421 | { 422 | $this->storage->expects($this->any())->method('getNutDetails') 423 | ->with($this->equalTo('anut')) 424 | ->will($this->returnValue([ 425 | 'tif' => 0x5, 426 | 'originalKey' => 'some key', 427 | 'originalNut' => 'someNut', 428 | 'createdDate' => new \DateTime(), 429 | 'nutIP' => '192.168.0.105' 430 | ])); 431 | 432 | $this->assertFalse($this->obj->nutIPMatches('anut', '127.0.0.1')); 433 | } 434 | } 435 | --------------------------------------------------------------------------------