├── 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 |
4 |
5 |
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 |
56 |
57 | - Public key:
58 | - SUK:
59 | - VUK:
60 |
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 |
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 |
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 |
--------------------------------------------------------------------------------