Browse Source

Reference Synapse Identity Verification and Lookup Server

matrix.org 9 years ago
commit
2360cd427f

+ 177 - 0
LICENSE

@@ -0,0 +1,177 @@
+
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS

+ 32 - 0
README.rst

@@ -0,0 +1,32 @@
+Installation
+============
+
+Dependencies can be installed using setup.py in the same way as synapse: see synpase/README.rst.
+
+Having installed dependencies, you can run sydent using::
+
+    $ python -m sydent.sydent
+
+This will create a configuration file in sydent.conf with some defaults. You'll most likley want to change the server name and specify a mail relay.
+
+Requests
+========
+
+The requests that synapse servers and clients submit to the identity server are, briefly, as follows:
+
+curl -XPOST 'http://localhost:8001/matrix/identity/api/v1/validate/email/requestToken' -d'email=matthew@arasphere.net&clientSecret=abcd'
+{"success": true, "tokenId": 1}
+
+# receive 943258 by mail
+
+curl -XPOST 'http://localhost:8001/matrix/identity/api/v1/validate/email/submitToken' -d'token=943258&tokenId=1'
+{"success": true}
+
+# lookup:
+
+curl 'http://localhost:8001/matrix/identity/api/v1/lookup?medium=email&address=henry%40matrix.org'
+
+# fetch pubkey key for a server
+
+curl http://localhost:8001/matrix/identity/api/v1/pubkey/ed25519
+

+ 5 - 0
res/email.template

@@ -0,0 +1,5 @@
+Hi!
+
+Your email validation token is {token}.
+
+If you were not expecting this message, it is safe to ignore it.

+ 10 - 0
setup.cfg

@@ -0,0 +1,10 @@
+[build_sphinx]
+source-dir = docs/sphinx
+build-dir  = docs/build
+all_files  = 1
+
+[aliases]
+test = trial
+
+[trial]
+test_suite = tests

+ 51 - 0
setup.py

@@ -0,0 +1,51 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2014 matrix.org
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+from setuptools import setup, find_packages
+
+
+# Utility function to read the README file.
+# Used for the long_description.  It's nice, because now 1) we have a top level
+# README file and 2) it's easier to type in the README file than to put a raw
+# string in below ...
+def read(fname):
+    return open(os.path.join(os.path.dirname(__file__), fname)).read()
+
+setup(
+    name="SynapseIdentityServer",
+    version="0.1",
+    packages=find_packages(exclude=["tests"]),
+    description="Reference Synapse Identity Verification and Lookup Server",
+    install_requires=[
+        "syutil==0.0.1",
+        "Twisted>=14.0.0",
+        "service_identity>=1.0.0",
+        "pyasn1",
+        "pynacl",
+        "daemonize",
+    ],
+    dependency_links=[
+        "git+ssh://git@git.openmarket.com/tng/syutil.git#egg=syutil-0.0.1",
+    ],
+    setup_requires=[
+        "setuptools_trial",
+        "setuptools>=1.0.0", # Needs setuptools that supports git+ssh. It's not obvious when support for this was introduced.
+        "mock"
+    ],
+    include_package_data=True,
+    long_description=read("README"),
+)

+ 0 - 0
sydent/__init__.py


+ 0 - 0
sydent/db/__init__.py


+ 45 - 0
sydent/db/sqlitedb.py

@@ -0,0 +1,45 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2014 matrix.org
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sqlite3
+import logging
+import os
+
+logger = logging.getLogger(__name__)
+
+class SqliteDatabase:
+    def __init__(self, syd):
+        self.sydent = syd
+
+        dbFilePath = self.sydent.cfg.get("db", "db.file")
+        logger.info("Using DB file %s", dbFilePath)
+
+        self.db = sqlite3.connect(dbFilePath)
+
+        schemaDir = os.path.dirname(__file__)
+
+        c = self.db.cursor()
+
+        for f in os.listdir(schemaDir):
+            if not f.endswith(".sql"):
+                continue
+            scriptPath = os.path.join(schemaDir, f)
+            fp = open(scriptPath, 'r')
+            c.executescript(fp.read())
+            fp.close()
+
+        c.close()
+        self.db.commit()

+ 18 - 0
sydent/db/threepid_associations.sql

