summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.rst4
-rw-r--r--UPGRADE.rst5
-rw-r--r--WISHLIST.rst9
-rw-r--r--docs/server-server/signing.rst151
-rw-r--r--scripts/basic.css510
-rwxr-xr-xscripts/gendoc.sh14
-rw-r--r--scripts/nature.css270
-rw-r--r--synapse/api/errors.py1
-rw-r--r--synapse/api/events/factory.py4
-rw-r--r--synapse/api/urls.py3
-rwxr-xr-xsynapse/app/homeserver.py10
-rw-r--r--synapse/config/server.py39
-rw-r--r--synapse/crypto/keyclient.py75
-rw-r--r--synapse/crypto/keyring.py154
-rw-r--r--synapse/crypto/keyserver.py111
-rw-r--r--synapse/crypto/resource/__init__.py15
-rw-r--r--synapse/crypto/resource/key.py161
-rw-r--r--synapse/federation/__init__.py1
-rw-r--r--synapse/federation/pdu_codec.py4
-rw-r--r--synapse/federation/persistence.py2
-rw-r--r--synapse/federation/replication.py22
-rw-r--r--synapse/federation/transport.py139
-rw-r--r--synapse/federation/units.py27
-rw-r--r--synapse/handlers/message.py16
-rw-r--r--synapse/http/client.py88
-rw-r--r--synapse/http/server_key_resource.py89
-rw-r--r--synapse/rest/presence.py4
-rw-r--r--synapse/server.py6
-rw-r--r--synapse/storage/__init__.py3
-rw-r--r--synapse/storage/_base.py14
-rw-r--r--synapse/storage/keys.py77
-rw-r--r--synapse/storage/schema/keys.sql13
-rw-r--r--synapse/storage/transactions.py13
-rw-r--r--tests/federation/test_federation.py22
-rw-r--r--tests/federation/test_pdu_codec.py4
-rw-r--r--tests/handlers/test_federation.py13
-rw-r--r--tests/handlers/test_presence.py55
-rw-r--r--tests/handlers/test_room.py5
-rw-r--r--tests/handlers/test_typing.py14
-rw-r--r--tests/rest/test_presence.py15
-rw-r--r--tests/test_state.py2
-rw-r--r--tests/utils.py22
-rw-r--r--webclient/app-filter.js57
-rw-r--r--webclient/room/room.html4
44 files changed, 932 insertions, 1335 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index 1690490f66..5b05900daf 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -1,3 +1,7 @@
+Changes in latest
+=================
+This breaks federation becuase of signing
+
 Changes in synapse 0.3.4 (2014-09-25)
 =====================================
 This version adds support for using a TURN server. See docs/turn-howto.rst on
diff --git a/UPGRADE.rst b/UPGRADE.rst
index 713fb9ae83..2ae9254ecf 100644
--- a/UPGRADE.rst
+++ b/UPGRADE.rst
@@ -1,3 +1,8 @@
+Upgrading to latest
+===================
+This breaks federation between old and new servers due to signing of
+transactions.
+
 Upgrading to v0.3.0
 ===================
 
