From aed50e530fece1b7e5637aff483efc50fc883857 Mon Sep 17 00:00:00 2001 From: Lukas Metzger Date: Sun, 29 Apr 2018 16:47:20 +0200 Subject: [PATCH] Added POST /remote/changekey API --- backend/src/config/ConfigDefault.php | 3 + backend/src/controllers/Remote.php | 27 +++++++ backend/src/operations/Remote.php | 52 ++++++++++++- backend/src/public/index.php | 1 + backend/test/db.sql | 2 +- backend/test/package-lock.json | 13 ++++ backend/test/package.json | 3 +- backend/test/tests/remote-changekey.js | 102 +++++++++++++++++++++++++ 8 files changed, 200 insertions(+), 3 deletions(-) create mode 100644 backend/test/tests/remote-changekey.js diff --git a/backend/src/config/ConfigDefault.php b/backend/src/config/ConfigDefault.php index 36cd419..de0a803 100644 --- a/backend/src/config/ConfigDefault.php +++ b/backend/src/config/ConfigDefault.php @@ -24,6 +24,9 @@ $defaultConfig = [ 'config' => null ] ], + 'remote' => [ + 'timestampWindow' => 15 + ], 'records' => [ 'allowedTypes' => [ 'A', 'A6', 'AAAA', 'AFSDB', 'ALIAS', 'CAA', 'CDNSKEY', 'CDS', 'CERT', 'CNAME', 'DHCID', diff --git a/backend/src/controllers/Remote.php b/backend/src/controllers/Remote.php index ea8a35f..e4c6102 100644 --- a/backend/src/controllers/Remote.php +++ b/backend/src/controllers/Remote.php @@ -54,6 +54,33 @@ class Remote return $res->withStatus(204); } + public function updateKey(Request $req, Response $res, array $args) + { + $record = $req->getParsedBodyParam('record'); + $content = $req->getParsedBodyParam('content'); + $time = $req->getParsedBodyParam('time'); + $signature = $req->getParsedBodyParam('signature'); + + if ($record === null || $content === null || $time === null || $signature === null) { + return $res->withJson(['error' => 'One of the required fields is missing.'], 422); + } + + $remote = new \Operations\Remote($this->c); + + try { + $remote->updateKey($record, $content, $time, $signature); + } catch (\Exceptions\NotFoundException $e) { + $this->logger->debug('User tried to update non existent record via changekey api.'); + return $res->withJson(['error' => 'The given record does not exist.'], 404); + } catch (\Exceptions\ForbiddenException $e) { + $this->logger->debug('User tried to update an record via changekey api with incorrect password.'); + return $res->withJson(['error' => 'The provided password was invalid.'], 403); + } + + $this->logger->info('Record ' . $record . ' was changed via the changekey api.'); + return $res->withStatus(204); + } + public function servertime(Request $req, Response $res, array $args) { return $res->withJson([ diff --git a/backend/src/operations/Remote.php b/backend/src/operations/Remote.php index 6763521..0a96447 100644 --- a/backend/src/operations/Remote.php +++ b/backend/src/operations/Remote.php @@ -26,7 +26,7 @@ class Remote } /** - * Add new record + * Update given record with password * * @param $record Name of the new record * @param $content Type of the new record @@ -65,4 +65,54 @@ class Remote $records = new \Operations\Records($this->c); $records->updateRecord($record, null, null, $content, null, null); } + + /** + * Update given record with password + * + * @param $record Name of the new record + * @param $content Type of the new record + * @param $time Timestamp of the signature + * @param $signature Signature + * + * @throws NotFoundException if the record does not exist + * @throws ForbiddenException if the signature is not valid for the record + */ + public function updateKey(int $record, string $content, int $time, string $signature) : void + { + $timestampWindow = $this->c['config']['remote']['timestampWindow']; + + $query = $this->db->prepare('SELECT id FROM records WHERE id=:record'); + $query->bindValue(':record', $record, \PDO::PARAM_INT); + $query->execute(); + + if ($query->fetch() === false) { + throw new \Exceptions\NotFoundException(); + } + + $query = $this->db->prepare('SELECT security FROM remote WHERE record=:record AND type=\'key\''); + $query->bindValue(':record', $record, \PDO::PARAM_INT); + $query->execute(); + + if (abs($time - time()) > $timestampWindow) { + throw new \Exceptions\ForbiddenException(); + } + + $validKeyFound = false; + + $verifyString = $record . $content . $time; + + while ($row = $query->fetch()) { + if (openssl_verify($verifyString, base64_decode($signature), $row['security'], OPENSSL_ALGO_SHA512)) { + $validKeyFound = true; + break; + } + } + + if (!$validKeyFound) { + throw new \Exceptions\ForbiddenException(); + } + + $records = new \Operations\Records($this->c); + $records->updateRecord($record, null, null, $content, null, null); + } } diff --git a/backend/src/public/index.php b/backend/src/public/index.php index c3f617d..ecd2ab9 100644 --- a/backend/src/public/index.php +++ b/backend/src/public/index.php @@ -33,6 +33,7 @@ $app->group('/v1', function () { $this->get('/remote/ip', '\Controllers\Remote:ip'); $this->get('/remote/servertime', '\Controllers\Remote:servertime'); $this->get('/remote/updatepw', '\Controllers\Remote:updatePassword'); + $this->post('/remote/updatekey', '\Controllers\Remote:updateKey'); $this->group('', function () { $this->delete('/sessions/{sessionId}', '\Controllers\Sessions:delete'); diff --git a/backend/test/db.sql b/backend/test/db.sql index 6bb215f..390a8d2 100644 --- a/backend/test/db.sql +++ b/backend/test/db.sql @@ -169,7 +169,7 @@ CREATE TABLE `remote` ( INSERT INTO `remote` (`id`, `record`, `description`, `type`, `security`, `nonce`) VALUES (1, 1, 'Password Test', 'password', '$2y$10$abocd6jj/Tw4jzDtqTnjreNzwcerzkXwoVc.JvZBoZ6p0grEKDWoW', NULL), (2, 4, 'Key Test', 'key', '-----BEGIN PUBLIC KEY-----\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5mu3aH90uSXY9sVLgVSz\nKj4FEctrpFDPyVC4ufbJa/44fuLABFe+IizgZUheNBBO7FjpLJYvsL24o6TEeht4\no5j0KHrRHXqp4WQuAL3ZREv/AhNaOC9/xyjoGwUkKkdC2bIfh0J/ACkezxvUrPsh\nbzhzY+co/M9PqlgTbjKjvlv/pRj2dSp98FzUme3HCh7Nn1EOM3yPMtaKNA9Qkkz1\noalfR3xmJjIanoS9zcK77/yyQ8VwI//CgxvnpnWbORZG0B9W2ZBoI8Bj4zprbbFG\nKNmrb403wfDijYF3MXpSMjKvJ5YVuZsn35EWIi5tqFc0oV7Ryy9nBHzKeoYN7Szs\nrXIS5+ZcQDLuN+pqJ7ByVaw4aVn85py8IdO0IYD5xeKd1i0iqm+KSoFTS1jiNSZu\n6iVl4odixWtW7oPLYBbd/vD2F7Ua5cLd12Rs+6kEVtlpnIf7txyFQL4QHYJxB7fI\ny+m70mfufVvKbFh/mHkhe+Arv71ERDMfAV3AD8++axLqYfU/LLFzanjwIBctAA9a\nj++G0lwl1adURwnBeq8+YrMU4/wg9efquKXLR40dU9nkMJOm5tPm+XHt4o3wio4X\n2FqnD57I7qJCWVc00HtpeWno5vHL+eJu0TdxjBuYXnQfwa1z9pWvGaoBtg7tyHgv\ng7YZJzF1MW5N9ZqnkdFJVEsCAwEAAQ==\n-----END PUBLIC KEY-----', NULL), -(3, 1, 'Key Test 2', 'key', '-----BEGIN PUBLIC KEY-----\r\nMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5mu3aH90uSXY9sVLgVSz\r\nKj4FEctrpFDPyVC4ufbJa/44fuLABFe+IizgZUheNBBO7FjpLJYvsL24o6TEeht4\r\no5j0KHrRHXqp4WQuAL3ZREv/AhNaOC9/xyjoGwUkKkdC2bIfh0J/ACkezxvUrPsh\r\nbzhzY+co/M9PqlgTbjKjvlv/pRj2dSp98FzUme3HCh7Nn1EOM3yPMtaKNA9Qkkz1\r\noalfR3xmJjIanoS9zcK77/yyQ8VwI//CgxvnpnWbORZG0B9W2ZBoI8Bj4zprbbFG\r\nKNmrb403wfDijYF3MXpSMjKvJ5YVuZsn35EWIi5tqFc0oV7Ryy9nBHzKeoYN7Szs\r\nrXIS5+ZcQDLuN+pqJ7ByVaw4aVn85py8IdO0IYD5xeKd1i0iqm+KSoFTS1jiNSZu\r\n6iVl4odixWtW7oPLYBbd/vD2F7Ua5cLd12Rs+6kEVtlpnIf7txyFQL4QHYJxB7fI\r\ny+m70mfufVvKbFh/mHkhe+Arv71ERDMfAV3AD8++axLqYfU/LLFzanjwIBctAA9a\r\nj++G0lwl1adURwnBeq8+YrMU4/wg9efquKXLR40dU9nkMJOm5tPm+XHt4o3wio4X\r\n2FqnD57I7qJCWVc00HtpeWno5vHL+eJu0TdxjBuYXnQfwa1z9pWvGaoBtg7tyHgv\r\ng7YZJzF1MW5N9ZqnkdFJVEsCAwEAAQ==\r\n-----END PUBLIC KEY-----', NULL); +(3, 1, 'Key Test 2', 'key', '-----BEGIN PUBLIC KEY-----\nMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCrJ/UoQoN5rO1nwrWBNDr3TgPB\nkm6UmN/B6NY7RXcYTJOFEP6iWqTj9Pw8aT8/DSn2uTMeQK6kWNUAWmRaylQI2QHQ\ndPtrI6piTpjvKm+KbR+n3e4QJ/zOcg06cHYJJiyhPjfC12j3ZxINOV3LDbEKq4s0\nHxMGYZHPu+UezapeeQIDAQAB\n-----END PUBLIC KEY-----', NULL); -- -------------------------------------------------------- diff --git a/backend/test/package-lock.json b/backend/test/package-lock.json index 80731bb..373f47f 100644 --- a/backend/test/package-lock.json +++ b/backend/test/package-lock.json @@ -4,6 +4,11 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=" + }, "axios": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/axios/-/axios-0.18.0.tgz", @@ -43,6 +48,14 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" + }, + "node-rsa": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/node-rsa/-/node-rsa-0.4.2.tgz", + "integrity": "sha1-1jkXKewWqDDtWjgEKzFX0tXXJTA=", + "requires": { + "asn1": "0.2.3" + } } } } diff --git a/backend/test/package.json b/backend/test/package.json index 3a0a721..746fa56 100644 --- a/backend/test/package.json +++ b/backend/test/package.json @@ -4,6 +4,7 @@ "description": "Dependencies for pdnsmanager test", "dependencies": { "axios": "^0.18.0", - "cartesian-product": "^2.1.2" + "cartesian-product": "^2.1.2", + "node-rsa": "^0.4.2" } } diff --git a/backend/test/tests/remote-changekey.js b/backend/test/tests/remote-changekey.js new file mode 100644 index 0000000..32c97a4 --- /dev/null +++ b/backend/test/tests/remote-changekey.js @@ -0,0 +1,102 @@ +const test = require('../testlib'); + +const NodeRSA = require('node-rsa'); + +const privkey = + `-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQCrJ/UoQoN5rO1nwrWBNDr3TgPBkm6UmN/B6NY7RXcYTJOFEP6i +WqTj9Pw8aT8/DSn2uTMeQK6kWNUAWmRaylQI2QHQdPtrI6piTpjvKm+KbR+n3e4Q +J/zOcg06cHYJJiyhPjfC12j3ZxINOV3LDbEKq4s0HxMGYZHPu+UezapeeQIDAQAB +AoGAGGkbgwFxhPIP7gOMJYBQhKMA0CPVV6YyC5LsswlmQfXx+EGDP56T89sl+mu8 +VH7JJGInk0IAZnow7tr1gylmMJ0ir6KfDKZQG95tkFHwCVM3ZqUx/X8VAVuZT2mo +6ckAC7/ZrqORiFCNDC1kWgiaNj7GldvcbNOGUIBOkStgM4ECQQDVLWI/hO0fiPhT +QWVu+4md1NjSv9MZdaIdm+FEVKyTjN/j1fDLNFIguC24veYvsgKf2AyYAJqiAihz +RQWey38RAkEAzYmjjZuKmtsaUknZxmYVJwZlatvHv/3V2REa3UwhVXhgpbBGahav +khH8W5u4JJ/VUpX34wje8g/Gp2M6aCg46QJAGtux8jDMM1ntd4fYwMfeSc1kWAEl +FqMUfsiB9Dr610g7eRgeU2vPISIzWIBMfRvfasYsqAYDdX/yGrvKfnxDEQJAcTUr +aXbPfAXMVKCqm3Vkly8VsyrEtcHZBItAUb156rq3+OrDjfFa2MihR8/YOAv1ElzZ +wSoEqiz4TQABjpcA6QJAX1QXYhHQpjLj4UF+8TkZg93Zmd86W5CN/gXSTFJGrZ8M +3DOyePDIw1omSzyfvYa3Rbl/NL5BxFH6cURg++z8FA== +-----END RSA PRIVATE KEY-----`; +const key = new NodeRSA(privkey, 'pkcs1', { signingScheme: 'pkcs1-sha512' }); + +test.run(async function () { + await test('admin', async function (assert, req) { + // Test updating + var time = Math.floor(new Date() / 1000); + + var res = await req({ + url: '/remote/updatekey', + method: 'post', + data: { + record: 1, + content: 'foobarbaz', + time: time, + signature: key.sign('1foobarbaz' + time, 'base64') + } + }); + + assert.equal(res.status, 204, 'Update should succeed'); + + var res = await req({ + url: '/records/1', + method: 'get' + }); + + assert.equal(res.data.content, 'foobarbaz', 'Updating should change content.'); + + var res = await req({ + url: '/remote/updatekey', + method: 'post', + data: { + record: 1, + content: 'foobarbaz', + time: time, + signature: key.sign('1foobarbazdef' + time, 'base64') + } + }); + + assert.equal(res.status, 403); + + // Test not existing record + var res = await req({ + url: '/remote/updatekey', + method: 'post', + data: { + record: 100, + content: 'foobarbaz', + time: time, + signature: key.sign('1foobarbazdef' + time, 'base64') + } + }); + + assert.equal(res.status, 404, 'Not existing record should trigger error'); + + // Test missing fields + var res = await req({ + url: '/remote/updatekey', + method: 'post', + data: { + record: 100, + signature: key.sign('1foobarbazdef' + time, 'base64') + } + }); + + assert.equal(res.status, 422, 'Missing field should fail'); + + // Test wrong time + var time = Math.floor(new Date() / 1000) - 60; + var res = await req({ + url: '/remote/updatekey', + method: 'post', + data: { + record: 1, + content: 'foobarbaz', + time: time, + signature: key.sign('1foobarbaz' + time, 'base64') + } + }); + + assert.equal(res.status, 403, 'Wrong time should fail'); + }); +}); \ No newline at end of file