@@ -0,0 +1,18 @@
+/*
+Copyright 2014 matrix.org
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+CREATE TABLE if not exists threepid_associations (id integer primary key, medium varchar(16) not null, address varchar(256) not null, mxId varchar(256) not null, createdAt bigint not null, expires bigint not null); 
+CREATE UNIQUE INDEX if not exists medium_address on threepid_associations(medium, address);

+ 18 - 0
sydent/db/threepid_validation.sql

@@ -0,0 +1,18 @@
+/*
+Copyright 2014 matrix.org
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+*/
+
+CREATE TABLE IF NOT EXISTS threepid_validation_tokens (id integer primary key, medium varchar(16) not null, address varchar(256) not null, token varchar(32) not null, clientSecret varchar(32) not null, validated int default 0, createdAt bigint not null);
+

+ 41 - 0
sydent/db/threepidtoken.py

@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2014 matrix.org
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from sydent.validators import Token
+
+class ThreePidTokenStore:
+    def __init__(self, syd):
+        self.sydent = syd
+
+    def addToken(self, medium, address, token, clientSecret, createdAt):
+        cur = self.sydent.db.cursor()
+
+        cur.execute("insert into threepid_validation_tokens ('medium', 'address', 'token', 'clientSecret', 'createdAt')"+
+            " values (?, ?, ?, ?, ?)", (medium, address, token, clientSecret, createdAt))
+        self.sydent.db.commit()
+        return cur.lastrowid
+
+    def getTokenById(self, tokId):
+        cur = self.sydent.db.cursor()
+
+        cur.execute("select id, medium, address, token, clientSecret, validated, createdAt from "+
+            "threepid_validation_tokens where id = ?", [tokId])
+        row = cur.fetchone()
+
+        if not row:
+            return None
+
+        return Token(row[0], row[1], row[2], row[3], row[4], row[5], row[6])

+ 0 - 0
sydent/http/__init__.py


+ 71 - 0
sydent/http/httpserver.py

@@ -0,0 +1,71 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2014 matrix.org
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from twisted.web.server import Site
+from twisted.web.resource import Resource
+from twisted.python import log
+
+import logging
+import twisted.internet.reactor
+
+logger = logging.getLogger(__name__)
+
+class HttpServer:
+    def __init__(self, sydent):
+        self.sydent = sydent
+
+        observer = log.PythonLoggingObserver()
+        observer.start()
+
+        root = Resource()
+        matrix = Resource()
+        identity = Resource()
+        api = Resource()
+        v1 = Resource()
+
+        validate = Resource()
+        email = Resource()
+        emailReqCode = self.sydent.servlets.emailRequestCode
+        emailValCode = self.sydent.servlets.emailValidate
+
+        lookup = self.sydent.servlets.lookup
+
+        pubkey = Resource()
+        pk_ed25519 = self.sydent.servlets.pubkey_ed25519
+
+        root.putChild('matrix', matrix)
+        matrix.putChild('identity', identity)
+        identity.putChild('api', api)
+        api.putChild('v1', v1)
+
+        v1.putChild('validate', validate)
+        validate.putChild('email', email)
+
+        v1.putChild('lookup', lookup)
+
+        v1.putChild('pubkey', pubkey)
+        pubkey.putChild('ed25519', pk_ed25519)
+
+        email.putChild('requestToken', emailReqCode)
+        email.putChild('submitToken', emailValCode)
+
+        self.factory = Site(root)
+
+    def run(self):
+        httpPort = int(self.sydent.cfg.get('http', 'http.port'))
+        logger.info("Starting HTTP server on port %d", httpPort)
+        twisted.internet.reactor.listenTCP(httpPort, self.factory)
+        twisted.internet.reactor.run()

+ 0 - 0
sydent/http/servlets/__init__.py


+ 97 - 0
sydent/http/servlets/emailservlet.py