diff --git a/WISHLIST.rst b/WISHLIST.rst
deleted file mode 100644
index a0713f1966..0000000000
--- a/WISHLIST.rst
+++ /dev/null
@@ -1,9 +0,0 @@
-Broad-sweeping stuff which would be nice to have
-================================================
-
- - Additional SQL backends beyond sqlite
- - homeserver implementation in go
- - homeserver implementation in node.js
- - client SDKs
- - libpurple library
- - irssi plugin?
diff --git a/docs/server-server/signing.rst b/docs/server-server/signing.rst
new file mode 100644
index 0000000000..dae10f121b
--- /dev/null
+++ b/docs/server-server/signing.rst
@@ -0,0 +1,151 @@
+Signing JSON
+============
+
+JSON is signed by encoding the JSON object without ``signatures`` or ``meta``
+keys using a canonical encoding. The JSON bytes are then signed using the
+signature algorithm and the signature encoded using base64 with the padding
+stripped. The resulting base64 signature is added to an object under the
+*signing key identifier* which is added to the ``signatures`` object under the
+name of the server signing it which is added back to the original JSON object
+along with the ``meta`` object.
+
+The *signing key identifier* is the concatenation of the *signing algorithm*
+and a *key version*. The *signing algorithm* identifies the algorithm used to
+sign the JSON. The currently support value for *signing algorithm* is
+``ed25519`` as implemented by NACL (http://nacl.cr.yp.to/). The *key version*
+is used to distinguish between different signing keys used by the same entity.
+
+The ``meta`` object and the ``signatures`` object are not covered by the
+signature. Therefore intermediate servers can add metadata such as time stamps
+and additional signatures.
+
+
+::
+
+  {
+     "name": "example.org",
+     "signing_keys": {
+       "ed25519:1": "XSl0kuyvrXNj6A+7/tkrB9sxSbRi08Of5uRhxOqZtEQ"
+     },
+     "meta": {
+        "retrieved_ts_ms": 922834800000
+     },
+     "signatures": {
+        "example.org": {
+           "ed25519:1": "s76RUgajp8w172am0zQb/iPTHsRnb4SkrzGoeCOSFfcBY2V/1c8QfrmdXHpvnc2jK5BD1WiJIxiMW95fMjK7Bw"
+        }
+     }
+  }
+
+::
+
+  def sign_json(json_object, signing_key, signing_name):
+      signatures = json_object.pop("signatures", {})
+      meta = json_object.pop("meta", None)
+
+      signed = signing_key.sign(encode_canonical_json(json_object))
+      signature_base64 = encode_base64(signed.signature)
+
+      key_id = "%s:%s" % (signing_key.alg, signing_key.version)
+      signatures.setdefault(sigature_name, {})[key_id] = signature_base64
+
+      json_object["signatures"] = signatures
+      if meta is not None:
+          json_object["meta"] = meta
+
+      return json_object
+
+Checking for a Signature
+------------------------
+
+To check if an entity has signed a JSON object a server does the following
+
+1. Checks if the ``signatures`` object contains an entry with the name of the
+   entity. If the entry is missing then the check fails.
+2. Removes any *signing key identifiers* from the entry with algorithms it
+   doesn't understand. If there are no *signing key identifiers* left then the
+   check fails.
+3. Looks up *verification keys* for the remaining *signing key identifiers*
+   either from a local cache or by consulting a trusted key server. If it
+   cannot find a *verification key* then the check fails.
+4. Decodes the base64 encoded signature bytes. If base64 decoding fails then
+   the check fails.
+5. Checks the signature bytes using the *verification key*. If this fails then
+   the check fails. Otherwise the check succeeds.
+
+Canonical JSON
+--------------
+
+The canonical JSON encoding for a value is the shortest UTF-8 JSON encoding
+with dictionary keys lexicographically sorted by unicode codepoint. Numbers in
+the JSON value must be integers in the range [-(2**53)+1, (2**53)-1].
+
+::
+
+ import json
+
+ def canonical_json(value):
+     return json.dumps(
+         value,
+         ensure_ascii=False,
+         separators=(',',':'),
+         sort_keys=True,
+     ).encode("UTF-8")
+
+Grammar
++++++++
+
+Adapted from the grammar in http://tools.ietf.org/html/rfc7159 removing
+insignificant whitespace, fractions, exponents and redundant character escapes
+
+::
+
+ value     = false / null / true / object / array / number / string
+ false     = %x66.61.6c.73.65
+ null      = %x6e.75.6c.6c
+ true      = %x74.72.75.65
+ object    = %x7B [ member *( %x2C member ) ] %7D
+ member    = string %x3A value
+ array     = %x5B [ value *( %x2C value ) ] %5B
+ number    = [ %x2D ] int
+ int       = %x30 / ( %x31-39 *digit )
+ digit     = %x30-39
+ string    = %x22 *char %x22
+ char      = unescaped / %x5C escaped
+ unescaped = %x20-21 / %x23-5B / %x5D-10FFFF
+ escaped   = %x22 ; "    quotation mark  U+0022
+           / %x5C ; \    reverse solidus U+005C
+           / %x62 ; b    backspace       U+0008
+           / %x66 ; f    form feed       U+000C
+           / %x6E ; n    line feed       U+000A
+           / %x72 ; r    carriage return U+000D
+           / %x74 ; t    tab             U+0009
+           / %x75.30.30.30 (%x30-37 / %x62 / %x65-66) ; u000X
+           / %x75.30.30.31 (%x30-39 / %x61-66)        ; u001X
+
+Signing Events
+==============
+
+Signing events is a more complicated process since servers can choose to redact
+non-essential event contents. Before signing the event it is encoded as
+Canonical JSON and hashed using SHA-256. The resulting hash is then stored
+in the event JSON in a ``hash`` object under a ``sha256`` key. Then all
+non-essential keys are stripped from the event object, and the resulting object
+which included the ``hash`` key is signed using the JSON signing algorithm.
+
+Servers can then transmit the entire event or the event with the non-essential
+keys removed. Receiving servers can then check the entire event if it is
+present by computing the SHA-256 of the event excluding the ``hash`` object, or
+by using the ``hash`` object included in the event if keys have been redacted.
+
+New hash functions can be introduced by adding additional keys to the ``hash``
+object. Since the ``hash`` object cannot be redacted a server shouldn't allow
+too many hashes to be listed, otherwise a server might embed illict data within
+the ``hash`` object. For similar reasons a server shouldn't allow hash values
+that are too long.
+
+[[TODO(markjh): We might want to specify a maximum number of keys for the
+``hash`` and we might want to specify the maximum output size of a hash]]
+
+[[TODO(markjh) We might want to allow the server to omit the output of well
+known hash functions like SHA-256 when none of the keys have been redacted]]
diff --git a/scripts/basic.css b/scripts/basic.css
deleted file mode 100644
index 6411570ee6..0000000000
--- a/scripts/basic.css
+++ /dev/null
@@ -1,510 +0,0 @@
-/*
- * basic.css
- * ~~~~~~~~~
- *
- * Sphinx stylesheet -- basic theme.
- *
- * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
- * :license: BSD, see LICENSE for details.
- *
- */
-
-/* -- main layout ----------------------------------------------------------- */
-
-div.clearer {
-    clear: both;
-}
-
-/* -- relbar ---------------------------------------------------------------- */
-
-div.related {
-    width: 100%;
-    font-size: 90%;
-}
-
-div.related h3 {
-    display: none;
-}
-
-div.related ul {
-    margin: 0;
-    padding: 0 0 0 10px;
-    list-style: none;
-}
-
-div.related li {
-    display: inline;
-}
-
-div.related li.right {
-    float: right;
-    margin-right: 5px;
-}
-
-/* -- sidebar --------------------------------------------------------------- */
-
-div.sphinxsidebarwrapper {
-    padding: 10px 5px 0 10px;
-}
-
-div.sphinxsidebar {
-    float: left;
-    width: 230px;
-    margin-left: -100%;
-    font-size: 90%;
-}
-
-div.sphinxsidebar ul {
-    list-style: none;
-}
-
-div.sphinxsidebar ul ul,
-div.sphinxsidebar ul.want-points {
-    margin-left: 20px;
-    list-style: square;
-}
-
-div.sphinxsidebar ul ul {
-    margin-top: 0;
-    margin-bottom: 0;
-}
-
-div.sphinxsidebar form {
-    margin-top: 10px;
-}
-
-div.sphinxsidebar input {
-    border: 1px solid #98dbcc;
-    font-family: sans-serif;
-    font-size: 1em;
-}
-
-img {
-    border: 0;
-}
-
-/* -- search page ----------------------------------------------------------- */
-
-ul.search {
-    margin: 10px 0 0 20px;
-    padding: 0;
-}
-
-ul.search li {
-    padding: 5px 0 5px 20px;
-    background-image: url(file.png);
-    background-repeat: no-repeat;
-    background-position: 0 7px;
-}
-
-ul.search li a {
-    font-weight: bold;
-}
-
-ul.search li div.context {
-    color: #888;
-    margin: 2px 0 0 30px;
-    text-align: left;
-}
-
-ul.keywordmatches li.goodmatch a {
-    font-weight: bold;
-}
-
-/* -- index page ------------------------------------------------------------ */
-
-table.contentstable {
-    width: 90%;
-}
-
-table.contentstable p.biglink {
-    line-height: 150%;
-}
-
-a.biglink {
-    font-size: 1.3em;
-}
-
-span.linkdescr {
-    font-style: italic;
-    padding-top: 5px;
-    font-size: 90%;
-}
-
-/* -- general index --------------------------------------------------------- */
-
-table.indextable {
-    width: 100%;
-}
-
-table.indextable td {
-    text-align: left;
-    vertical-align: top;
-}
-
-table.indextable dl, table.indextable dd {
-    margin-top: 0;
-    margin-bottom: 0;
-}
-
-table.indextable tr.pcap {
-    height: 10px;
-}
-
-table.indextable tr.cap {
-    margin-top: 10px;
-    background-color: #f2f2f2;
-}
-
-img.toggler {
-    margin-right: 3px;
-    margin-top: 3px;
-    cursor: pointer;
-}
-
-div.modindex-jumpbox {
-    border-top: 1px solid #ddd;
-    border-bottom: 1px solid #ddd;
-    margin: 1em 0 1em 0;
-    padding: 0.4em;
-}
-
-div.genindex-jumpbox {
-    border-top: 1px solid #ddd;
-    border-bottom: 1px solid #ddd;
-    margin: 1em 0 1em 0;
-    padding: 0.4em;
-}
-
-/* -- general body styles --------------------------------------------------- */
-
-a.headerlink {
-    visibility: hidden;
-}
-
-h1:hover > a.headerlink,
-h2:hover > a.headerlink,
-h3:hover > a.headerlink,
-h4:hover > a.headerlink,
-h5:hover > a.headerlink,
-h6:hover > a.headerlink,
-dt:hover > a.headerlink {
-    visibility: visible;
-}
-
-div.document p.caption {
-    text-align: inherit;
-}
-
-div.document td {
-    text-align: left;
-}
-
-.field-list ul {
-    padding-left: 1em;
-}
-
-.first {
-    margin-top: 0 !important;
-}
-
-p.rubric {
-    margin-top: 30px;
-    font-weight: bold;
-}
-
-.align-left {
-    text-align: left;
-}
-
-.align-center {
-    clear: both;
-    text-align: center;
-}
-
-.align-right {
-    text-align: right;
-}
-
-/* -- sidebars -------------------------------------------------------------- */
-
-div.sidebar {
-    margin: 0 0 0.5em 1em;
-    border: 1px solid #ddb;
-    padding: 7px 7px 0 7px;
-    background-color: #ffe;
-    width: 40%;
-    float: right;
-}
-
-p.sidebar-title {
-    font-weight: bold;
-}
-
-/* -- topics ---------------------------------------------------------------- */
-
-div.topic {
-    border: 1px solid #ccc;
-    padding: 7px 7px 0 7px;
-    margin: 10px 0 10px 0;
-}
-
-p.topic-title {
-    font-size: 1.1em;
-    font-weight: bold;
-    margin-top: 10px;
-}
-
-/* -- admonitions ----------------------------------------------------------- */
-
-div.admonition {
-    margin-top: 10px;
-    margin-bottom: 10px;
-    padding: 7px;
-}
-
-div.admonition dt {
-    font-weight: bold;
-}
-
-div.admonition dl {
-    margin-bottom: 0;
-}
-
-p.admonition-title {
-    margin: 0px 10px 5px 0px;
-    font-weight: bold;
-}
-
-div.document p.centered {
-    text-align: center;
-    margin-top: 25px;
-}
-
-/* -- tables ---------------------------------------------------------------- */
-
-table.docutils {
-    border: 0;
-    border-collapse: collapse;
-}
-
-table.docutils td, table.docutils th {
-    padding: 1px 8px 1px 5px;
-    border-top: 0;
-    border-left: 0;
-    border-right: 0;
-    border-bottom: 1px solid #aaa;
-}
-
-table.field-list td, table.field-list th {
-    border: 0 !important;
-}
-
-table.footnote td, table.footnote th {
-    border: 0 !important;
-}
-
-th {
-    text-align: left;
-    padding-right: 5px;
-}
-
-table.citation {
-    border-left: solid 1px gray;
-    margin-left: 1px;
-}
-
-table.citation td {
-    border-bottom: none;
-}
-
-/* -- other body styles ----------------------------------------------------- */
-
-ol.arabic {
-    list-style: decimal;
-}
-
-ol.loweralpha {
-    list-style: lower-alpha;
-}
-
-ol.upperalpha {
-    list-style: upper-alpha;
-}
-
-ol.lowerroman {
-    list-style: lower-roman;
-}
-
-ol.upperroman {
-    list-style: upper-roman;
-}
-
-dl {
-    margin-bottom: 15px;
-}
-
-dd p {
-    margin-top: 0px;
-}
-
-dd ul, dd table {
-    margin-bottom: 10px;
-}
-
-dd {
-    margin-top: 3px;
-    margin-bottom: 10px;
-    margin-left: 30px;
-}
-
-dt:target, .highlighted {
-    background-color: #fbe54e;
-}
-
-dl.glossary dt {
-    font-weight: bold;
-    font-size: 1.1em;
-}
-
-.field-list ul {
-    margin: 0;
-    padding-left: 1em;
-}
-
-.field-list p {
-    margin: 0;
-}
-
-.refcount {
-    color: #060;
-}
-
-.optional {
-    font-size: 1.3em;
-}
-
-.versionmodified {
-    font-style: italic;
-}
-
-.system-message {
-    background-color: #fda;
-    padding: 5px;
-    border: 3px solid red;
-}
-
-.footnote:target  {
-    background-color: #ffa
-}
-
-.line-block {
-    display: block;
-    margin-top: 1em;
-    margin-bottom: 1em;
-}
-
-.line-block .line-block {
-    margin-top: 0;
-    margin-bottom: 0;
-    margin-left: 1.5em;
-}
-
-.guilabel, .menuselection {
-    font-family: sans-serif;
-}
-
-.accelerator {
-    text-decoration: underline;
-}
-
-.classifier {
-    font-style: oblique;
-}
-
-/* -- code displays --------------------------------------------------------- */
-
-pre {
-    overflow: auto;
-}
-
-td.linenos pre {
-    padding: 5px 0px;
-    border: 0;
-    background-color: transparent;
-    color: #aaa;
-}
-
-table.highlighttable {
-    margin-left: 0.5em;
-}
-
-table.highlighttable td {
-    padding: 0 0.5em 0 0.5em;
-}
-
-tt.descname {
-    background-color: transparent;
-    font-weight: bold;
-    font-size: 1.2em;
-}
-
-tt.descclassname {
-    background-color: transparent;
-}
-
-tt.xref, a tt {
-    background-color: transparent;
-    font-weight: bold;
-}
-
-h1 tt, h2 tt, h3 tt, h4 tt, h5 tt, h6 tt {
-    background-color: transparent;
-}
-
-.viewcode-link {
-    float: right;
-}
-
-.viewcode-back {
-    float: right;
-    font-family: sans-serif;
-}
-
-div.viewcode-block:target {
-    margin: -1px -10px;
-    padding: 0 10px;
-}
-
-/* -- math display ---------------------------------------------------------- */
-
-img.math {
-    vertical-align: middle;
-}
-
-div.document div.math p {
-    text-align: center;
-}
-
-span.eqno {
-    float: right;
-}
-
-/* -- printout stylesheet --------------------------------------------------- */
-
-@media print {
-    div.document,
-    div.documentwrapper,
-    div.bodywrapper {
-        margin: 0 !important;
-        width: 100%;
-    }
-
-    div.sphinxsidebar,
-    div.related,
-    div.footer,
-    #top-link {
-        display: none;
-    }
-}
-
diff --git a/scripts/gendoc.sh b/scripts/gendoc.sh
deleted file mode 100755
index 64aff3155e..0000000000
--- a/scripts/gendoc.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/bin/bash
-
-MATRIXDOTORG=$HOME/workspace/matrix.org
-
-rst2html-2.7.py --stylesheet=basic.css,nature.css ../docs/specification.rst > $MATRIXDOTORG/docs/spec/index.html
-rst2html-2.7.py --stylesheet=basic.css,nature.css ../docs/client-server/howto.rst > $MATRIXDOTORG/docs/howtos/client-server.html
-
-perl -pi -e 's#<head>#<head><link rel="stylesheet" href="/site.css">#' $MATRIXDOTORG/docs/spec/index.html $MATRIXDOTORG/docs/howtos/client-server.html
-
-perl -pi -e 's#<body>#<body><div id="header"><div id="headerContent">&nbsp;</div></div><div id="page"><div id="wrapper"><div style="text-align: center; padding: 40px;"><a href="/"><img src="/matrix.png" width="305" height="130" alt="[matrix]"/></a></div>#' $MATRIXDOTORG/docs/spec/index.html $MATRIXDOTORG/docs/howtos/client-server.html
-
-perl -pi -e 's#</body>#</div></div><div id="footer"><div id="footerContent">&copy 2014 Matrix.org</div></div></body>#' $MATRIXDOTORG/docs/spec/index.html $MATRIXDOTORG/docs/howtos/client-server.html
-
-scp -r $MATRIXDOTORG/docs matrix@ldc-prd-matrix-001:/sites/matrix
\ No newline at end of file
diff --git a/scripts/nature.css b/scripts/nature.css
deleted file mode 100644
index b8147f10ee..0000000000
--- a/scripts/nature.css
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
- * nature.css_t
- * ~~~~~~~~~~~~
- *
- * Sphinx stylesheet -- nature theme.
- *
- * :copyright: Copyright 2007-2010 by the Sphinx team, see AUTHORS.
- * :license: BSD, see LICENSE for details.
- *
- */
- 
-/* -- page layout ----------------------------------------------------------- */
- 
-body {
-    font-family: Arial, sans-serif;
-    font-size: 100%;
-    /*background-color: #111;*/
-    color: #555;
-    margin: 0;
-    padding: 0;
-}
-
-div.documentwrapper {
-    float: left;
-    width: 100%;
-}
-
-div.bodywrapper {
-    margin: 0 0 0 230px;
-}
-
-hr {
-    border: 1px solid #B1B4B6;
-}
- 
-/*
-div.document {
-    background-color: #eee;
-}
-*/
- 
-div.document {
-    background-color: #ffffff;
-    color: #3E4349;
-    padding: 0 30px 30px 30px;
-    font-size: 0.9em;
-}
- 
-div.footer {
-    color: #555;
-    width: 100%;
-    padding: 13px 0;
-    text-align: center;
-    font-size: 75%;
-}
- 
-div.footer a {
-    color: #444;
-    text-decoration: underline;
-}
- 
-div.related {
-    background-color: #6BA81E;
-    line-height: 32px;
-    color: #fff;
-    text-shadow: 0px 1px 0 #444;
-    font-size: 0.9em;
-}
- 
-div.related a {
-    color: #E2F3CC;
-}
- 
-div.sphinxsidebar {
-    font-size: 0.75em;
-    line-height: 1.5em;
-}
-
-div.sphinxsidebarwrapper{
-    padding: 20px 0;
-}
- 
-div.sphinxsidebar h3,
-div.sphinxsidebar h4 {
-    font-family: Arial, sans-serif;
-    color: #222;
-    font-size: 1.2em;
-    font-weight: normal;
-    margin: 0;
-    padding: 5px 10px;
-    background-color: #ddd;
-    text-shadow: 1px 1px 0 white
-}
-
-div.sphinxsidebar h4{
-    font-size: 1.1em;
-}
- 
-div.sphinxsidebar h3 a {
-    color: #444;
-}
- 
- 
-div.sphinxsidebar p {
-    color: #888;
-    padding: 5px 20px;
-}
- 
-div.sphinxsidebar p.topless {
-}
- 
-div.sphinxsidebar ul {
-    margin: 10px 20px;
-    padding: 0;
-    color: #000;
-}
- 
-div.sphinxsidebar a {
-    color: #444;
-}
- 
-div.sphinxsidebar input {
-    border: 1px solid #ccc;
-    font-family: sans-serif;
-    font-size: 1em;
-}
-
-div.sphinxsidebar input[type=text]{
-    margin-left: 20px;
-}
- 
-/* -- body styles ----------------------------------------------------------- */
- 
-a {
-    color: #005B81;
-    text-decoration: none;
-}
- 
-a:hover {
-    color: #E32E00;
-    text-decoration: underline;
-}
- 
-div.document h1,
-div.document h2,
-div.document h3,
-div.document h4,
-div.document h5,
-div.document h6 {
-    font-family: Arial, sans-serif;
-    background-color: #BED4EB;
-    font-weight: normal;
-    color: #212224;
-    margin: 30px 0px 10px 0px;
-    padding: 5px 0 5px 10px;
-    text-shadow: 0px 1px 0 white
-}
- 
-div.document h1 { border-top: 20px solid white; margin-top: 0; font-size: 200%; }
-div.document h2 { font-size: 150%; background-color: #C8D5E3; }
-div.document h3 { font-size: 120%; background-color: #D8DEE3; }
-div.document h4 { font-size: 110%; background-color: #D8DEE3; }
-div.document h5 { font-size: 100%; background-color: #D8DEE3; }
-div.document h6 { font-size: 100%; background-color: #D8DEE3; }
- 
-a.headerlink {
-    color: #c60f0f;
-    font-size: 0.8em;
-    padding: 0 4px 0 4px;
-    text-decoration: none;
-}
- 
-a.headerlink:hover {
-    background-color: #c60f0f;
-    color: white;
-}
- 
-div.document p, div.document dd, div.document li {
-    line-height: 1.5em;
-}
- 
-div.admonition p.admonition-title + p {
-    display: inline;
-}
-
-div.highlight{
-    background-color: white;
-}
-
-div.note {
-    background-color: #eee;
-    border: 1px solid #ccc;
-}
- 
-div.seealso {
-    background-color: #ffc;
-    border: 1px solid #ff6;
-}
- 
-div.topic {
-    background-color: #eee;
-}
- 
-div.warning {
-    background-color: #ffe4e4;
-    border: 1px solid #f66;
-}
- 
-p.admonition-title {
-    display: inline;
-}
- 
-p.admonition-title:after {
-    content: ":";
-}
- 
-pre {
-    padding: 10px;
-    background-color: White;
-    color: #222;
-    line-height: 1.2em;
-    border: 1px solid #C6C9CB;
-    font-size: 1.1em;
-    margin: 1.5em 0 1.5em 0;
-    -webkit-box-shadow: 1px 1px 1px #d8d8d8;
-    -moz-box-shadow: 1px 1px 1px #d8d8d8;
-}
- 
-tt {
-    background-color: #ecf0f3;
-    color: #222;
-    /* padding: 1px 2px; */
-    font-size: 1.1em;
-    font-family: monospace;
-}
-
-.viewcode-back {
-    font-family: Arial, sans-serif;
-}
-
-div.viewcode-block:target {
-    background-color: #f4debf;
-    border-top: 1px solid #ac9;
-    border-bottom: 1px solid #ac9;
-}
-
-p {
-	margin: 0;
-}
-
-ul li dd {
-	margin-top: 0;
-}
-
-ul li dl {
-	margin-bottom: 0;
-}
-
-li dl dd {
-	margin-bottom: 0;
-}
-
-dd ul {
-	padding-left: 0;
-}
-
-li dd ul {
-	margin-bottom: 0;
-}
-
diff --git a/synapse/api/errors.py b/synapse/api/errors.py
index 88175602c4..6d7d499fea 100644
--- a/synapse/api/errors.py
+++ b/synapse/api/errors.py
@@ -19,6 +19,7 @@ import logging
 
 
 class Codes(object):
+    UNAUTHORIZED = "M_UNAUTHORIZED"
     FORBIDDEN = "M_FORBIDDEN"
     BAD_JSON = "M_BAD_JSON"
     NOT_JSON = "M_NOT_JSON"
diff --git a/synapse/api/events/factory.py b/synapse/api/events/factory.py
index 0d94850cec..74d0ef77f4 100644
--- a/synapse/api/events/factory.py
+++ b/synapse/api/events/factory.py
@@ -58,8 +58,8 @@ class EventFactory(object):
                 random_string(10), self.hs.hostname
             )
 
-        if "ts" not in kwargs:
-            kwargs["ts"] = int(self.clock.time_msec())
+        if "origin_server_ts" not in kwargs:
+            kwargs["origin_server_ts"] = int(self.clock.time_msec())
 
         # The "age" key is a delta timestamp that should be converted into an
         # absolute timestamp the minute we see it.
diff --git a/synapse/api/urls.py b/synapse/api/urls.py
index 6314f31f7a..6dc19305b7 100644
--- a/synapse/api/urls.py
+++ b/synapse/api/urls.py
@@ -18,4 +18,5 @@
 CLIENT_PREFIX = "/_matrix/client/api/v1"
 FEDERATION_PREFIX = "/_matrix/federation/v1"
 WEB_CLIENT_PREFIX = "/_matrix/client"
-CONTENT_REPO_PREFIX = "/_matrix/content"
\ No newline at end of file
+CONTENT_REPO_PREFIX = "/_matrix/content"
+SERVER_KEY_PREFIX = "/_matrix/key/v1"
diff --git a/synapse/app/homeserver.py b/synapse/app/homeserver.py
index 61d574a00f..6394bc27d1 100755
--- a/synapse/app/homeserver.py
+++ b/synapse/app/homeserver.py
@@ -25,9 +25,11 @@ from twisted.web.static import File
 from twisted.web.server import Site
 from synapse.http.server import JsonResource, RootRedirect
 from synapse.http.content_repository import ContentRepoResource
+from synapse.http.server_key_resource import LocalKey
 from synapse.http.client import MatrixHttpClient
 from synapse.api.urls import (
-    CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX
+    CLIENT_PREFIX, FEDERATION_PREFIX, WEB_CLIENT_PREFIX, CONTENT_REPO_PREFIX,
+    SERVER_KEY_PREFIX,
 )
 from synapse.config.homeserver import HomeServerConfig
 from synapse.crypto import context_factory
@@ -63,6 +65,9 @@ class SynapseHomeServer(HomeServer):
             self, self.upload_dir, self.auth, self.content_addr
         )
 
+    def build_resource_for_server_key(self):
+        return LocalKey(self)
+
     def build_db_pool(self):
         return adbapi.ConnectionPool(
             "sqlite3", self.get_db_name(),
@@ -88,7 +93,8 @@ class SynapseHomeServer(HomeServer):
         desired_tree = [
             (CLIENT_PREFIX, self.get_resource_for_client()),
             (FEDERATION_PREFIX, self.get_resource_for_federation()),
-            (CONTENT_REPO_PREFIX, self.get_resource_for_content_repo())
+            (CONTENT_REPO_PREFIX, self.get_resource_for_content_repo()),
+            (SERVER_KEY_PREFIX, self.get_resource_for_server_key()),
         ]
         if web_client:
             logger.info("Adding the web client.")
diff --git a/synapse/config/server.py b/synapse/config/server.py
index 516e4cf882..d9d8d0e14e 100644
--- a/synapse/config/server.py
+++ b/synapse/config/server.py
@@ -13,10 +13,9 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-import nacl.signing
 import os
-from ._base import Config
-from syutil.base64util import encode_base64, decode_base64
+from ._base import Config, ConfigError
+import syutil.crypto.signing_key
 
 
 class ServerConfig(Config):
@@ -70,9 +69,16 @@ class ServerConfig(Config):
                                   "content repository")
 
     def read_signing_key(self, signing_key_path):
-        signing_key_base64 = self.read_file(signing_key_path, "signing_key")
-        signing_key_bytes = decode_base64(signing_key_base64)
-        return nacl.signing.SigningKey(signing_key_bytes)
+        signing_keys = self.read_file(signing_key_path, "signing_key")
+        try:
+            return syutil.crypto.signing_key.read_signing_keys(
+                signing_keys.splitlines(True)
+            )
+        except Exception as e:
+            raise ConfigError(
+                "Error reading signing_key."
+                " Try running again with --generate-config"
+            )
 
     @classmethod
     def generate_config(cls, args, config_dir_path):
@@ -86,6 +92,21 @@ class ServerConfig(Config):
 
         if not os.path.exists(args.signing_key_path):
             with open(args.signing_key_path, "w") as signing_key_file:
-                key = nacl.signing.SigningKey.generate()
-                signing_key_file.write(encode_base64(key.encode()))
-
+                syutil.crypto.signing_key.write_signing_keys(
+                    signing_key_file,
+                    (syutil.crypto.SigningKey.generate("auto"),),
+                )
+        else:
+            signing_keys = cls.read_file(args.signing_key_path, "signing_key")
+            if len(signing_keys.split("\n")[0].split()) == 1:
+                # handle keys in the old format.
+                key = syutil.crypto.signing_key.decode_signing_key_base64(
+                    syutil.crypto.signing_key.NACL_ED25519,
+                    "auto",
+                    signing_keys.split("\n")[0]
+                )
+                with open(args.signing_key_path, "w") as signing_key_file:
+                    syutil.crypto.signing_key.write_signing_keys(
+                        signing_key_file,
+                        (key,),
+                    )
diff --git a/synapse/crypto/keyclient.py b/synapse/crypto/keyclient.py
index c11df5c529..5949ea0573 100644
--- a/synapse/crypto/keyclient.py
+++ b/synapse/crypto/keyclient.py
@@ -15,9 +15,10 @@
 
 
 from twisted.web.http import HTTPClient
+from twisted.internet.protocol import Factory
 from twisted.internet import defer, reactor
-from twisted.internet.protocol import ClientFactory
-from twisted.names.srvconnect import SRVConnector
+from twisted.internet.endpoints import connectProtocol
+from synapse.http.endpoint import matrix_endpoint
 import json
 import logging
 
@@ -30,15 +31,19 @@ def fetch_server_key(server_name, ssl_context_factory):
     """Fetch the keys for a remote server."""
 
     factory = SynapseKeyClientFactory()
+    endpoint = matrix_endpoint(
+        reactor, server_name, ssl_context_factory, timeout=30
+    )
 
-    SRVConnector(
-        reactor, "matrix", server_name, factory,
-        protocol="tcp", connectFuncName="connectSSL", defaultPort=443,
-        connectFuncKwArgs=dict(contextFactory=ssl_context_factory)).connect()
-
-    server_key, server_certificate = yield factory.remote_key
-
-    defer.returnValue((server_key, server_certificate))
+    for i in range(5):
+        try:
+            protocol = yield endpoint.connect(factory)
+            server_response, server_certificate = yield protocol.remote_key
+            defer.returnValue((server_response, server_certificate))
+            return
+        except Exception as e:
+            logger.exception(e)
+    raise IOError("Cannot get key for %s" % server_name)
 
 
 class SynapseKeyClientError(Exception):
@@ -51,69 +56,47 @@ class SynapseKeyClientProtocol(HTTPClient):
     the server and extracts the X.509 certificate for the remote peer from the
     SSL connection."""
 
+    timeout = 30
+
+    def __init__(self):
+        self.remote_key = defer.Deferred()
+
     def connectionMade(self):
         logger.debug("Connected to %s", self.transport.getHost())
-        self.sendCommand(b"GET", b"/key")
+        self.sendCommand(b"GET", b"/_matrix/key/v1/")
         self.endHeaders()
         self.timer = reactor.callLater(
-            self.factory.timeout_seconds,
+            self.timeout,
             self.on_timeout
         )
 
     def handleStatus(self, version, status, message):
         if status != b"200":
-            logger.info("Non-200 response from %s: %s %s",
-                        self.transport.getHost(), status, message)
+            #logger.info("Non-200 response from %s: %s %s",
+            #            self.transport.getHost(), status, message)
             self.transport.abortConnection()
 
     def handleResponse(self, response_body_bytes):
         try:
             json_response = json.loads(response_body_bytes)
         except ValueError:
-            logger.info("Invalid JSON response from %s",
-                        self.transport.getHost())
+            #logger.info("Invalid JSON response from %s",
+            #            self.transport.getHost())
             self.transport.abortConnection()
             return
 
         certificate = self.transport.getPeerCertificate()
-        self.factory.on_remote_key((json_response, certificate))
+        self.remote_key.callback((json_response, certificate))
         self.transport.abortConnection()
         self.timer.cancel()
 
     def on_timeout(self):
         logger.debug("Timeout waiting for response from %s",
                      self.transport.getHost())
+        self.remote_key.errback(IOError("Timeout waiting for response"))
         self.transport.abortConnection()
 
 
-class SynapseKeyClientFactory(ClientFactory):
+class SynapseKeyClientFactory(Factory):
     protocol = SynapseKeyClientProtocol
-    max_retries = 5
-    timeout_seconds = 30
-
-    def __init__(self):
-        self.succeeded = False
-        self.retries = 0
-        self.remote_key = defer.Deferred()
 
-    def on_remote_key(self, key):
-        self.succeeded = True
-        self.remote_key.callback(key)
-
-    def retry_connection(self, connector):
-        self.retries += 1
-        if self.retries < self.max_retries:
-            connector.connector = None
-            connector.connect()
-        else:
-            self.remote_key.errback(
-                SynapseKeyClientError("Max retries exceeded"))
-
-    def clientConnectionFailed(self, connector, reason):
-        logger.info("Connection failed %s", reason)
-        self.retry_connection(connector)
-
-    def clientConnectionLost(self, connector, reason):
-        logger.info("Connection lost %s", reason)
-        if not self.succeeded:
-            self.retry_connection(connector)
diff --git a/synapse/crypto/keyring.py b/synapse/crypto/keyring.py
new file mode 100644
index 0000000000..015f76ebe3
--- /dev/null
+++ b/synapse/crypto/keyring.py
@@ -0,0 +1,154 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# 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 synapse.crypto.keyclient import fetch_server_key
+from twisted.internet import defer
+from syutil.crypto.jsonsign import verify_signed_json, signature_ids
+from syutil.crypto.signing_key import (
+    is_signing_algorithm_supported, decode_verify_key_bytes
+)
+from syutil.base64util import decode_base64, encode_base64
+from synapse.api.errors import SynapseError, Codes
+
+from OpenSSL import crypto
+
+import logging
+
+
+logger = logging.getLogger(__name__)
+
+
+class Keyring(object):
+    def __init__(self, hs):
+        self.store = hs.get_datastore()
+        self.clock = hs.get_clock()
+        self.hs = hs
+
+    @defer.inlineCallbacks
+    def verify_json_for_server(self, server_name, json_object):
+        key_ids = signature_ids(json_object, server_name)
+        if not key_ids:
+            raise SynapseError(
+                400,
+                "Not signed with a supported algorithm",
+                 Codes.UNAUTHORIZED,
+            )
+        try:
+            verify_key = yield self.get_server_verify_key(server_name, key_ids)
+        except IOError:
+            raise SynapseError(
+                502,
+                "Error downloading keys for %s" % (server_name,),
+                Codes.UNAUTHORIZED,
+            )
+        except:
+            raise SynapseError(
+                401,
+                "No key for %s with id %s" % (server_name, key_ids),
+                Codes.UNAUTHORIZED,
+            )
+        try:
+            verify_signed_json(json_object, server_name, verify_key)
+        except:
+            raise SynapseError(
+                401,
+                "Invalid signature for server %s with key %s:%s" % (
+                    server_name, verify_key.alg, verify_key.version
+                ),
+                Codes.UNAUTHORIZED,
+            )
+
+    @defer.inlineCallbacks
+    def get_server_verify_key(self, server_name, key_ids):
+        """Finds a verification key for the server with one of the key ids.
+        Args:
+            server_name (str): The name of the server to fetch a key for.
+            keys_ids (list of str): The key_ids to check for.
+        """
+
+        # Check the datastore to see if we have one cached.
+        cached = yield self.store.get_server_verify_keys(server_name, key_ids)
+
+        if cached:
+            defer.returnValue(cached[0])
+            return
+
+        # Try to fetch the key from the remote server.
+        # TODO(markjh): Ratelimit requests to a given server.
+
+        (response, tls_certificate) = yield fetch_server_key(
+            server_name, self.hs.tls_context_factory
+        )
+
+        # Check the response.
+
+        x509_certificate_bytes = crypto.dump_certificate(
+            crypto.FILETYPE_ASN1, tls_certificate
+        )
+
+        if ("signatures" not in response
+            or server_name not in response["signatures"]):
+            raise ValueError("Key response not signed by remote server")
+
+        if "tls_certificate" not in response:
+            raise ValueError("Key response missing TLS certificate")
+
+        tls_certificate_b64 = response["tls_certificate"]
+
+        if encode_base64(x509_certificate_bytes) != tls_certificate_b64:
+            raise ValueError("TLS certificate doesn't match")
+
+        verify_keys = {}
+        for key_id, key_base64 in response["verify_keys"].items():
+            if is_signing_algorithm_supported(key_id):
+                key_bytes = decode_base64(key_base64)
+                verify_key = decode_verify_key_bytes(key_id, key_bytes)
+                verify_keys[key_id] = verify_key
+
+        for key_id in response["signatures"][server_name]:
+            if key_id not in response["verify_keys"]:
+                raise ValueError(
+                    "Key response must include verification keys for all"
+                    " signatures"
+                )
+            if key_id in verify_keys:
+                verify_signed_json(
+                    response,
+                    server_name,
+                    verify_keys[key_id]
+                )
+
+        # Cache the result in the datastore.
+
+        time_now_ms = self.clock.time_msec()
+
+        self.store.store_server_certificate(
+            server_name,
+            server_name,
+            time_now_ms,
+            tls_certificate,
+        )
+
+        for key_id, key in verify_keys.items():
+            self.store.store_server_verify_key(
+                server_name, server_name, time_now_ms, key
+            )
+
+        for key_id in key_ids:
+            if key_id in verify_keys:
+                defer.returnValue(verify_keys[key_id])
+                return
+
+        raise ValueError("No verification key found for given key ids")
diff --git a/synapse/crypto/keyserver.py b/synapse/crypto/keyserver.py
deleted file mode 100644
index a23484dbae..0000000000
--- a/synapse/crypto/keyserver.py
+++ /dev/null
@@ -1,111 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright 2014 OpenMarket Ltd
-#
-# 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.internet import reactor, ssl
-from twisted.web import server
-from twisted.web.resource import Resource
-from twisted.python.log import PythonLoggingObserver
-
-from synapse.crypto.resource.key import LocalKey
-from synapse.crypto.config import load_config
-
-from syutil.base64util import decode_base64
-
-from OpenSSL import crypto, SSL
-
-import logging
-import nacl.signing
-import sys
-
-
-class KeyServerSSLContextFactory(ssl.ContextFactory):
-    """Factory for PyOpenSSL SSL contexts that are used to handle incoming
-    connections and to make connections to remote servers."""
-
-    def __init__(self, key_server):
-        self._context = SSL.Context(SSL.SSLv23_METHOD)
-        self.configure_context(self._context, key_server)
-
-    @staticmethod
-    def configure_context(context, key_server):
-        context.set_options(SSL.OP_NO_SSLv2 | SSL.OP_NO_SSLv3)
-        context.use_certificate(key_server.tls_certificate)
-        context.use_privatekey(key_server.tls_private_key)
-        context.load_tmp_dh(key_server.tls_dh_params_path)
-        context.set_cipher_list("!ADH:HIGH+kEDH:!AECDH:HIGH+kEECDH")
-
-    def getContext(self):
-        return self._context
-
-
-class KeyServer(object):
-    """An HTTPS server serving LocalKey and RemoteKey resources."""
-
-    def __init__(self, server_name, tls_certificate_path, tls_private_key_path,
-                 tls_dh_params_path, signing_key_path, bind_host, bind_port):
-        self.server_name = server_name
-        self.tls_certificate = self.read_tls_certificate(tls_certificate_path)
-        self.tls_private_key = self.read_tls_private_key(tls_private_key_path)
-        self.tls_dh_params_path = tls_dh_params_path
-        self.signing_key = self.read_signing_key(signing_key_path)
-        self.bind_host = bind_host
-        self.bind_port = int(bind_port)
-        self.ssl_context_factory = KeyServerSSLContextFactory(self)
-
-    @staticmethod
-    def read_tls_certificate(cert_path):
-        with open(cert_path) as cert_file:
-            cert_pem = cert_file.read()
-            return crypto.load_certificate(crypto.FILETYPE_PEM, cert_pem)
-
-    @staticmethod
-    def read_tls_private_key(private_key_path):
-        with open(private_key_path) as private_key_file:
-            private_key_pem = private_key_file.read()
-            return crypto.load_privatekey(crypto.FILETYPE_PEM, private_key_pem)
-
-    @staticmethod
-    def read_signing_key(signing_key_path):
-        with open(signing_key_path) as signing_key_file:
-            signing_key_b64 = signing_key_file.read()
-            signing_key_bytes = decode_base64(signing_key_b64)
-            return nacl.signing.SigningKey(signing_key_bytes)
-
-    def run(self):
-        root = Resource()
-        root.putChild("key", LocalKey(self))
-        site = server.Site(root)
-        reactor.listenSSL(
-            self.bind_port,
-            site,
-            self.ssl_context_factory,
-            interface=self.bind_host
-        )
-
-        logging.basicConfig(level=logging.DEBUG)
-        observer = PythonLoggingObserver()
-        observer.start()
-
-        reactor.run()
-
-
-def main():
-    key_server = KeyServer(**load_config(__doc__, sys.argv[1:]))
-    key_server.run()
-
-
-if __name__ == "__main__":
-    main()
diff --git a/synapse/crypto/resource/__init__.py b/synapse/crypto/resource/__init__.py
deleted file mode 100644
index 9bff9ec169..0000000000
--- a/synapse/crypto/resource/__init__.py
+++ /dev/null
@@ -1,15 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright 2014 OpenMarket Ltd
-#
-# 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.
-
diff --git a/synapse/crypto/resource/key.py b/synapse/crypto/resource/key.py
deleted file mode 100644
index 48d14b9f4a..0000000000
--- a/synapse/crypto/resource/key.py
+++ /dev/null
@@ -1,161 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright 2014 OpenMarket Ltd
-#
-# 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 twisted.web.server import NOT_DONE_YET
-from twisted.internet import defer
-from synapse.http.server import respond_with_json_bytes
-from synapse.crypto.keyclient import fetch_server_key
-from syutil.crypto.jsonsign import sign_json, verify_signed_json
-from syutil.base64util import encode_base64, decode_base64
-from syutil.jsonutil import encode_canonical_json
-from OpenSSL import crypto
-from nacl.signing import VerifyKey
-import logging
-
-
-logger = logging.getLogger(__name__)
-
-
-class LocalKey(Resource):
-    """HTTP resource containing encoding the TLS X.509 certificate and NACL
-    signature verification keys for this server::
-
-        GET /key HTTP/1.1
-
-        HTTP/1.1 200 OK
-        Content-Type: application/json
-        {
-            "server_name": "this.server.example.com"
-            "signature_verify_key": # base64 encoded NACL verification key.
-            "tls_certificate": # base64 ASN.1 DER encoded X.509 tls cert.
-            "signatures": {
-                "this.server.example.com": # NACL signature for this server.
-            }
-        }
-    """
-
-    def __init__(self, key_server):
-        self.key_server = key_server
-        self.response_body = encode_canonical_json(
-            self.response_json_object(key_server)
-        )
-        Resource.__init__(self)
-
-    @staticmethod
-    def response_json_object(key_server):
-        verify_key_bytes = key_server.signing_key.verify_key.encode()
-        x509_certificate_bytes = crypto.dump_certificate(
-            crypto.FILETYPE_ASN1,
-            key_server.tls_certificate
-        )
-        json_object = {
-            u"server_name": key_server.server_name,
-            u"signature_verify_key": encode_base64(verify_key_bytes),
-            u"tls_certificate": encode_base64(x509_certificate_bytes)
-        }
-        signed_json = sign_json(
-            json_object,
-            key_server.server_name,
-            key_server.signing_key
-        )
-        return signed_json
-
-    def getChild(self, name, request):
-        logger.info("getChild %s %s", name, request)
-        if name == '':
-            return self
-        else:
-            return RemoteKey(name, self.key_server)
-
-    def render_GET(self, request):
-        return respond_with_json_bytes(request, 200, self.response_body)
-
-
-class RemoteKey(Resource):
-    """HTTP resource for retreiving the TLS certificate and NACL signature
-    verification keys for a another server. Checks that the reported X.509 TLS
-    certificate matches the one used in the HTTPS connection. Checks that the
-    NACL signature for the remote server is valid. Returns JSON signed by both
-    the remote server and by this server.
-
-    GET /key/remote.server.example.com HTTP/1.1
-
-    HTTP/1.1 200 OK
-    Content-Type: application/json
-    {
-        "server_name": "remote.server.example.com"
-        "signature_verify_key": # base64 encoded NACL verification key.
-        "tls_certificate": # base64 ASN.1 DER encoded X.509 tls cert.
-        "signatures": {
-            "remote.server.example.com": # NACL signature for remote server.
-            "this.server.example.com": # NACL signature for this server.
-        }
-    }
-    """
-
-    isLeaf = True
-
-    def __init__(self, server_name, key_server):
-        self.server_name = server_name
-        self.key_server = key_server
-        Resource.__init__(self)
-
-    def render_GET(self, request):
-        self._async_render_GET(request)
-        return NOT_DONE_YET
-
-    @defer.inlineCallbacks
-    def _async_render_GET(self, request):
-        try:
-            server_keys, certificate = yield fetch_server_key(
-                self.server_name,
-                self.key_server.ssl_context_factory
-            )
-
-            resp_server_name = server_keys[u"server_name"]
-            verify_key_b64 = server_keys[u"signature_verify_key"]
-            tls_certificate_b64 = server_keys[u"tls_certificate"]
-            verify_key = VerifyKey(decode_base64(verify_key_b64))
-
-            if resp_server_name != self.server_name:
-                raise ValueError("Wrong server name '%s' != '%s'" %
-                                 (resp_server_name, self.server_name))
-
-            x509_certificate_bytes = crypto.dump_certificate(
-                crypto.FILETYPE_ASN1,
-                certificate
-            )
-
-            if encode_base64(x509_certificate_bytes) != tls_certificate_b64:
-                raise ValueError("TLS certificate doesn't match")
-
-            verify_signed_json(server_keys, self.server_name, verify_key)
-
-            signed_json = sign_json(
-                server_keys,
-                self.key_server.server_name,
-                self.key_server.signing_key
-            )
-
-            json_bytes = encode_canonical_json(signed_json)
-            respond_with_json_bytes(request, 200, json_bytes)
-
-        except Exception as e:
-            json_bytes = encode_canonical_json({
-                u"error": {u"code": 502, u"message": e.message}
-            })
-            respond_with_json_bytes(request, 502, json_bytes)
diff --git a/synapse/federation/__init__.py b/synapse/federation/__init__.py
index 1351b68fd6..0112588656 100644
--- a/synapse/federation/__init__.py
+++ b/synapse/federation/__init__.py
@@ -22,6 +22,7 @@ from .transport import TransportLayer
 
 def initialize_http_replication(homeserver):
     transport = TransportLayer(
+        homeserver,
         homeserver.hostname,
         server=homeserver.get_resource_for_federation(),
         client=homeserver.get_http_client()
diff --git a/synapse/federation/pdu_codec.py b/synapse/federation/pdu_codec.py
index cef61108dd..e8180d94fd 100644
--- a/synapse/federation/pdu_codec.py
+++ b/synapse/federation/pdu_codec.py
@@ -96,7 +96,7 @@ class PduCodec(object):
             if k not in ["event_id", "room_id", "type", "prev_events"]
         })
 
-        if "ts" not in kwargs:
-            kwargs["ts"] = int(self.clock.time_msec())
+        if "origin_server_ts" not in kwargs:
+            kwargs["origin_server_ts"] = int(self.clock.time_msec())
 
         return Pdu(**kwargs)
diff --git a/synapse/federation/persistence.py b/synapse/federation/persistence.py
index de36a80e41..7043fcc504 100644
--- a/synapse/federation/persistence.py
+++ b/synapse/federation/persistence.py
@@ -157,7 +157,7 @@ class TransactionActions(object):
         transaction.prev_ids = yield self.store.prep_send_transaction(
             transaction.transaction_id,
             transaction.destination,
-            transaction.ts,
+            transaction.origin_server_ts,
             [(p["pdu_id"], p["origin"]) for p in transaction.pdus]
         )
 
diff --git a/synapse/federation/replication.py b/synapse/federation/replication.py
index 5f96f79998..092411eaf9 100644
--- a/synapse/federation/replication.py
+++ b/synapse/federation/replication.py
@@ -319,7 +319,7 @@ class ReplicationLayer(object):
 
         if hasattr(transaction, "edus"):
             for edu in [Edu(**x) for x in transaction.edus]:
-                self.received_edu(edu.origin, edu.edu_type, edu.content)
+                self.received_edu(transaction.origin, edu.edu_type, edu.content)
 
         results = yield defer.DeferredList(dl)
 
@@ -421,7 +421,7 @@ class ReplicationLayer(object):
         return Transaction(
             origin=self.server_name,
             pdus=pdus,
-            ts=int(self._clock.time_msec()),
+            origin_server_ts=int(self._clock.time_msec()),
             destination=None,
         )
 
@@ -492,7 +492,6 @@ class _TransactionQueue(object):
     """
 
     def __init__(self, hs, transaction_actions, transport_layer):
-
         self.server_name = hs.hostname
         self.transaction_actions = transaction_actions
         self.transport_layer = transport_layer
@@ -590,8 +589,8 @@ class _TransactionQueue(object):
             logger.debug("TX [%s] Persisting transaction...", destination)
 
             transaction = Transaction.create_new(
-                ts=self._clock.time_msec(),
-                transaction_id=self._next_txn_id,
+                origin_server_ts=self._clock.time_msec(),
+                transaction_id=str(self._next_txn_id),
                 origin=self.server_name,
                 destination=destination,
                 pdus=pdus,
@@ -609,18 +608,17 @@ class _TransactionQueue(object):
 
             # FIXME (erikj): This is a bit of a hack to make the Pdu age
             # keys work
-            def cb(transaction):
+            def json_data_cb():
+                data = transaction.get_dict()
                 now = int(self._clock.time_msec())
-                if "pdus" in transaction:
-                    for p in transaction["pdus"]:
+                if "pdus" in data:
+                    for p in data["pdus"]:
                         if "age_ts" in p:
                             p["age"] = now - int(p["age_ts"])
-
-                return transaction
+                return data
 
             code, response = yield self.transport_layer.send_transaction(
-                transaction,
-                on_send_callback=cb,
+                transaction, json_data_cb
             )
 
             logger.debug("TX [%s] Sent transaction", destination)
diff --git a/synapse/federation/transport.py b/synapse/federation/transport.py
index 93296af204..755eee8cf6 100644
--- a/synapse/federation/transport.py
+++ b/synapse/federation/transport.py
@@ -24,6 +24,7 @@ over a different (albeit still reliable) protocol.
 from twisted.internet import defer
 
 from synapse.api.urls import FEDERATION_PREFIX as PREFIX
+from synapse.api.errors import Codes, SynapseError
 from synapse.util.logutils import log_function
 
 import logging
@@ -54,7 +55,7 @@ class TransportLayer(object):
             we receive data.
     """
 
-    def __init__(self, server_name, server, client):
+    def __init__(self, homeserver, server_name, server, client):
         """
         Args:
             server_name (str): Local home server host
@@ -63,6 +64,7 @@ class TransportLayer(object):
             client (synapse.protocol.http.HttpClient): the http client used to
                 send requests
         """
+        self.keyring = homeserver.get_keyring()
         self.server_name = server_name
         self.server = server
         self.client = client
@@ -144,7 +146,7 @@ class TransportLayer(object):
 
     @defer.inlineCallbacks
     @log_function
-    def send_transaction(self, transaction, on_send_callback=None):
+    def send_transaction(self, transaction, json_data_callback=None):
         """ Sends the given Transaction to it's destination
 
         Args:
@@ -163,25 +165,15 @@ class TransportLayer(object):
         if transaction.destination == self.server_name:
             raise RuntimeError("Transport layer cannot send to itself!")
 
-        data = transaction.get_dict()
-
-        # FIXME (erikj): This is a bit of a hack to make the Pdu age
-        # keys work
-        def cb(destination, method, path_bytes, producer):
-            if not on_send_callback:
-                return
-
-            transaction = json.loads(producer.body)
-
-            new_transaction = on_send_callback(transaction)
-
-            producer.reset(new_transaction)
+        # FIXME: This is only used by the tests. The actual json sent is
+        # generated by the json_data_callback.
+        json_data = transaction.get_dict()
 
         code, response = yield self.client.put_json(
             transaction.destination,
             path=PREFIX + "/send/%s/" % transaction.transaction_id,
-            data=data,
-            on_send_callback=cb,
+            data=json_data,
+            json_data_callback=json_data_callback,
         )
 
         logger.debug(
@@ -205,6 +197,72 @@ class TransportLayer(object):
 
         defer.returnValue(response)
 
+    @defer.inlineCallbacks
+    def _authenticate_request(self, request):
+        json_request = {
+            "method": request.method,
+            "uri": request.uri,
+            "destination": self.server_name,
+            "signatures": {},
+        }
+
+        content = None
+        origin = None
+
+        if request.method == "PUT":
+            #TODO: Handle other method types? other content types?
+            try:
+                content_bytes = request.content.read()
+                content = json.loads(content_bytes)
+                json_request["content"] = content
+            except:
+                raise SynapseError(400, "Unable to parse JSON", Codes.BAD_JSON)
+
+        def parse_auth_header(header_str):
+            try:
+                params = auth.split(" ")[1].split(",")
+                param_dict = dict(kv.split("=") for kv in params)
+                def strip_quotes(value):
+                    if value.startswith("\""):
+                        return value[1:-1]
+                    else:
+                        return value
+                origin = strip_quotes(param_dict["origin"])
+                key = strip_quotes(param_dict["key"])
+                sig = strip_quotes(param_dict["sig"])
+                return (origin, key, sig)
+            except:
+                raise SynapseError(
+                    400, "Malformed Authorization header", Codes.UNAUTHORIZED
+                )
+
+        auth_headers = request.requestHeaders.getRawHeaders(b"Authorization")
+
+        for auth in auth_headers:
+            if auth.startswith("X-Matrix"):
+                (origin, key, sig) = parse_auth_header(auth)
+                json_request["origin"] = origin
+                json_request["signatures"].setdefault(origin,{})[key] = sig
+
+        if not json_request["signatures"]:
+            raise SynapseError(
+                401, "Missing Authorization headers", Codes.UNAUTHORIZED,
+            )
+
+        yield self.keyring.verify_json_for_server(origin, json_request)
+
+        defer.returnValue((origin, content))
+
+    def _with_authentication(self, handler):
+        @defer.inlineCallbacks
+        def new_handler(request, *args, **kwargs):
+            (origin, content) = yield self._authenticate_request(request)
+            response = yield handler(
+                origin, content, request.args, *args, **kwargs
+            )
+            defer.returnValue(response)
+        return new_handler
+
     @log_function
     def register_received_handler(self, handler):
         """ Register a handler that will be fired when we receive data.
@@ -218,7 +276,7 @@ class TransportLayer(object):
         self.server.register_path(
             "PUT",
             re.compile("^" + PREFIX + "/send/([^/]*)/$"),
-            self._on_send_request
+            self._with_authentication(self._on_send_request)
         )
 
     @log_function
@@ -236,9 +294,9 @@ class TransportLayer(object):
         self.server.register_path(
             "GET",
             re.compile("^" + PREFIX + "/pull/$"),
-            lambda request: handler.on_pull_request(
-                request.args["origin"][0],
-                request.args["v"]
+            self._with_authentication(
+                lambda origin, content, query:
+                handler.on_pull_request(query["origin"][0], query["v"])
             )
         )
 
@@ -247,8 +305,9 @@ class TransportLayer(object):
         self.server.register_path(
             "GET",
             re.compile("^" + PREFIX + "/pdu/([^/]*)/([^/]*)/$"),
-            lambda request, pdu_origin, pdu_id: handler.on_pdu_request(
-                pdu_origin, pdu_id
+            self._with_authentication(
+                lambda origin, content, query, pdu_origin, pdu_id:
+                handler.on_pdu_request(pdu_origin, pdu_id)
             )
         )
 
@@ -256,38 +315,47 @@ class TransportLayer(object):
         self.server.register_path(
             "GET",
             re.compile("^" + PREFIX + "/state/([^/]*)/$"),
-            lambda request, context: handler.on_context_state_request(
-                context
+            self._with_authentication(
+                lambda origin, content, query, context:
+                handler.on_context_state_request(context)
             )
         )
 
         self.server.register_path(
             "GET",
             re.compile("^" + PREFIX + "/backfill/([^/]*)/$"),
-            lambda request, context: self._on_backfill_request(
-                context, request.args["v"],
-                request.args["limit"]
+            self._with_authentication(
+                lambda origin, content, query, context:
+                self._on_backfill_request(
+                    context, query["v"], query["limit"]
+                )
             )
         )
 
         self.server.register_path(
             "GET",
             re.compile("^" + PREFIX + "/context/([^/]*)/$"),
-            lambda request, context: handler.on_context_pdus_request(context)
+            self._with_authentication(
+                lambda origin, content, query, context:
+                handler.on_context_pdus_request(context)
+            )
         )
 
         # This is when we receive a server-server Query
         self.server.register_path(
             "GET",
             re.compile("^" + PREFIX + "/query/([^/]*)$"),
-            lambda request, query_type: handler.on_query_request(
-                query_type, {k: v[0] for k, v in request.args.items()}
+            self._with_authentication(
+                lambda origin, content, query, query_type:
+                handler.on_query_request(
+                    query_type, {k: v[0] for k, v in query.items()}
+                )
             )
         )
 
     @defer.inlineCallbacks
     @log_function
-    def _on_send_request(self, request, transaction_id):
+    def _on_send_request(self, origin, content, query, transaction_id):
         """ Called on PUT /send/<transaction_id>/
 
         Args:
@@ -302,12 +370,7 @@ class TransportLayer(object):
         """
         # Parse the request
         try:
-            data = request.content.read()
-
-            l = data[:20].encode("string_escape")
-            logger.debug("Got data: \"%s\"", l)
-
-            transaction_data = json.loads(data)
+            transaction_data = content
 
             logger.debug(
                 "Decoded %s: %s",
diff --git a/synapse/federation/units.py b/synapse/federation/units.py
index 622fe66a8f..b2fb964180 100644
--- a/synapse/federation/units.py
+++ b/synapse/federation/units.py
@@ -40,7 +40,7 @@ class Pdu(JsonEncodedObject):
 
         {
             "pdu_id": "78c",
-            "ts": 1404835423000,
+            "origin_server_ts": 1404835423000,
             "origin": "bar",
             "prev_ids": [
                 ["23b", "foo"],
@@ -55,7 +55,7 @@ class Pdu(JsonEncodedObject):
         "pdu_id",
         "context",
         "origin",
-        "ts",
+        "origin_server_ts",
         "pdu_type",
         "destinations",
         "transaction_id",
@@ -82,7 +82,7 @@ class Pdu(JsonEncodedObject):
         "pdu_id",
         "context",
         "origin",
-        "ts",
+        "origin_server_ts",
         "pdu_type",
         "content",
     ]
@@ -118,6 +118,7 @@ class Pdu(JsonEncodedObject):
         """
         if pdu_tuple:
             d = copy.copy(pdu_tuple.pdu_entry._asdict())
+            d["origin_server_ts"] = d.pop("ts")
 
             d["content"] = json.loads(d["content_json"])
             del d["content_json"]
@@ -156,11 +157,15 @@ class Edu(JsonEncodedObject):
     ]
 
     required_keys = [
-        "origin",
-        "destination",
         "edu_type",
     ]
 
+#    TODO: SYN-103: Remove "origin" and "destination" keys.
+#    internal_keys = [
+#        "origin",
+#        "destination",
+#    ]
+
 
 class Transaction(JsonEncodedObject):
     """ A transaction is a list of Pdus and Edus to be sent to a remote home
@@ -182,10 +187,12 @@ class Transaction(JsonEncodedObject):
         "transaction_id",
         "origin",
         "destination",
-        "ts",
+        "origin_server_ts",
         "previous_ids",
         "pdus",
         "edus",
+        "transaction_id",
+        "destination",
     ]
 
     internal_keys = [
@@ -197,7 +204,7 @@ class Transaction(JsonEncodedObject):
         "transaction_id",
         "origin",
         "destination",
-        "ts",
+        "origin_server_ts",
         "pdus",
     ]
 
@@ -219,10 +226,10 @@ class Transaction(JsonEncodedObject):
     @staticmethod
     def create_new(pdus, **kwargs):
         """ Used to create a new transaction. Will auto fill out
-        transaction_id and ts keys.
+        transaction_id and origin_server_ts keys.
         """
-        if "ts" not in kwargs:
-            raise KeyError("Require 'ts' to construct a Transaction")
+        if "origin_server_ts" not in kwargs:
+            raise KeyError("Require 'origin_server_ts' to construct a Transaction")
         if "transaction_id" not in kwargs:
             raise KeyError(
                 "Require 'transaction_id' to construct a Transaction"
diff --git a/synapse/handlers/message.py b/synapse/handlers/message.py
index 317ef2c80c..7b2b8549ed 100644
--- a/synapse/handlers/message.py
+++ b/synapse/handlers/message.py
@@ -64,7 +64,7 @@ class MessageHandler(BaseHandler):
         defer.returnValue(None)
 
     @defer.inlineCallbacks
-    def send_message(self, event=None, suppress_auth=False, stamp_event=True):
+    def send_message(self, event=None, suppress_auth=False):
         """ Send a message.
 
         Args:
@@ -72,7 +72,6 @@ class MessageHandler(BaseHandler):
             suppress_auth (bool) : True to suppress auth for this message. This
             is primarily so the home server can inject messages into rooms at
             will.
-            stamp_event (bool) : True to stamp event content with server keys.
         Raises:
             SynapseError if something went wrong.
         """
@@ -82,9 +81,6 @@ class MessageHandler(BaseHandler):
         user = self.hs.parse_userid(event.user_id)
         assert user.is_mine, "User must be our own: %s" % (user,)
 
-        if stamp_event:
-            event.content["hsob_ts"] = int(self.clock.time_msec())
-
         snapshot = yield self.store.snapshot_room(event.room_id, event.user_id)
 
         if not suppress_auth:
@@ -132,7 +128,7 @@ class MessageHandler(BaseHandler):
         defer.returnValue(chunk)
 
     @defer.inlineCallbacks
-    def store_room_data(self, event=None, stamp_event=True):
+    def store_room_data(self, event=None):
         """ Stores data for a room.
 
         Args:
@@ -151,9 +147,6 @@ class MessageHandler(BaseHandler):
 
         yield self.auth.check(event, snapshot, raises=True)
 
-        if stamp_event:
-            event.content["hsob_ts"] = int(self.clock.time_msec())
-
         yield self.state_handler.handle_new_event(event, snapshot)
 
         yield self._on_new_room_event(event, snapshot)
@@ -221,10 +214,7 @@ class MessageHandler(BaseHandler):
         defer.returnValue(None)
 
     @defer.inlineCallbacks
-    def send_feedback(self, event, stamp_event=True):
-        if stamp_event:
-            event.content["hsob_ts"] = int(self.clock.time_msec())
-
+    def send_feedback(self, event):
         snapshot = yield self.store.snapshot_room(event.room_id, event.user_id)
 
         yield self.auth.check(event, snapshot, raises=True)
diff --git a/synapse/http/client.py b/synapse/http/client.py
index 5c2fbd1f87..316ca1ccb9 100644
--- a/synapse/http/client.py
+++ b/synapse/http/client.py
@@ -26,11 +26,14 @@ from syutil.jsonutil import encode_canonical_json
 
 from synapse.api.errors import CodeMessageException, SynapseError
 
+from syutil.crypto.jsonsign import sign_json
+
 from StringIO import StringIO
 
 import json
 import logging
 import urllib
+import urlparse
 
 
 logger = logging.getLogger(__name__)
@@ -68,16 +71,20 @@ class BaseHttpClient(object):
         self.hs = hs
 
     @defer.inlineCallbacks
-    def _create_request(self, destination, method, path_bytes, param_bytes=b"",
-                        query_bytes=b"", producer=None, headers_dict={},
-                        retry_on_dns_fail=True, on_send_callback=None):
+    def _create_request(self, destination, method, path_bytes,
+                        body_callback, headers_dict={}, param_bytes=b"",
+                        query_bytes=b"", retry_on_dns_fail=True):
         """ Creates and sends a request to the given url
         """
         headers_dict[b"User-Agent"] = [b"Synapse"]
         headers_dict[b"Host"] = [destination]
 
-        logger.debug("Sending request to %s: %s %s;%s?%s",
-                     destination, method, path_bytes, param_bytes, query_bytes)
+        url_bytes = urlparse.urlunparse(
+            ("", "", path_bytes, param_bytes, query_bytes, "",)
+        )
+
+        logger.debug("Sending request to %s: %s %s",
+                     destination, method, url_bytes)
 
         logger.debug(
             "Types: %s",
@@ -93,8 +100,8 @@ class BaseHttpClient(object):
         endpoint = self._getEndpoint(reactor, destination);
 
         while True:
-            if on_send_callback:
-                on_send_callback(destination, method, path_bytes, producer)
+
+            producer = body_callback(method, url_bytes, headers_dict)
 
             try:
                 response = yield self.agent.request(
@@ -142,7 +149,7 @@ class BaseHttpClient(object):
 
 
 class MatrixHttpClient(BaseHttpClient):
-    """ Wrapper around the twisted HTTP client api. Implements 
+    """ Wrapper around the twisted HTTP client api. Implements
 
     Attributes:
         agent (twisted.web.client.Agent): The twisted Agent used to send the
@@ -151,8 +158,38 @@ class MatrixHttpClient(BaseHttpClient):
 
     RETRY_DNS_LOOKUP_FAILURES = "__retry_dns"
 
+    def __init__(self, hs):
+        self.signing_key = hs.config.signing_key[0]
+        self.server_name = hs.hostname
+        BaseHttpClient.__init__(self, hs)
+
+    def sign_request(self, destination, method, url_bytes, headers_dict,
+                     content=None):
+        request = {
+            "method": method,
+            "uri": url_bytes,
+            "origin": self.server_name,
+            "destination": destination,
+        }
+
+        if content is not None:
+            request["content"] = content
+
+        request = sign_json(request, self.server_name, self.signing_key)
+
+        auth_headers = []
+
+        for key,sig in request["signatures"][self.server_name].items():
+            auth_headers.append(bytes(
+                "X-Matrix origin=%s,key=\"%s\",sig=\"%s\"" % (
+                    self.server_name, key, sig,
+                )
+            ))
+
+        headers_dict[b"Authorization"] = auth_headers
+
     @defer.inlineCallbacks
-    def put_json(self, destination, path, data, on_send_callback=None):
+    def put_json(self, destination, path, data={}, json_data_callback=None):
         """ Sends the specifed json data using PUT
 
         Args:
@@ -161,19 +198,33 @@ class MatrixHttpClient(BaseHttpClient):
             path (str): The HTTP path.
             data (dict): A dict containing the data that will be used as
                 the request body. This will be encoded as JSON.
+            json_data_callback (callable): A callable returning the dict to
+                use as the request body.
 
         Returns:
             Deferred: Succeeds when we get a 2xx HTTP response. The result
             will be the decoded JSON body. On a 4xx or 5xx error response a
             CodeMessageException is raised.
         """
+
+        if not json_data_callback:
+            def json_data_callback():
+                return data
+
+        def body_callback(method, url_bytes, headers_dict):
+            json_data = json_data_callback()
+            self.sign_request(
+                destination, method, url_bytes, headers_dict, json_data
+            )
+            producer = _JsonProducer(json_data)
+            return producer
+
         response = yield self._create_request(
             destination.encode("ascii"),
             "PUT",
             path.encode("ascii"),
-            producer=_JsonProducer(data),
+            body_callback=body_callback,
             headers_dict={"Content-Type": ["application/json"]},
-            on_send_callback=on_send_callback,
         )
 
         logger.debug("Getting resp body")
@@ -206,11 +257,16 @@ class MatrixHttpClient(BaseHttpClient):
         query_bytes = urllib.urlencode(args, True)
         logger.debug("Query bytes: %s Retry DNS: %s", args, retry_on_dns_fail)
 
+        def body_callback(method, url_bytes, headers_dict):
+            self.sign_request(destination, method, url_bytes, headers_dict)
+            return None
+
         response = yield self._create_request(
             destination.encode("ascii"),
             "GET",
             path.encode("ascii"),
             query_bytes=query_bytes,
+            body_callback=body_callback,
             retry_on_dns_fail=retry_on_dns_fail
         )
 
@@ -239,11 +295,14 @@ class IdentityServerHttpClient(BaseHttpClient):
         logger.debug("post_urlencoded_get_json args: %s", args)
         query_bytes = urllib.urlencode(args, True)
 
+        def body_callback(method, url_bytes, headers_dict):
+            return FileBodyProducer(StringIO(query_bytes))
+
         response = yield self._create_request(
             destination.encode("ascii"),
             "POST",
             path.encode("ascii"),
-            producer=FileBodyProducer(StringIO(query_bytes)),
+            body_callback=body_callback,
             headers_dict={
                 "Content-Type": ["application/x-www-form-urlencoded"]
             }
@@ -265,11 +324,14 @@ class CaptchaServerHttpClient(MatrixHttpClient):
                                 args={}):
         query_bytes = urllib.urlencode(args, True)
 
+        def body_callback(method, url_bytes, headers_dict):
+            return FileBodyProducer(StringIO(query_bytes))
+
         response = yield self._create_request(
             destination.encode("ascii"),
             "POST",
             path.encode("ascii"),
-            producer=FileBodyProducer(StringIO(query_bytes)),
+            body_callback=body_callback,
             headers_dict={
                 "Content-Type": ["application/x-www-form-urlencoded"]
             }
diff --git a/synapse/http/server_key_resource.py b/synapse/http/server_key_resource.py
new file mode 100644
index 0000000000..b30ecead27
--- /dev/null
+++ b/synapse/http/server_key_resource.py
@@ -0,0 +1,89 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014 OpenMarket Ltd
+#
+# 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 synapse.http.server import respond_with_json_bytes
+from syutil.crypto.jsonsign import sign_json
+from syutil.base64util import encode_base64
+from syutil.jsonutil import encode_canonical_json
+from OpenSSL import crypto
+import logging
+
+
+logger = logging.getLogger(__name__)
+
+
+class LocalKey(Resource):
+    """HTTP resource containing encoding the TLS X.509 certificate and NACL
+    signature verification keys for this server::
+
+        GET /key HTTP/1.1
+
+        HTTP/1.1 200 OK
+        Content-Type: application/json
+        {
+            "server_name": "this.server.example.com"
+            "verify_keys": {
+                "algorithm:version": # base64 encoded NACL verification key.
+            },
+            "tls_certificate": # base64 ASN.1 DER encoded X.509 tls cert.
+            "signatures": {
+                "this.server.example.com": {
+                   "algorithm:version": # NACL signature for this server.
+                }
+            }
+        }
+    """
+
+    def __init__(self, hs):
+        self.hs = hs
+        self.response_body = encode_canonical_json(
+            self.response_json_object(hs.config)
+        )
+        Resource.__init__(self)
+
+    @staticmethod
+    def response_json_object(server_config):
+        verify_keys = {}
+        for key in server_config.signing_key:
+            verify_key_bytes = key.verify_key.encode()
+            key_id = "%s:%s" % (key.alg, key.version)
+            verify_keys[key_id] = encode_base64(verify_key_bytes)
+
+        x509_certificate_bytes = crypto.dump_certificate(
+            crypto.FILETYPE_ASN1,
+            server_config.tls_certificate
+        )
+        json_object = {
+            u"server_name": server_config.server_name,
+            u"verify_keys": verify_keys,
+            u"tls_certificate": encode_base64(x509_certificate_bytes)
+        }
+        for key in server_config.signing_key:
+            json_object = sign_json(
+                json_object,
+                server_config.server_name,
+                key,
+            )
+
+        return json_object
+
+    def render_GET(self, request):
+        return respond_with_json_bytes(request, 200, self.response_body)
+
+    def getChild(self, name, request):
+        if name == '':
+            return self
diff --git a/synapse/rest/presence.py b/synapse/rest/presence.py
index 7fc8ce4404..138cc88a05 100644
--- a/synapse/rest/presence.py
+++ b/synapse/rest/presence.py
@@ -68,7 +68,7 @@ class PresenceStatusRestServlet(RestServlet):
         yield self.handlers.presence_handler.set_state(
             target_user=user, auth_user=auth_user, state=state)
 
-        defer.returnValue((200, ""))
+        defer.returnValue((200, {}))
 
     def on_OPTIONS(self, request):
         return (200, {})
@@ -141,7 +141,7 @@ class PresenceListRestServlet(RestServlet):
 
         yield defer.DeferredList(deferreds)
 
-        defer.returnValue((200, ""))
+        defer.returnValue((200, {}))
 
     def on_OPTIONS(self, request):
         return (200, {})
diff --git a/synapse/server.py b/synapse/server.py
index e5b048ede0..a4d2d4aba5 100644
--- a/synapse/server.py
+++ b/synapse/server.py
@@ -34,6 +34,7 @@ from synapse.util.distributor import Distributor
 from synapse.util.lockutils import LockManager
 from synapse.streams.events import EventSources
 from synapse.api.ratelimiting import Ratelimiter
+from synapse.crypto.keyring import Keyring
 
 
 class BaseHomeServer(object):
@@ -75,8 +76,10 @@ class BaseHomeServer(object):
         'resource_for_federation',
         'resource_for_web_client',
         'resource_for_content_repo',
+        'resource_for_server_key',
         'event_sources',
         'ratelimiter',
+        'keyring',
     ]
 
     def __init__(self, hostname, **kwargs):
@@ -212,6 +215,9 @@ class HomeServer(BaseHomeServer):
     def build_ratelimiter(self):
         return Ratelimiter()
 
+    def build_keyring(self):
+        return Keyring(self)
+
     def register_servlets(self):
         """ Register all servlets associated with this HomeServer.
         """
diff --git a/synapse/storage/__init__.py b/synapse/storage/__init__.py
index 32d9c1392b..c8e0efb18f 100644
--- a/synapse/storage/__init__.py
+++ b/synapse/storage/__init__.py
@@ -57,6 +57,7 @@ SCHEMAS = [
     "presence",
     "im",
     "room_aliases",
+    "keys",
     "redactions",
 ]
 
@@ -154,6 +155,8 @@ class DataStore(RoomMemberStore, RoomStore,
 
         cols["unrecognized_keys"] = json.dumps(unrec_keys)
 
+        cols["ts"] = cols.pop("origin_server_ts")
+
         logger.debug("Persisting: %s", repr(cols))
 
         if pdu.is_state:
diff --git a/synapse/storage/_base.py b/synapse/storage/_base.py
index 889de2bedc..65a86e9056 100644
--- a/synapse/storage/_base.py
+++ b/synapse/storage/_base.py
@@ -121,7 +121,7 @@ class SQLBaseStore(object):
     # "Simple" SQL API methods that operate on a single table with no JOINs,
     # no complex WHERE clauses, just a dict of values for columns.
 
-    def _simple_insert(self, table, values, or_replace=False):
+    def _simple_insert(self, table, values, or_replace=False, or_ignore=False):
         """Executes an INSERT query on the named table.
 
         Args:
@@ -130,13 +130,16 @@ class SQLBaseStore(object):
             or_replace : bool; if True performs an INSERT OR REPLACE
         """
         return self.runInteraction(
-            self._simple_insert_txn, table, values, or_replace=or_replace
+            self._simple_insert_txn, table, values, or_replace=or_replace,
+            or_ignore=or_ignore,
         )
 
     @log_function
-    def _simple_insert_txn(self, txn, table, values, or_replace=False):
+    def _simple_insert_txn(self, txn, table, values, or_replace=False,
+                           or_ignore=False):
         sql = "%s INTO %s (%s) VALUES(%s)" % (
-            ("INSERT OR REPLACE" if or_replace else "INSERT"),
+            ("INSERT OR REPLACE" if or_replace else
+             "INSERT OR IGNORE" if or_ignore else "INSERT"),
             table,
             ", ".join(k for k in values),
             ", ".join("?" for k in values)
@@ -351,6 +354,7 @@ class SQLBaseStore(object):
         d.pop("stream_ordering", None)
         d.pop("topological_ordering", None)
         d.pop("processed", None)
+        d["origin_server_ts"] = d.pop("ts", 0)
 
         d.update(json.loads(row_dict["unrecognized_keys"]))
         d["content"] = json.loads(d["content"])
@@ -358,7 +362,7 @@ class SQLBaseStore(object):
 
         if "age_ts" not in d:
             # For compatibility
-            d["age_ts"] = d["ts"] if "ts" in d else 0
+            d["age_ts"] = d.get("origin_server_ts", 0)
 
         return self.event_factory.create_event(
             etype=d["type"],
diff --git a/synapse/storage/keys.py b/synapse/storage/keys.py
index 5a38c3e8f2..8189e071a3 100644
--- a/synapse/storage/keys.py
+++ b/synapse/storage/keys.py
@@ -18,7 +18,8 @@ from _base import SQLBaseStore
 from twisted.internet import defer
 
 import OpenSSL
-import nacl.signing
+from  syutil.crypto.signing_key import decode_verify_key_bytes
+import hashlib
 
 class KeyStore(SQLBaseStore):
     """Persistence for signature verification keys and tls X.509 certificates
@@ -42,62 +43,76 @@ class KeyStore(SQLBaseStore):
         )
         defer.returnValue(tls_certificate)
 
-    def store_server_certificate(self, server_name, key_server, ts_now_ms,
+    def store_server_certificate(self, server_name, from_server, time_now_ms,
                                  tls_certificate):
         """Stores the TLS X.509 certificate for the given server
         Args:
-            server_name (bytes): The name of the server.
-            key_server (bytes): Where the certificate was looked up
-            ts_now_ms (int): The time now in milliseconds
+            server_name (str): The name of the server.
+            from_server (str): Where the certificate was looked up
+            time_now_ms (int): The time now in milliseconds
             tls_certificate (OpenSSL.crypto.X509): The X.509 certificate.
         """
         tls_certificate_bytes = OpenSSL.crypto.dump_certificate(
             OpenSSL.crypto.FILETYPE_ASN1, tls_certificate
         )
+        fingerprint = hashlib.sha256(tls_certificate_bytes).hexdigest()
         return self._simple_insert(
             table="server_tls_certificates",
-            keyvalues={
+            values={
                 "server_name": server_name,
-                "key_server": key_server,
-                "ts_added_ms": ts_now_ms,
-                "tls_certificate": tls_certificate_bytes,
+                "fingerprint": fingerprint,
+                "from_server": from_server,
+                "ts_added_ms": time_now_ms,
+                "tls_certificate": buffer(tls_certificate_bytes),
             },
+            or_ignore=True,
         )
 
     @defer.inlineCallbacks
-    def get_server_verification_key(self, server_name):
-        """Retrieve the NACL verification key for a given server
+    def get_server_verify_keys(self, server_name, key_ids):
+        """Retrieve the NACL verification key for a given server for the given
+        key_ids
         Args:
-            server_name (bytes): The name of the server.
+            server_name (str): The name of the server.
+            key_ids (list of str): List of key_ids to try and look up.
         Returns:
-            (nacl.signing.VerifyKey): The verification key.
+            (list of VerifyKey): The verification keys.
         """
-        verification_key_bytes, = yield self._simple_select_one(
-            table="server_signature_keys",
-            key_values={"server_name": server_name},
-            retcols=("tls_certificate",),
+        sql = (
+            "SELECT key_id, verify_key FROM server_signature_keys"
+            " WHERE server_name = ?"
+            " AND key_id in (" + ",".join("?" for key_id in key_ids) + ")"
         )
-        verification_key = nacl.signing.VerifyKey(verification_key_bytes)
-        defer.returnValue(verification_key)
 
-    def store_server_verification_key(self, server_name, key_version,
-                                      key_server, ts_now_ms, verification_key):
+        rows = yield self._execute_and_decode(sql, server_name, *key_ids)
+
+        keys = []
+        for row in rows:
+            key_id = row["key_id"]
+            key_bytes = row["verify_key"]
+            key = decode_verify_key_bytes(key_id, str(key_bytes))
+            keys.append(key)
+        defer.returnValue(keys)
+
+    def store_server_verify_key(self, server_name, from_server, time_now_ms,
+                                verify_key):
         """Stores a NACL verification key for the given server.
         Args:
-            server_name (bytes): The name of the server.
-            key_version (bytes): The version of the key for the server.
-            key_server (bytes): Where the verification key was looked up
+            server_name (str): The name of the server.
+            key_id (str): The version of the key for the server.
+            from_server (str): Where the verification key was looked up
             ts_now_ms (int): The time now in milliseconds
-            verification_key (nacl.signing.VerifyKey): The NACL verify key.
+            verification_key (VerifyKey): The NACL verify key.
         """
-        verification_key_bytes = verification_key.encode()
+        verify_key_bytes = verify_key.encode()
         return self._simple_insert(
             table="server_signature_keys",
-            key_values={
+            values={
                 "server_name": server_name,
-                "key_version": key_version,
-                "key_server": key_server,
-                "ts_added_ms": ts_now_ms,
-                "verification_key": verification_key_bytes,
+                "key_id": "%s:%s" % (verify_key.alg, verify_key.version),
+                "from_server": from_server,
+                "ts_added_ms": time_now_ms,
+                "verify_key": buffer(verify_key.encode()),
             },
+            or_ignore=True,
         )
diff --git a/synapse/storage/schema/keys.sql b/synapse/storage/schema/keys.sql
index 706a1a03ff..9bf2068d84 100644
--- a/synapse/storage/schema/keys.sql
+++ b/synapse/storage/schema/keys.sql
@@ -14,17 +14,18 @@
  */
 CREATE TABLE IF NOT EXISTS server_tls_certificates(
   server_name TEXT, -- Server name.
-  key_server TEXT, -- Which key server the certificate was fetched from.
+  fingerprint TEXT, -- Certificate fingerprint.
+  from_server TEXT, -- Which key server the certificate was fetched from.
   ts_added_ms INTEGER, -- When the certifcate was added.
   tls_certificate BLOB, -- DER encoded x509 certificate.
-  CONSTRAINT uniqueness UNIQUE (server_name)
+  CONSTRAINT uniqueness UNIQUE (server_name, fingerprint)
 );
 
 CREATE TABLE IF NOT EXISTS server_signature_keys(
   server_name TEXT, -- Server name.
-  key_version TEXT, -- Key version.
-  key_server TEXT, -- Which key server the key was fetched form.
+  key_id TEXT, -- Key version.
+  from_server TEXT, -- Which key server the key was fetched form.
   ts_added_ms INTEGER, -- When the key was added.
-  verification_key BLOB, -- NACL verification key.
-  CONSTRAINT uniqueness UNIQUE (server_name, key_version)
+  verify_key BLOB, -- NACL verification key.
+  CONSTRAINT uniqueness UNIQUE (server_name, key_id)
 );
diff --git a/synapse/storage/transactions.py b/synapse/storage/transactions.py
index ab4599b468..2ba8e30efe 100644
--- a/synapse/storage/transactions.py
+++ b/synapse/storage/transactions.py
@@ -87,7 +87,8 @@ class TransactionStore(SQLBaseStore):
 
         txn.execute(query, (code, response_json, transaction_id, origin))
 
-    def prep_send_transaction(self, transaction_id, destination, ts, pdu_list):
+    def prep_send_transaction(self, transaction_id, destination,
+                              origin_server_ts, pdu_list):
         """Persists an outgoing transaction and calculates the values for the
         previous transaction id list.
 
@@ -97,7 +98,7 @@ class TransactionStore(SQLBaseStore):
         Args:
             transaction_id (str)
             destination (str)
-            ts (int)
+            origin_server_ts (int)
             pdu_list (list)
 
         Returns:
@@ -106,11 +107,11 @@ class TransactionStore(SQLBaseStore):
 
         return self.runInteraction(
             self._prep_send_transaction,
-            transaction_id, destination, ts, pdu_list
+            transaction_id, destination, origin_server_ts, pdu_list
         )
 
-    def _prep_send_transaction(self, txn, transaction_id, destination, ts,
-                               pdu_list):
+    def _prep_send_transaction(self, txn, transaction_id, destination,
+                               origin_server_ts, pdu_list):
 
         # First we find out what the prev_txs should be.
         # Since we know that we are only sending one transaction at a time,
@@ -131,7 +132,7 @@ class TransactionStore(SQLBaseStore):
             None,
             transaction_id=transaction_id,
             destination=destination,
-            ts=ts,
+            ts=origin_server_ts,
             response_code=0,
             response_json=None
         ))
diff --git a/tests/federation/test_federation.py b/tests/federation/test_federation.py
index d95b9013a3..933aa61c77 100644
--- a/tests/federation/test_federation.py
+++ b/tests/federation/test_federation.py
@@ -19,7 +19,7 @@ from tests import unittest
 # python imports
 from mock import Mock, ANY
 
-from ..utils import MockHttpResource, MockClock
+from ..utils import MockHttpResource, MockClock, MockKey
 
 from synapse.server import HomeServer
 from synapse.federation import initialize_http_replication
@@ -64,6 +64,8 @@ class FederationTestCase(unittest.TestCase):
         self.mock_persistence.get_received_txn_response.return_value = (
                 defer.succeed(None)
         )
+        self.mock_config = Mock()
+        self.mock_config.signing_key = [MockKey()]
         self.clock = MockClock()
         hs = HomeServer("test",
                 resource_for_federation=self.mock_resource,
@@ -71,6 +73,8 @@ class FederationTestCase(unittest.TestCase):
                 db_pool=None,
                 datastore=self.mock_persistence,
                 clock=self.clock,
+                config=self.mock_config,
+                keyring=Mock(),
         )
         self.federation = initialize_http_replication(hs)
         self.distributor = hs.get_distributor()
@@ -154,7 +158,7 @@ class FederationTestCase(unittest.TestCase):
                 origin="red",
                 destinations=["remote"],
                 context="my-context",
-                ts=123456789002,
+                origin_server_ts=123456789002,
                 pdu_type="m.test",
                 content={"testing": "content here"},
                 depth=1,
@@ -166,14 +170,14 @@ class FederationTestCase(unittest.TestCase):
                 "remote",
                 path="/_matrix/federation/v1/send/1000000/",
                 data={
-                    "ts": 1000000,
+                    "origin_server_ts": 1000000,
                     "origin": "test",
                     "pdus": [
                         {
                             "origin": "red",
                             "pdu_id": "abc123def456",
                             "prev_pdus": [],
-                            "ts": 123456789002,
+                            "origin_server_ts": 123456789002,
                             "context": "my-context",
                             "pdu_type": "m.test",
                             "is_state": False,
@@ -182,7 +186,7 @@ class FederationTestCase(unittest.TestCase):
                         },
                     ]
                 },
-                on_send_callback=ANY,
+                json_data_callback=ANY,
         )
 
     @defer.inlineCallbacks
@@ -203,10 +207,11 @@ class FederationTestCase(unittest.TestCase):
                 path="/_matrix/federation/v1/send/1000000/",
                 data={
                     "origin": "test",
-                    "ts": 1000000,
+                    "origin_server_ts": 1000000,
                     "pdus": [],
                     "edus": [
                         {
+                            # TODO: SYN-103: Remove "origin" and "destination"
                             "origin": "test",
                             "destination": "remote",
                             "edu_type": "m.test",
@@ -214,9 +219,10 @@ class FederationTestCase(unittest.TestCase):
                         }
                     ],
                 },
-                on_send_callback=ANY,
+                json_data_callback=ANY,
         )
 
+
     @defer.inlineCallbacks
     def test_recv_edu(self):
         recv_observer = Mock()
@@ -228,7 +234,7 @@ class FederationTestCase(unittest.TestCase):
                 "/_matrix/federation/v1/send/1001000/",
                 """{
                     "origin": "remote",
-                    "ts": 1001000,
+                    "origin_server_ts": 1001000,
                     "pdus": [],
                     "edus": [
                         {
diff --git a/tests/federation/test_pdu_codec.py b/tests/federation/test_pdu_codec.py
index 344e1baf60..0754ef92e8 100644
--- a/tests/federation/test_pdu_codec.py
+++ b/tests/federation/test_pdu_codec.py
@@ -68,7 +68,7 @@ class PduCodecTestCase(unittest.TestCase):
             context="rooooom",
             pdu_type="m.room.message",
             origin="bar.com",
-            ts=12345,
+            origin_server_ts=12345,
             depth=5,
             prev_pdus=[("alice", "bob.com")],
             is_state=False,
@@ -123,7 +123,7 @@ class PduCodecTestCase(unittest.TestCase):
             context="rooooom",
             pdu_type="m.room.topic",
             origin="bar.com",
-            ts=12345,
+            origin_server_ts=12345,
             depth=5,
             prev_pdus=[("alice", "bob.com")],
             is_state=True,
diff --git a/tests/handlers/test_federation.py b/tests/handlers/test_federation.py
index 7208afdb3b..219b2c4c5e 100644
--- a/tests/handlers/test_federation.py
+++ b/tests/handlers/test_federation.py
@@ -26,12 +26,16 @@ from synapse.federation.units import Pdu
 
 from mock import NonCallableMock, ANY
 
-from ..utils import get_mock_call_args
+from ..utils import get_mock_call_args, MockKey
 
 
 class FederationTestCase(unittest.TestCase):
 
     def setUp(self):
+
+        self.mock_config = NonCallableMock()
+        self.mock_config.signing_key = [MockKey()]
+
         self.hostname = "test"
         hs = HomeServer(
             self.hostname,
@@ -48,6 +52,7 @@ class FederationTestCase(unittest.TestCase):
                 "room_member_handler",
                 "federation_handler",
             ]),
+            config=self.mock_config,
         )
 
         self.datastore = hs.get_datastore()
@@ -63,7 +68,7 @@ class FederationTestCase(unittest.TestCase):
             pdu_type=MessageEvent.TYPE,
             context="foo",
             content={"msgtype": u"fooo"},
-            ts=0,
+            origin_server_ts=0,
             pdu_id="a",
             origin="b",
         )
@@ -90,7 +95,7 @@ class FederationTestCase(unittest.TestCase):
             target_host=self.hostname,
             context=room_id,
             content={},
-            ts=0,
+            origin_server_ts=0,
             pdu_id="a",
             origin="b",
         )
@@ -122,7 +127,7 @@ class FederationTestCase(unittest.TestCase):
             state_key="@red:not%s" % self.hostname,
             context=room_id,
             content={},
-            ts=0,
+            origin_server_ts=0,
             pdu_id="a",
             origin="b",
         )
diff --git a/tests/handlers/test_presence.py b/tests/handlers/test_presence.py
index 765929d204..1850deacf5 100644
--- a/tests/handlers/test_presence.py
+++ b/tests/handlers/test_presence.py
@@ -17,11 +17,12 @@
 from tests import unittest
 from twisted.internet import defer, reactor
 
-from mock import Mock, call, ANY
+from mock import Mock, call, ANY, NonCallableMock, patch
 import json
 
 from tests.utils import (
-    MockHttpResource, MockClock, DeferredMockCallable, SQLiteMemoryDbPool
+    MockHttpResource, MockClock, DeferredMockCallable, SQLiteMemoryDbPool,
+    MockKey
 )
 
 from synapse.server import HomeServer
@@ -38,10 +39,11 @@ ONLINE = PresenceState.ONLINE
 def _expect_edu(destination, edu_type, content, origin="test"):
     return {
         "origin": origin,
-        "ts": 1000000,
+        "origin_server_ts": 1000000,
         "pdus": [],
         "edus": [
             {
+                # TODO: SYN-103: Remove "origin" and "destination" keys.
                 "origin": origin,
                 "destination": destination,
                 "edu_type": edu_type,
@@ -58,7 +60,6 @@ class JustPresenceHandlers(object):
     def __init__(self, hs):
         self.presence_handler = PresenceHandler(hs)
 
-
 class PresenceStateTestCase(unittest.TestCase):
     """ Tests presence management. """
 
@@ -67,12 +68,17 @@ class PresenceStateTestCase(unittest.TestCase):
         db_pool = SQLiteMemoryDbPool()
         yield db_pool.prepare()
 
+        self.mock_config = NonCallableMock()
+        self.mock_config.signing_key = [MockKey()]
+
         hs = HomeServer("test",
             clock=MockClock(),
             db_pool=db_pool,
             handlers=None,
             resource_for_federation=Mock(),
             http_client=None,
+            config=self.mock_config,
+            keyring=Mock(),
         )
         hs.handlers = JustPresenceHandlers(hs)
 
@@ -214,6 +220,9 @@ class PresenceInvitesTestCase(unittest.TestCase):
         db_pool = SQLiteMemoryDbPool()
         yield db_pool.prepare()
 
+        self.mock_config = NonCallableMock()
+        self.mock_config.signing_key = [MockKey()]
+
         hs = HomeServer("test",
             clock=MockClock(),
             db_pool=db_pool,
@@ -221,6 +230,8 @@ class PresenceInvitesTestCase(unittest.TestCase):
             resource_for_client=Mock(),
             resource_for_federation=self.mock_federation_resource,
             http_client=self.mock_http_client,
+            config=self.mock_config,
+            keyring=Mock(),
         )
         hs.handlers = JustPresenceHandlers(hs)
 
@@ -290,7 +301,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
                         "observed_user": "@cabbage:elsewhere",
                     }
                 ),
-                on_send_callback=ANY,
+                json_data_callback=ANY,
             ),
             defer.succeed((200, "OK"))
         )
@@ -319,7 +330,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
                         "observed_user": "@apple:test",
                     }
                 ),
-                on_send_callback=ANY,
+                json_data_callback=ANY,
             ),
             defer.succeed((200, "OK"))
         )
@@ -355,7 +366,7 @@ class PresenceInvitesTestCase(unittest.TestCase):
                         "observed_user": "@durian:test",
                     }
                 ),
-                on_send_callback=ANY,
+                json_data_callback=ANY,
             ),
             defer.succeed((200, "OK"))
         )