@@ -0,0 +1,97 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2014 matrix.org
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from twisted.web.resource import Resource
+
+from sydent.validators.emailvalidator import EmailAddressException, EmailSendException
+
+import json
+
+
+def send_cors(request):
+    request.setHeader(b"Content-Type", b"application/json")
+    request.setHeader("Access-Control-Allow-Origin", "*")
+    request.setHeader("Access-Control-Allow-Methods",
+                      "GET, POST, PUT, DELETE, OPTIONS")
+    request.setHeader("Access-Control-Allow-Headers",
+                      "Origin, X-Requested-With, Content-Type, Accept")
+
+
+class EmailRequestCodeServlet(Resource):
+    isLeaf = True
+
+    def __init__(self, syd):
+        self.sydent = syd
+
+    def render_POST(self, request):
+        send_cors(request)
+        if 'email' not in request.args or 'clientSecret' not in request.args:
+            request.setResponseCode(400)
+            resp = {'error': 'badrequest', 'message': "'email' and 'clientSecret' fields are required"}
+            return json.dumps(resp)
+
+        email = request.args['email'][0]
+        clientSecret = request.args['clientSecret'][0]
+
+        resp = None
+
+        try:
+            tokenId = self.sydent.validators.email.requestToken(email, clientSecret)
+        except EmailAddressException:
+            request.setResponseCode(400)
+            resp = {'error': 'email_invalid'}
+        except EmailSendException:
+            request.setResponseCode(500)
+            resp = {'error': 'send_error'}
+
+        if not resp:
+            resp = {'success':True, 'tokenId':tokenId}
+
+        return json.dumps(resp).encode("UTF-8")
+
+    def render_OPTIONS(self, request):
+        send_cors(request)
+        request.setResponseCode(200)
+        return "{}".encode("UTF-8")
+
+class EmailValidateCodeServlet(Resource):
+    isLeaf = True
+
+    def __init__(self, syd):
+        self.sydent = syd
+
+    def render_POST(self, request):
+        send_cors(request)
+        if 'tokenId' not in request.args or 'token' not in request.args or 'mxId' not in request.args:
+            request.setResponseCode(400)
+            resp = {'error': 'badrequest', 'message': "'tokenId', 'token' and 'mxId' fields are required"}
+            return json.dumps(resp)
+
+        tokenId = request.args['tokenId'][0]
+        tokenString = request.args['token'][0]
+        mxId = request.args['mxId'][0]
+
+        sgassoc = self.sydent.validators.email.validateToken(tokenId, None, tokenString, mxId)
+
+        if not sgassoc:
+            sgassoc = {'success':False}
+
+        return json.dumps(sgassoc).encode("UTF-8")
+
+    def render_OPTIONS(self, request):
+        send_cors(request)
+        request.setResponseCode(200)
+        return "{}".encode("UTF-8")

+ 52 - 0
sydent/http/servlets/lookupservlet.py

@@ -0,0 +1,52 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2014 matrix.org
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from twisted.web.resource import Resource
+
+import json
+
+from sydent.util import validationutils
+
+class LookupServlet(Resource):
+    isLeaf = True
+
+    def __init__(self, syd):
+        self.sydent = syd
+
+    def render_GET(self, request):
+        if not 'medium' in request.args or not 'address' in request.args:
+            request.setResponseCode(400)
+            resp = {'error': 'badrequest', 'message': "'medium' and 'address' fields are required"}
+            return json.dumps(resp)
+
+        medium = request.args['medium'][0]
+        address = request.args['address'][0]
+
+        cur = self.sydent.db.cursor()
+
+        # sqlite's support for upserts is atrocious but this is temporary anyway
+        res = cur.execute("select mxId,createdAt,expires from threepid_associations "+
+                    "where medium = ? and address = ?", (medium, address))
+        row = res.fetchone()
+        if not row:
+            return json.dumps({})
+
+        mxId = row[0]
+        created = row[1]
+        expires = row[2]
+
+        sgassoc = validationutils.signedThreePidAssociation(self.sydent, medium, address, mxId, created, expires)
+        return json.dumps(sgassoc)

+ 32 - 0
sydent/http/servlets/pubkeyservlets.py

@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2014 matrix.org
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from twisted.web.resource import Resource
+
+import json
+import nacl.encoding
+
+class Ed25519Servlet(Resource):
+    isLeaf = True
+
+    def __init__(self, syd):
+        self.sydent = syd
+
+    def render_GET(self, request):
+        pubKey = self.sydent.signers.ed25519.signing_key.verify_key
+        pubKeyHex = pubKey.encode(encoder=nacl.encoding.HexEncoder)
+
+        return json.dumps({'public_key':pubKeyHex})

+ 0 - 0
sydent/sign/__init__.py


+ 39 - 0
sydent/sign/ed25519.py

@@ -0,0 +1,39 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2014 matrix.org
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import nacl.encoding
+import nacl.signing
+import nacl.exceptions
+
+import logging
+
+logger = logging.getLogger(__name__)
+
+class SydentEd25519:
+    def __init__(self, syd):
+        self.sydent = syd
+
+        skHex = self.sydent.cfg.get('crypto', 'ed25519.signingkey')
+        if skHex != '':
+            self.signing_key = nacl.signing.SigningKey(skHex, encoder=nacl.encoding.HexEncoder)
+        else:
+            logger.info("This server does not yet have an ed25519 signing key. "+
+                        "Creating one and saving it in the config file.")
+
+            self.signing_key = nacl.signing.SigningKey.generate()
+            skHex = self.signing_key.encode(encoder=nacl.encoding.HexEncoder)
+            self.sydent.cfg.set('crypto', 'ed25519.signingkey', skHex)
+            self.sydent.save_config()

+ 115 - 0
sydent/sydent.py

@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2014 matrix.org
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+#from twisted.enterprise import adbapi
+
+import ConfigParser
+import logging
+import os
+
+from db.sqlitedb import SqliteDatabase
+
+from http.httpserver import HttpServer
+from validators.emailvalidator import EmailValidator
+
+from sign.ed25519 import SydentEd25519
+
+from http.servlets.emailservlet import EmailRequestCodeServlet, EmailValidateCodeServlet
+from http.servlets.lookupservlet import LookupServlet
+from http.servlets.pubkeyservlets import Ed25519Servlet
+
+logger = logging.getLogger(__name__)
+
+class Sydent:
+    CONFIG_SECTIONS = ['general', 'db', 'http', 'email', 'crypto']
+    CONFIG_DEFAULTS = {
+        'server.name' : '',
+        'db.file': 'sydent.sqlite',
+        'token.length': '6',
+        'http.port': '8090',
+        'email.template': 'res/email.template',
+        'email.from': 'Sydent Validation <noreply@{hostname}>',
+        'email.subject': 'Your Validation Token',
+        'email.smtphost': 'localhost',
+        'log.path':'',
+        'ed25519.signingkey':''
+    }
+
+    def __init__(self):
+        logger.info("Starting Sydent server")
+        self.parse_config()
+
+        logPath = self.cfg.get('general', "log.path")
+        if logPath != '':
+            logging.basicConfig(level=logging.INFO, filename=logPath)
+        else:
+            logging.basicConfig(level=logging.INFO, filename=logPath)
+
+
+        self.db = SqliteDatabase(self).db
+
+        self.server_name = self.cfg.get('general', 'server.name')
+        if self.server_name == '':
+            self.server_name = os.uname()[1]
+            logger.warn(("You had not specified a server name. I have guessed that this server is called '%s' "
+                        +" and saved this in the config file. If this is incorrect, you should edit server.name in "
+                        +"the config file.") % (self.server_name))
+            self.cfg.set('general', 'server.name', self.server_name)
+            self.save_config()
+
+        self.validators = Validators()
+        self.validators.email = EmailValidator(self)
+
+        self.keyring = Keyring()
+        self.keyring.ed25519 = SydentEd25519(self).signing_key
+
+        self.servlets = Servlets()
+        self.servlets.emailRequestCode = EmailRequestCodeServlet(self)
+        self.servlets.emailValidate = EmailValidateCodeServlet(self)
+        self.servlets.lookup = LookupServlet(self)
+        self.servlets.pubkey_ed25519 = Ed25519Servlet(self)
+
+        self.httpServer = HttpServer(self)
+
+    def parse_config(self):
+        self.cfg = ConfigParser.SafeConfigParser(Sydent.CONFIG_DEFAULTS)
+        for sect in Sydent.CONFIG_SECTIONS:
+            try:
+                self.cfg.add_section(sect)
+            except ConfigParser.DuplicateSectionError:
+                pass
+        self.cfg.read("sydent.conf")
+
+    def save_config(self):
+        fp = open("sydent.conf", 'w')
+        self.cfg.write(fp)
+        fp.close()
+
+    def run(self):
+        self.httpServer.run()
+
+class Validators:
+    pass
+
+class Servlets:
+    pass
+
+class Keyring:
+    pass
+
+if __name__ == '__main__':
+    syd = Sydent()
+    syd.run()

+ 0 - 0
sydent/util/__init__.py


+ 21 - 0
sydent/util/tokenutils.py

@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2014 matrix.org
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import string
+import random
+
+def generateNumericTokenOfLength(length):
+    return "".join([random.choice(string.digits) for _ in range(length)])

+ 35 - 0
sydent/util/validationutils.py

@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2014 matrix.org
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import syutil.crypto.jsonsign
+
+
+def signedThreePidAssociation(sydent, medium, address, mxId, not_before, not_after):
+    sgassoc = { 'medium' : medium,
+                'address' : address,
+                'mxid' : mxId,
+                'not_before':not_before,
+                'not_after':not_after
+              }
+    sgassoc = syutil.jsonsign.sign_json(sgassoc, sydent.server_name, sydent.keyring.ed25519)
+    return sgassoc
+
+
+def verifyThreePidAssociationFromHere(sydent, sgassoc):
+    """
+    Verifies that this association signature is from *this* server
+    """
+    syutil.jsonsign.verify_signed_json(sgassoc, sydent.server_name, sydent.keyring.ed25519)