@@ -503,6 +514,9 @@ class PresencePushTestCase(unittest.TestCase):
 
         self.mock_federation_resource = MockHttpResource()
 
+        self.mock_config = NonCallableMock()
+        self.mock_config.signing_key = [MockKey()]
+
         hs = HomeServer("test",
                 clock=self.clock,
                 db_pool=None,
@@ -520,6 +534,8 @@ class PresencePushTestCase(unittest.TestCase):
                 resource_for_client=Mock(),
                 resource_for_federation=self.mock_federation_resource,
                 http_client=self.mock_http_client,
+                config=self.mock_config,
+                keyring=Mock(),
             )
         hs.handlers = JustPresenceHandlers(hs)
 
@@ -771,7 +787,7 @@ class PresencePushTestCase(unittest.TestCase):
                         ],
                     }
                 ),
-                on_send_callback=ANY,
+                json_data_callback=ANY,
             ),
             defer.succeed((200, "OK"))
         )
@@ -787,7 +803,7 @@ class PresencePushTestCase(unittest.TestCase):
                         ],
                     }
                 ),
-                on_send_callback=ANY,
+                json_data_callback=ANY,
             ),
             defer.succeed((200, "OK"))
         )
@@ -913,7 +929,7 @@ class PresencePushTestCase(unittest.TestCase):
                         ],
                     }
                 ),
-                on_send_callback=ANY,
+                json_data_callback=ANY,
             ),
             defer.succeed((200, "OK"))
         )
@@ -928,7 +944,7 @@ class PresencePushTestCase(unittest.TestCase):
                         ],
                     }
                 ),
-                on_send_callback=ANY,
+                json_data_callback=ANY,
             ),
             defer.succeed((200, "OK"))
         )
@@ -958,7 +974,7 @@ class PresencePushTestCase(unittest.TestCase):
                         ],
                     }
                 ),
-                on_send_callback=ANY,
+                json_data_callback=ANY,
             ),
             defer.succeed((200, "OK"))
         )
@@ -995,6 +1011,9 @@ class PresencePollingTestCase(unittest.TestCase):
 
         self.mock_federation_resource = MockHttpResource()
 
+        self.mock_config = NonCallableMock()
+        self.mock_config.signing_key = [MockKey()]
+
         hs = HomeServer("test",
                 clock=MockClock(),
                 db_pool=None,
@@ -1009,6 +1028,8 @@ class PresencePollingTestCase(unittest.TestCase):
                 resource_for_client=Mock(),
                 resource_for_federation=self.mock_federation_resource,
                 http_client=self.mock_http_client,
+                config=self.mock_config,
+                keyring=Mock(),
             )
         hs.handlers = JustPresenceHandlers(hs)
 