+ 26 - 0
sydent/validators/__init__.py

@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2014 matrix.org
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+class Token:
+    def __init__(self, _tokenId, _medium, _address, _tokenString, _clientSecret, _validated, _createdAt):
+        self.tokenId = _tokenId
+        self.medium = _medium
+        self.address = _address
+        self.tokenString = _tokenString
+        self.clientSecret = _clientSecret
+        self.validated = _validated
+        self.createdAt = _createdAt

+ 123 - 0
sydent/validators/emailvalidator.py

@@ -0,0 +1,123 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2014 matrix.org
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sydent.util.tokenutils
+import smtplib
+import os
+import email.utils
+import logging
+import twisted.python.log
+import time
+
+from email.mime.text import MIMEText
+from email.mime.multipart import MIMEMultipart
+
+from sydent.db.threepidtoken import ThreePidTokenStore
+
+from sydent.util import validationutils
+
+
+logger = logging.getLogger(__name__)
+
+class EmailValidator:
+    THREEPID_ASSOCIATION_LIFETIME_MS = 100 * 365 * 24 * 60 * 60 * 1000
+
+    def __init__(self, sydent):
+        self.sydent = sydent
+
+    def requestToken(self, emailAddress, clientSecret):
+        tokenString = sydent.util.tokenutils.generateNumericTokenOfLength(
+            int(self.sydent.cfg.get('email', 'token.length')))
+
+        myHostname = os.uname()[1]
+
+        mailFrom = self.sydent.cfg.get('email', 'email.from').format(hostname=myHostname)
+        mailTo = emailAddress
+
+        mailTemplateFile = self.sydent.cfg.get('email', 'email.template')
+
+        mailString = open(mailTemplateFile).read().format(token=tokenString)
+
+        msg = MIMEMultipart('alternative')
+        msg['Subject'] = self.sydent.cfg.get('email', 'email.subject')
+        msg['From'] = mailFrom
+        msg['To'] = mailTo
+
+        plainPart = MIMEText(mailString)
+        msg.attach(plainPart)
+
+        rawFrom = email.utils.parseaddr(mailFrom)[1]
+        rawTo = email.utils.parseaddr(mailTo)[1]
+
+        if rawFrom == '' or rawTo == '':
+            logger.info("Couldn't parse from / to address %s / %s", mailFrom, mailTo)
+            raise EmailAddressException()
+
+        mailServer = self.sydent.cfg.get('email', 'email.smtphost')
+
+        logger.info("Attempting to mail code %s to %s using mail server %s", tokenString, rawTo, mailServer)
+
+        try:
+            smtp = smtplib.SMTP(mailServer)
+            smtp.sendmail(rawFrom, rawTo, msg.as_string())
+            smtp.quit()
+        except Exception as origException:
+            twisted.python.log.err()
+            ese = EmailSendException()
+            ese.cause = origException
+            raise ese
+
+        threePidStore = ThreePidTokenStore(self.sydent)
+        createdMs = int(time.time() * 1000.0)
+        tokenId = threePidStore.addToken('email', rawTo, tokenString, clientSecret, createdMs)
+
+        return tokenId
+
+    def validateToken(self, tokenId, clientSecret, token, mxId):
+        """
+        XXX: This also binds the validated 3pid to an mxId so the structure needs a rethink.
+        """
+        threePidStore = ThreePidTokenStore(self.sydent)
+        tokenObj = threePidStore.getTokenById(tokenId)
+        if not tokenObj:
+            return False
+
+        # TODO once we can validate the token oob
+        #if tokenObj.validated and clientSecret == tokenObj.clientSecret:
+        #    return True
+
+        if tokenObj.tokenString == token:
+            createdAt = int(time.time() * 1000.0)
+            expires = createdAt + EmailValidator.THREEPID_ASSOCIATION_LIFETIME_MS
+
+            cur = self.sydent.db.cursor()
+
+            # sqlite's support for upserts is atrocious but this is temporary anyway
+            cur.execute("insert or replace into threepid_associations ('medium', 'address', 'mxId', 'createdAt', 'expires')"+
+                " values (?, ?, ?, ?, ?)",
+                (tokenObj.medium, tokenObj.address, mxId, createdAt, expires))
+            self.sydent.db.commit()
+
+            return validationutils.signedThreePidAssociation(self.sydent, tokenObj.medium, tokenObj.address,
+                                                             createdAt, expires, mxId)
+        else:
+            return False
+
+class EmailAddressException(Exception):
+    pass
+
+class EmailSendException(Exception):
+    pass