@@ -1155,7 +1176,7 @@ class PresencePollingTestCase(unittest.TestCase):
                         "poll": [ "@potato:remote" ],
                     },
                 ),
-                on_send_callback=ANY,
+                json_data_callback=ANY,
             ),
             defer.succeed((200, "OK"))
         )
@@ -1168,7 +1189,7 @@ class PresencePollingTestCase(unittest.TestCase):
                         "push": [ {"user_id": "@clementine:test" }],
                     },
                 ),
-                on_send_callback=ANY,
+                json_data_callback=ANY,
             ),
             defer.succeed((200, "OK"))
         )
@@ -1197,7 +1218,7 @@ class PresencePollingTestCase(unittest.TestCase):
                         "push": [ {"user_id": "@fig:test" }],
                     },
                 ),
-                on_send_callback=ANY,
+                json_data_callback=ANY,
             ),
             defer.succeed((200, "OK"))
         )
@@ -1230,7 +1251,7 @@ class PresencePollingTestCase(unittest.TestCase):
                         "unpoll": [ "@potato:remote" ],
                     },
                 ),
-                on_send_callback=ANY,
+                json_data_callback=ANY,
             ),
             defer.succeed((200, "OK"))
         )
@@ -1262,7 +1283,7 @@ class PresencePollingTestCase(unittest.TestCase):
                         ],
                     },
                 ),
-                on_send_callback=ANY,
+                json_data_callback=ANY,
             ),
             defer.succeed((200, "OK"))
         )
diff --git a/tests/handlers/test_room.py b/tests/handlers/test_room.py
index a1a2e80492..c88d1c8840 100644
--- a/tests/handlers/test_room.py
+++ b/tests/handlers/test_room.py
@@ -24,6 +24,7 @@ from synapse.api.constants import Membership
 from synapse.handlers.room import RoomMemberHandler, RoomCreationHandler
 from synapse.handlers.profile import ProfileHandler
 from synapse.server import HomeServer
+from ..utils import MockKey
 
 from mock import Mock, NonCallableMock
 
@@ -31,6 +32,8 @@ from mock import Mock, NonCallableMock
 class RoomMemberHandlerTestCase(unittest.TestCase):
 
     def setUp(self):
+        self.mock_config = NonCallableMock()
+        self.mock_config.signing_key = [MockKey()]
         self.hostname = "red"
         hs = HomeServer(
             self.hostname,
@@ -38,7 +41,6 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
             ratelimiter=NonCallableMock(spec_set=[
                 "send_message",
             ]),
-            config=NonCallableMock(),
             datastore=NonCallableMock(spec_set=[
                 "persist_event",
                 "get_joined_hosts_for_room",
@@ -57,6 +59,7 @@ class RoomMemberHandlerTestCase(unittest.TestCase):
             ]),
             auth=NonCallableMock(spec_set=["check"]),
             state_handler=NonCallableMock(spec_set=["handle_new_event"]),
+            config=self.mock_config,
         )
 
         self.federation = NonCallableMock(spec_set=[
diff --git a/tests/handlers/test_typing.py b/tests/handlers/test_typing.py
index a66f208abf..f1d3b27f74 100644
--- a/tests/handlers/test_typing.py
+++ b/tests/handlers/test_typing.py
@@ -20,7 +20,7 @@ from twisted.internet import defer
 from mock import Mock, call, ANY
 import json
 
-from ..utils import MockHttpResource, MockClock, DeferredMockCallable
+from ..utils import MockHttpResource, MockClock, DeferredMockCallable, MockKey
 
 from synapse.server import HomeServer
 from synapse.handlers.typing import TypingNotificationHandler
@@ -29,10 +29,11 @@ from synapse.handlers.typing import TypingNotificationHandler
 def _expect_edu(destination, edu_type, content, origin="test"):
     return {
         "origin": origin,
-        "ts": 1000000,
+        "origin_server_ts": 1000000,
         "pdus": [],
         "edus": [
             {
+                # TODO: SYN-103: Remove "origin" and "destination" keys.
                 "origin": origin,
                 "destination": destination,
                 "edu_type": edu_type,
@@ -61,6 +62,9 @@ class TypingNotificationsTestCase(unittest.TestCase):
 
         self.mock_federation_resource = MockHttpResource()
 
+        self.mock_config = Mock()
+        self.mock_config.signing_key = [MockKey()]
+
         hs = HomeServer("test",
                 clock=self.clock,
                 db_pool=None,
@@ -75,6 +79,8 @@ class TypingNotificationsTestCase(unittest.TestCase):
                 resource_for_client=Mock(),
                 resource_for_federation=self.mock_federation_resource,
                 http_client=self.mock_http_client,
+                config=self.mock_config,
+                keyring=Mock(),
             )
         hs.handlers = JustTypingNotificationHandlers(hs)
 
@@ -170,7 +176,7 @@ class TypingNotificationsTestCase(unittest.TestCase):
                         "typing": True,
                     }
                 ),
-                on_send_callback=ANY,
+                json_data_callback=ANY,
             ),
             defer.succeed((200, "OK"))
         )
@@ -221,7 +227,7 @@ class TypingNotificationsTestCase(unittest.TestCase):
                         "typing": False,
                     }
                 ),
-                on_send_callback=ANY,
+                json_data_callback=ANY,
             ),
             defer.succeed((200, "OK"))
         )
diff --git a/tests/rest/test_presence.py b/tests/rest/test_presence.py
index e2dc3dec81..769c7824bc 100644
--- a/tests/rest/test_presence.py
+++ b/tests/rest/test_presence.py
@@ -20,7 +20,7 @@ from twisted.internet import defer
 
 from mock import Mock
 
-from ..utils import MockHttpResource
+from ..utils import MockHttpResource, MockKey
 
 from synapse.api.constants import PresenceState
 from synapse.handlers.presence import PresenceHandler
@@ -45,7 +45,8 @@ class PresenceStateTestCase(unittest.TestCase):
 
     def setUp(self):
         self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
-
+        self.mock_config = Mock()
+        self.mock_config.signing_key = [MockKey()]
         hs = HomeServer("test",
             db_pool=None,
             datastore=Mock(spec=[
@@ -56,7 +57,7 @@ class PresenceStateTestCase(unittest.TestCase):
             http_client=None,
             resource_for_client=self.mock_resource,
             resource_for_federation=self.mock_resource,
-            config=Mock(),
+            config=self.mock_config,
         )
         hs.handlers = JustPresenceHandlers(hs)
 
@@ -125,6 +126,8 @@ class PresenceListTestCase(unittest.TestCase):
 
     def setUp(self):
         self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
+        self.mock_config = Mock()
+        self.mock_config.signing_key = [MockKey()]
 
         hs = HomeServer("test",
             db_pool=None,
@@ -142,7 +145,7 @@ class PresenceListTestCase(unittest.TestCase):
             http_client=None,
             resource_for_client=self.mock_resource,
             resource_for_federation=self.mock_resource,
-            config=Mock(),
+            config=self.mock_config,
         )
         hs.handlers = JustPresenceHandlers(hs)
 
@@ -237,6 +240,9 @@ class PresenceEventStreamTestCase(unittest.TestCase):
     def setUp(self):
         self.mock_resource = MockHttpResource(prefix=PATH_PREFIX)
 
+        self.mock_config = Mock()
+        self.mock_config.signing_key = [MockKey()]
+
         # HIDEOUS HACKERY
         # TODO(paul): This should be injected in via the HomeServer DI system
         from synapse.streams.events import (
@@ -267,6 +273,7 @@ class PresenceEventStreamTestCase(unittest.TestCase):
                 "cancel_call_later",
                 "time_msec",
             ]),
+            config=self.mock_config,
         )
 
         hs.get_clock().time_msec.return_value = 1000000
diff --git a/tests/test_state.py b/tests/test_state.py
index b1624f0b25..4b1feaf410 100644
--- a/tests/test_state.py
+++ b/tests/test_state.py
@@ -599,7 +599,7 @@ def new_fake_pdu(pdu_id, context, pdu_type, state_key, prev_state_id,
         prev_state_id=prev_state_id,
         origin="example.com",
         context="context",
-        ts=1405353060021,
+        origin_server_ts=1405353060021,
         depth=depth,
         content_json="{}",
         unrecognized_keys="{}",
diff --git a/tests/utils.py b/tests/utils.py
index e7c4bc4cad..60fd6085ac 100644
--- a/tests/utils.py
+++ b/tests/utils.py
@@ -76,6 +76,13 @@ class MockHttpResource(HttpServer):
         mock_content.configure_mock(**config)
         mock_request.content = mock_content
 
+        mock_request.method = http_method
+        mock_request.uri = path
+
+        mock_request.requestHeaders.getRawHeaders.return_value=[
+            "X-Matrix origin=test,key=,sig="
+        ]
+
         # return the right path if the event requires it
         mock_request.path = path
 
@@ -108,6 +115,21 @@ class MockHttpResource(HttpServer):
         self.callbacks.append((method, path_pattern, callback))
 
 
+class MockKey(object):
+    alg = "mock_alg"
+    version = "mock_version"
+
+    @property
+    def verify_key(self):
+        return self
+
+    def sign(self, message):
+        return b"\x9a\x87$"
+
+    def verify(self, message, sig):
+        assert sig == b"\x9a\x87$"
+
+
 class MockClock(object):
     now = 1000
 
diff --git a/webclient/app-filter.js b/webclient/app-filter.js
index fc16492ef3..39ea1d637d 100644
--- a/webclient/app-filter.js
+++ b/webclient/app-filter.js
@@ -1,12 +1,12 @@
 /*
  Copyright 2014 OpenMarket Ltd
- 
+
  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.
@@ -80,4 +80,53 @@ angular.module('matrixWebClient')
     return function(text) {
         return $sce.trustAsHtml(text);
     };
-}]);
\ No newline at end of file
+}])
+// Exactly the same as ngSanitize's linky but instead of pushing sanitized
+// text in the addText function, we just push the raw text.
+.filter('unsanitizedLinky', ['$sanitize', function($sanitize) {
+  var LINKY_URL_REGEXP =
+        /((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"]/,
+      MAILTO_REGEXP = /^mailto:/;
+
+  return function(text, target) {
+    if (!text) return text;
+    var match;
+    var raw = text;
+    var html = [];
+    var url;
+    var i;
+    while ((match = raw.match(LINKY_URL_REGEXP))) {
+      // We can not end in these as they are sometimes found at the end of the sentence
+      url = match[0];
+      // if we did not match ftp/http/mailto then assume mailto
+      if (match[2] == match[3]) url = 'mailto:' + url;
+      i = match.index;
+      addText(raw.substr(0, i));
+      addLink(url, match[0].replace(MAILTO_REGEXP, ''));
+      raw = raw.substring(i + match[0].length);
+    }
+    addText(raw);
+    return $sanitize(html.join(''));
+
+    function addText(text) {
+      if (!text) {
+        return;
+      }
+      html.push(text);
+    }
+
+    function addLink(url, text) {
+      html.push('<a ');
+      if (angular.isDefined(target)) {
+        html.push('target="');
+        html.push(target);
+        html.push('" ');
+      }
+      html.push('href="');
+      html.push(url);
+      html.push('">');
+      addText(text);
+      html.push('</a>');
+    }
+  };
+}]);
diff --git a/webclient/room/room.html b/webclient/room/room.html
index b99413cbbf..79a60585a5 100644
--- a/webclient/room/room.html
+++ b/webclient/room/room.html
@@ -121,7 +121,9 @@
                         <span ng-show='msg.content.msgtype === "m.text"' 
                               class="message"
                               ng-class="containsBingWord(msg.content.body) && msg.user_id != state.user_id ? msg.echo_msg_state + ' messageBing' : msg.echo_msg_state"
-                              ng-bind-html="((msg.content.msgtype === 'm.text') ? msg.content.body : '') | linky:'_blank'"/>
+                              ng-bind-html="(msg.content.msgtype === 'm.text' && msg.type === 'm.room.message' && msg.content.format === 'org.matrix.custom.html') ? 
+                                                                                        (msg.content.formatted_body | unsanitizedLinky) :
+                                             (msg.content.msgtype === 'm.text' && msg.type === 'm.room.message') ? (msg.content.body | linky:'_blank') : '' "/>
 
                         <span ng-show='msg.type === "m.call.invite" && msg.user_id == state.user_id'>Outgoing Call{{ isWebRTCSupported ? '' : ' (But your browser does not support VoIP)' }}</span>
                         <span ng-show='msg.type === "m.call.invite" && msg.user_id != state.user_id'>Incoming Call{{ isWebRTCSupported ? '' : ' (But your browser does not support VoIP)' }}</span>