From 0309035aad2ff21583b5da3f688a328981562f15 Mon Sep 17 00:00:00 2001 From: Mark Haines Date: Fri, 5 May 2017 16:19:48 +0100 Subject: [PATCH] Update gomatrixserverlib (#89) --- .../dendrite/clientapi/config/config.go | 7 +- .../dendrite/cmd/create-room-events/main.go | 2 +- vendor/manifest | 4 +- .../matrix-org/gomatrixserverlib/LICENSE | 177 ++++++++++ .../matrix-org/gomatrixserverlib/client.go | 79 +++++ .../matrix-org/gomatrixserverlib/event.go | 30 +- .../gomatrixserverlib/eventcrypto.go | 4 +- .../gomatrixserverlib/eventcrypto_test.go | 4 +- .../matrix-org/gomatrixserverlib/keyring.go | 308 ++++++++++++++++++ .../gomatrixserverlib/keyring_test.go | 139 ++++++++ .../matrix-org/gomatrixserverlib/keys.go | 116 +++++-- .../matrix-org/gomatrixserverlib/request.go | 307 +++++++++++++++++ .../gomatrixserverlib/request_test.go | 175 ++++++++++ .../matrix-org/gomatrixserverlib/signing.go | 33 +- .../gomatrixserverlib/signing_test.go | 8 +- .../gomatrixserverlib/stateresolution.go | 15 + .../matrix-org/gomatrixserverlib/timestamp.go | 18 + 17 files changed, 1355 insertions(+), 71 deletions(-) create mode 100644 vendor/src/github.com/matrix-org/gomatrixserverlib/LICENSE create mode 100644 vendor/src/github.com/matrix-org/gomatrixserverlib/keyring.go create mode 100644 vendor/src/github.com/matrix-org/gomatrixserverlib/keyring_test.go create mode 100644 vendor/src/github.com/matrix-org/gomatrixserverlib/request.go create mode 100644 vendor/src/github.com/matrix-org/gomatrixserverlib/request_test.go create mode 100644 vendor/src/github.com/matrix-org/gomatrixserverlib/timestamp.go diff --git a/src/github.com/matrix-org/dendrite/clientapi/config/config.go b/src/github.com/matrix-org/dendrite/clientapi/config/config.go index 4413cb99..db5815f9 100644 --- a/src/github.com/matrix-org/dendrite/clientapi/config/config.go +++ b/src/github.com/matrix-org/dendrite/clientapi/config/config.go @@ -14,7 +14,10 @@ package config -import "golang.org/x/crypto/ed25519" +import ( + "github.com/matrix-org/gomatrixserverlib" + "golang.org/x/crypto/ed25519" +) // ClientAPI contains the config information necessary to spin up a clientapi process. type ClientAPI struct { @@ -24,7 +27,7 @@ type ClientAPI struct { PrivateKey ed25519.PrivateKey // An arbitrary string used to uniquely identify the PrivateKey. Must start with the // prefix "ed25519:". - KeyID string + KeyID gomatrixserverlib.KeyID // A list of URIs to send events to. These kafka logs should be consumed by a Room Server. KafkaProducerURIs []string // The topic for events which are written to the logs. diff --git a/src/github.com/matrix-org/dendrite/cmd/create-room-events/main.go b/src/github.com/matrix-org/dendrite/cmd/create-room-events/main.go index 275d41b0..c9b2c599 100644 --- a/src/github.com/matrix-org/dendrite/cmd/create-room-events/main.go +++ b/src/github.com/matrix-org/dendrite/cmd/create-room-events/main.go @@ -111,7 +111,7 @@ func buildAndOutput() gomatrixserverlib.EventReference { eventID++ id := fmt.Sprintf("$%d:%s", eventID, *serverName) now = time.Unix(0, 0) - event, err := b.Build(id, now, *serverName, *keyID, privateKey) + event, err := b.Build(id, now, *serverName, gomatrixserverlib.KeyID(*keyID), privateKey) if err != nil { panic(err) } diff --git a/vendor/manifest b/vendor/manifest index 2cc75e57..0bb2e207 100644 --- a/vendor/manifest +++ b/vendor/manifest @@ -92,7 +92,7 @@ { "importpath": "github.com/matrix-org/gomatrixserverlib", "repository": "https://github.com/matrix-org/gomatrixserverlib", - "revision": "8e847d5bdb5cc0dd11e0846188eb403b70ae6bb3", + "revision": "fd3b9faf8492989e8f782592d37dcbdc01c44fea", "branch": "master" }, { @@ -212,4 +212,4 @@ "branch": "v2" } ] -} +} \ No newline at end of file diff --git a/vendor/src/github.com/matrix-org/gomatrixserverlib/LICENSE b/vendor/src/github.com/matrix-org/gomatrixserverlib/LICENSE new file mode 100644 index 00000000..f433b1a5 --- /dev/null +++ b/vendor/src/github.com/matrix-org/gomatrixserverlib/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/vendor/src/github.com/matrix-org/gomatrixserverlib/client.go b/vendor/src/github.com/matrix-org/gomatrixserverlib/client.go index 31228ac9..fe47f04d 100644 --- a/vendor/src/github.com/matrix-org/gomatrixserverlib/client.go +++ b/vendor/src/github.com/matrix-org/gomatrixserverlib/client.go @@ -16,6 +16,7 @@ package gomatrixserverlib import ( + "bytes" "crypto/tls" "encoding/json" "fmt" @@ -141,3 +142,81 @@ func (fc *Client) LookupUserInfo(matrixServer, token string) (u UserInfo, err er return } + +// LookupServerKeys lookups up the keys for a matrix server from a matrix server. +// The first argument is the name of the matrix server to download the keys from. +// The second argument is a map from (server name, key ID) pairs to timestamps. +// The (server name, key ID) pair identifies the key to download. +// The timestamps tell the server when the keys need to be valid until. +// Perspective servers can use that timestamp to determine whether they can +// return a cached copy of the keys or whether they will need to retrieve a fresh +// copy of the keys. +// Returns the keys or an error if there was a problem talking to the server. +func (fc *Client) LookupServerKeys( + matrixServer string, keyRequests map[PublicKeyRequest]Timestamp, +) (map[PublicKeyRequest]ServerKeys, error) { + url := url.URL{ + Scheme: "matrix", + Host: matrixServer, + Path: "/_matrix/key/v2/query", + } + + // The request format is: + // { "server_keys": { "": { "": { "minimum_valid_until_ts": }}} + type keyreq struct { + MinimumValidUntilTS Timestamp `json:"minimum_valid_until_ts"` + } + request := struct { + ServerKeyMap map[string]map[KeyID]keyreq `json:"server_keys"` + }{map[string]map[KeyID]keyreq{}} + for k, ts := range keyRequests { + server := request.ServerKeyMap[k.ServerName] + if server == nil { + server = map[KeyID]keyreq{} + request.ServerKeyMap[k.ServerName] = server + } + server[k.KeyID] = keyreq{ts} + } + + requestBytes, err := json.Marshal(request) + if err != nil { + return nil, err + } + + response, err := fc.client.Post(url.String(), "application/json", bytes.NewBuffer(requestBytes)) + if response != nil { + defer response.Body.Close() + } + if err != nil { + return nil, err + } + + if response.StatusCode != 200 { + var errorOutput []byte + if errorOutput, err = ioutil.ReadAll(response.Body); err != nil { + return nil, err + } + return nil, fmt.Errorf("HTTP %d : %s", response.StatusCode, errorOutput) + } + + var body struct { + ServerKeyList []ServerKeys `json:"server_keys"` + } + if err = json.NewDecoder(response.Body).Decode(&body); err != nil { + return nil, err + } + + result := map[PublicKeyRequest]ServerKeys{} + for _, keys := range body.ServerKeyList { + keys.FromServer = matrixServer + // TODO: What happens if the same key ID appears in multiple responses? + // We should probably take the response with the highest valid_until_ts. + for keyID := range keys.VerifyKeys { + result[PublicKeyRequest{keys.ServerName, keyID}] = keys + } + for keyID := range keys.OldVerifyKeys { + result[PublicKeyRequest{keys.ServerName, keyID}] = keys + } + } + return result, nil +} diff --git a/vendor/src/github.com/matrix-org/gomatrixserverlib/event.go b/vendor/src/github.com/matrix-org/gomatrixserverlib/event.go index 77851086..ddb5e057 100644 --- a/vendor/src/github.com/matrix-org/gomatrixserverlib/event.go +++ b/vendor/src/github.com/matrix-org/gomatrixserverlib/event.go @@ -109,14 +109,14 @@ var emptyEventReferenceList = []EventReference{} // Call this after filling out the necessary fields. // This can be called mutliple times on the same builder. // A different event ID must be supplied each time this is called. -func (eb *EventBuilder) Build(eventID string, now time.Time, origin, keyID string, privateKey ed25519.PrivateKey) (result Event, err error) { +func (eb *EventBuilder) Build(eventID string, now time.Time, origin string, keyID KeyID, privateKey ed25519.PrivateKey) (result Event, err error) { var event struct { EventBuilder - EventID string `json:"event_id"` - RawContent rawJSON `json:"content"` - RawUnsigned rawJSON `json:"unsigned,omitempty"` - OriginServerTS int64 `json:"origin_server_ts"` - Origin string `json:"origin"` + EventID string `json:"event_id"` + RawContent rawJSON `json:"content"` + RawUnsigned rawJSON `json:"unsigned,omitempty"` + OriginServerTS Timestamp `json:"origin_server_ts"` + Origin string `json:"origin"` } event.EventBuilder = *eb if event.PrevEvents == nil { @@ -127,7 +127,7 @@ func (eb *EventBuilder) Build(eventID string, now time.Time, origin, keyID strin } event.RawContent = rawJSON(event.content) event.RawUnsigned = rawJSON(event.unsigned) - event.OriginServerTS = now.UnixNano() / 1000000 + event.OriginServerTS = AsTimestamp(now) event.Origin = origin event.EventID = eventID @@ -245,7 +245,7 @@ func (e Event) EventReference() EventReference { } // Sign returns a copy of the event with an additional signature. -func (e Event) Sign(signingName, keyID string, privateKey ed25519.PrivateKey) Event { +func (e Event) Sign(signingName string, keyID KeyID, privateKey ed25519.PrivateKey) Event { eventJSON, err := signEvent(signingName, keyID, privateKey, e.eventJSON) if err != nil { // This is unreachable for events created with EventBuilder.Build or NewEventFromUntrustedJSON @@ -262,23 +262,17 @@ func (e Event) Sign(signingName, keyID string, privateKey ed25519.PrivateKey) Ev } // KeyIDs returns a list of key IDs that the named entity has signed the event with. -func (e Event) KeyIDs(signingName string) []string { - var event struct { - Signatures map[string]map[string]rawJSON `json:"signatures"` - } - if err := json.Unmarshal(e.eventJSON, &event); err != nil { +func (e Event) KeyIDs(signingName string) []KeyID { + keyIDs, err := ListKeyIDs(signingName, e.eventJSON) + if err != nil { // This should unreachable for events created with EventBuilder.Build or NewEventFromUntrustedJSON panic(fmt.Errorf("gomatrixserverlib: invalid event %v", err)) } - var keyIDs []string - for keyID := range event.Signatures[signingName] { - keyIDs = append(keyIDs, keyID) - } return keyIDs } // Verify checks a ed25519 signature -func (e Event) Verify(signingName, keyID string, publicKey ed25519.PublicKey) error { +func (e Event) Verify(signingName string, keyID KeyID, publicKey ed25519.PublicKey) error { return verifyEventSignature(signingName, keyID, publicKey, e.eventJSON) } diff --git a/vendor/src/github.com/matrix-org/gomatrixserverlib/eventcrypto.go b/vendor/src/github.com/matrix-org/gomatrixserverlib/eventcrypto.go index ecc0f8fa..80ccf920 100644 --- a/vendor/src/github.com/matrix-org/gomatrixserverlib/eventcrypto.go +++ b/vendor/src/github.com/matrix-org/gomatrixserverlib/eventcrypto.go @@ -142,7 +142,7 @@ func referenceOfEvent(eventJSON []byte) (EventReference, error) { } // SignEvent adds a ED25519 signature to the event for the given key. -func signEvent(signingName, keyID string, privateKey ed25519.PrivateKey, eventJSON []byte) ([]byte, error) { +func signEvent(signingName string, keyID KeyID, privateKey ed25519.PrivateKey, eventJSON []byte) ([]byte, error) { // Redact the event before signing so signature that will remain valid even if the event is redacted. redactedJSON, err := redactEvent(eventJSON) @@ -176,7 +176,7 @@ func signEvent(signingName, keyID string, privateKey ed25519.PrivateKey, eventJS } // VerifyEventSignature checks if the event has been signed by the given ED25519 key. -func verifyEventSignature(signingName, keyID string, publicKey ed25519.PublicKey, eventJSON []byte) error { +func verifyEventSignature(signingName string, keyID KeyID, publicKey ed25519.PublicKey, eventJSON []byte) error { redactedJSON, err := redactEvent(eventJSON) if err != nil { return err diff --git a/vendor/src/github.com/matrix-org/gomatrixserverlib/eventcrypto_test.go b/vendor/src/github.com/matrix-org/gomatrixserverlib/eventcrypto_test.go index 0d8d667c..8f11b854 100644 --- a/vendor/src/github.com/matrix-org/gomatrixserverlib/eventcrypto_test.go +++ b/vendor/src/github.com/matrix-org/gomatrixserverlib/eventcrypto_test.go @@ -30,7 +30,7 @@ func TestVerifyEventSignatureTestVectors(t *testing.T) { } random := bytes.NewBuffer(seed) entityName := "domain" - keyID := "ed25519:1" + keyID := KeyID("ed25519:1") publicKey, _, err := ed25519.GenerateKey(random) if err != nil { @@ -173,7 +173,7 @@ func TestSignEventTestVectors(t *testing.T) { } random := bytes.NewBuffer(seed) entityName := "domain" - keyID := "ed25519:1" + keyID := KeyID("ed25519:1") _, privateKey, err := ed25519.GenerateKey(random) if err != nil { diff --git a/vendor/src/github.com/matrix-org/gomatrixserverlib/keyring.go b/vendor/src/github.com/matrix-org/gomatrixserverlib/keyring.go new file mode 100644 index 00000000..dbe5f9d9 --- /dev/null +++ b/vendor/src/github.com/matrix-org/gomatrixserverlib/keyring.go @@ -0,0 +1,308 @@ +package gomatrixserverlib + +import ( + "fmt" + "golang.org/x/crypto/ed25519" + "strings" + "time" +) + +// A PublicKeyRequest is a request for a public key with a particular key ID. +type PublicKeyRequest struct { + // The server to fetch a key for. + ServerName string + // The ID of the key to fetch. + KeyID KeyID +} + +// A KeyFetcher is a way of fetching public keys in bulk. +type KeyFetcher interface { + // Lookup a batch of public keys. + // Takes a map from (server name, key ID) pairs to timestamp. + // The timestamp is when the keys need to be vaild up to. + // Returns a map from (server name, key ID) pairs to server key objects for + // that server name containing that key ID + // The result may have fewer (server name, key ID) pairs than were in the request. + // The result may have more (server name, key ID) pairs than were in the request. + // Returns an error if there was a problem fetching the keys. + FetchKeys(requests map[PublicKeyRequest]Timestamp) (map[PublicKeyRequest]ServerKeys, error) +} + +// A KeyDatabase is a store for caching public keys. +type KeyDatabase interface { + KeyFetcher + // Add a block of public keys to the database. + StoreKeys(map[PublicKeyRequest]ServerKeys) error +} + +// A KeyRing stores keys for matrix servers and provides methods for verifying JSON messages. +type KeyRing struct { + KeyFetchers []KeyFetcher + KeyDatabase KeyDatabase +} + +// A VerifyJSONRequest is a request to check for a signature on a JSON message. +// A JSON message is valid for a server if the message has at least one valid +// signature from that server. +type VerifyJSONRequest struct { + // The name of the matrix server to check for a signature for. + ServerName string + // The millisecond posix timestamp the message needs to be valid at. + AtTS Timestamp + // The JSON bytes. + Message []byte +} + +// A VerifyJSONResult is the result of checking the signature of a JSON message. +type VerifyJSONResult struct { + // Whether the message passed the signature checks. + // This will be nil if the message passed the checks. + // This will have an error if the message did not pass the checks. + Result error +} + +// VerifyJSONs performs bulk JSON signature verification for a list of VerifyJSONRequests. +// Returns a list of VerifyJSONResults with the same length and order as the request list. +// The caller should check the Result field for each entry to see if it was valid. +// Returns an error if there was a problem talking to the database or one of the other methods +// of fetching the public keys. +func (k *KeyRing) VerifyJSONs(requests []VerifyJSONRequest) ([]VerifyJSONResult, error) { + results := make([]VerifyJSONResult, len(requests)) + keyIDs := make([][]KeyID, len(requests)) + + for i := range requests { + ids, err := ListKeyIDs(requests[i].ServerName, requests[i].Message) + if err != nil { + results[i].Result = fmt.Errorf("gomatrixserverlib: error extracting key IDs") + continue + } + for _, keyID := range ids { + if k.isAlgorithmSupported(keyID) { + keyIDs[i] = append(keyIDs[i], keyID) + } + } + if len(keyIDs[i]) == 0 { + results[i].Result = fmt.Errorf( + "gomatrixserverlib: not signed by %q with a supported algorithm", requests[i].ServerName, + ) + continue + } + // Set a place holder error in the result field. + // This will be unset if one of the signature checks passes. + // This will be overwritten if one of the signature checks fails. + // Therefore this will only remain in place if the keys couldn't be downloaded. + results[i].Result = fmt.Errorf( + "gomatrixserverlib: could not download key for %q", requests[i].ServerName, + ) + } + + keyRequests := k.publicKeyRequests(requests, results, keyIDs) + if len(keyRequests) == 0 { + // There aren't any keys to fetch so we can stop here. + // This will happen if all the objects are missing supported signatures. + return results, nil + } + keysFromDatabase, err := k.KeyDatabase.FetchKeys(keyRequests) + if err != nil { + return nil, err + } + k.checkUsingKeys(requests, results, keyIDs, keysFromDatabase) + + for i := range k.KeyFetchers { + keyRequests := k.publicKeyRequests(requests, results, keyIDs) + if len(keyRequests) == 0 { + // There aren't any keys to fetch so we can stop here. + // This means that we've checked every JSON object we can check. + return results, nil + } + // TODO: Coalesce in-flight requests for the same keys. + // Otherwise we risk spamming the servers we query the keys from. + keysFetched, err := k.KeyFetchers[i].FetchKeys(keyRequests) + if err != nil { + return nil, err + } + k.checkUsingKeys(requests, results, keyIDs, keysFetched) + + // Add the keys to the database so that we won't need to fetch them again. + if err := k.KeyDatabase.StoreKeys(keysFetched); err != nil { + return nil, err + } + } + + return results, nil +} + +func (k *KeyRing) isAlgorithmSupported(keyID KeyID) bool { + return strings.HasPrefix(string(keyID), "ed25519:") +} + +func (k *KeyRing) publicKeyRequests(requests []VerifyJSONRequest, results []VerifyJSONResult, keyIDs [][]KeyID) map[PublicKeyRequest]Timestamp { + keyRequests := map[PublicKeyRequest]Timestamp{} + for i := range requests { + if results[i].Result == nil { + // We've already verified this message, we don't need to refetch the keys for it. + continue + } + for _, keyID := range keyIDs[i] { + k := PublicKeyRequest{requests[i].ServerName, keyID} + // Grab the maximum neeeded TS for this server and key ID. + // This will default to 0 if the server and keyID weren't in the map. + maxTS := keyRequests[k] + if maxTS <= requests[i].AtTS { + // We clobber on equality since that means that if the server and keyID + // weren't already in the map and since AtTS is unsigned and since the + // default value for maxTS is 0 we will always insert an entry for the + // server and keyID. + keyRequests[k] = requests[i].AtTS + } + } + } + return keyRequests +} + +func (k *KeyRing) checkUsingKeys( + requests []VerifyJSONRequest, results []VerifyJSONResult, keyIDs [][]KeyID, + keys map[PublicKeyRequest]ServerKeys, +) { + for i := range requests { + if results[i].Result == nil { + // We've already checked this message and it passed the signature checks. + // So we can skip to the next message. + continue + } + for _, keyID := range keyIDs[i] { + serverKeys, ok := keys[PublicKeyRequest{requests[i].ServerName, keyID}] + if !ok { + // No key for this key ID so we continue onto the next key ID. + continue + } + publicKey := serverKeys.PublicKey(keyID, requests[i].AtTS) + if publicKey == nil { + // The key wasn't valid at the timestamp we needed it to be valid at. + // So skip onto the next key. + results[i].Result = fmt.Errorf( + "gomatrixserverlib: key with ID %q for %q not valid at %d", + keyID, requests[i].ServerName, requests[i].AtTS, + ) + continue + } + if err := VerifyJSON( + requests[i].ServerName, keyID, ed25519.PublicKey(publicKey), requests[i].Message, + ); err != nil { + // The signature wasn't valid, record the error and try the next key ID. + results[i].Result = err + continue + } + // The signature is valid, set the result to nil. + results[i].Result = nil + break + } + } +} + +// A PerspectiveKeyFetcher fetches server keys from a single perspective server. +type PerspectiveKeyFetcher struct { + // The name of the perspective server to fetch keys from. + PerspectiveServerName string + // The ed25519 public keys the perspective server must sign responses with. + PerspectiveServerKeys map[KeyID]ed25519.PublicKey + // The federation client to use to fetch keys with. + Client Client +} + +// FetchKeys implements KeyFetcher +func (p *PerspectiveKeyFetcher) FetchKeys(requests map[PublicKeyRequest]Timestamp) (map[PublicKeyRequest]ServerKeys, error) { + results, err := p.Client.LookupServerKeys(p.PerspectiveServerName, requests) + if err != nil { + return nil, err + } + + for req, keys := range results { + var valid bool + keyIDs, err := ListKeyIDs(p.PerspectiveServerName, keys.Raw) + if err != nil { + // The response from the perspective server was corrupted. + return nil, err + } + for _, keyID := range keyIDs { + perspectiveKey, ok := p.PerspectiveServerKeys[keyID] + if !ok { + // We don't have a key for that keyID, skip to the next keyID. + continue + } + if err := VerifyJSON(p.PerspectiveServerName, keyID, perspectiveKey, keys.Raw); err != nil { + // An invalid signature is very bad since it means we have a + // problem talking to the perspective server. + return nil, err + } + valid = true + break + } + if !valid { + // This means we don't have a known signature from the perspective server. + return nil, fmt.Errorf("gomatrixserverlib: not signed with a known key for the perspective server") + } + + // Check that the keys are valid for the server. + checks, _, _ := CheckKeys(req.ServerName, time.Unix(0, 0), keys, nil) + if !checks.AllChecksOK { + // This is bad because it means that the perspective server was trying to feed us an invalid response. + return nil, fmt.Errorf("gomatrixserverlib: key response from perspective server failed checks") + } + } + + return results, nil +} + +// A DirectKeyFetcher fetches keys directly from a server. +// This may be suitable for local deployments that are firewalled from the public internet where DNS can be trusted. +type DirectKeyFetcher struct { + // The federation client to use to fetch keys with. + Client Client +} + +// FetchKeys implements KeyFetcher +func (d *DirectKeyFetcher) FetchKeys(requests map[PublicKeyRequest]Timestamp) (map[PublicKeyRequest]ServerKeys, error) { + byServer := map[string]map[PublicKeyRequest]Timestamp{} + for req, ts := range requests { + server := byServer[req.ServerName] + if server == nil { + server = map[PublicKeyRequest]Timestamp{} + byServer[req.ServerName] = server + } + server[req] = ts + } + + results := map[PublicKeyRequest]ServerKeys{} + for server, reqs := range byServer { + // TODO: make these requests in parallel + serverResults, err := d.fetchKeysForServer(server, reqs) + if err != nil { + // TODO: Should we actually be erroring here? or should we just drop those keys from the result map? + return nil, err + } + for req, keys := range serverResults { + results[req] = keys + } + } + return results, nil +} + +func (d *DirectKeyFetcher) fetchKeysForServer( + serverName string, requests map[PublicKeyRequest]Timestamp, +) (map[PublicKeyRequest]ServerKeys, error) { + results, err := d.Client.LookupServerKeys(serverName, requests) + if err != nil { + return nil, err + } + + for req, keys := range results { + // Check that the keys are valid for the server. + checks, _, _ := CheckKeys(req.ServerName, time.Unix(0, 0), keys, nil) + if !checks.AllChecksOK { + return nil, fmt.Errorf("gomatrixserverlib: key response direct from %q failed checks", serverName) + } + } + + return results, nil +} diff --git a/vendor/src/github.com/matrix-org/gomatrixserverlib/keyring_test.go b/vendor/src/github.com/matrix-org/gomatrixserverlib/keyring_test.go new file mode 100644 index 00000000..dc164136 --- /dev/null +++ b/vendor/src/github.com/matrix-org/gomatrixserverlib/keyring_test.go @@ -0,0 +1,139 @@ +package gomatrixserverlib + +import ( + "encoding/json" + "testing" +) + +var privateKeySeed1 = `QJvXAPj0D9MUb1exkD8pIWmCvT1xajlsB8jRYz/G5HE` +var privateKeySeed2 = `AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA` + +// testKeys taken from a copy of synapse. +var testKeys = `{ + "old_verify_keys": { + "ed25519:old": { + "expired_ts": 929059200, + "key": "O2onvM62pC1io6jQKm8Nc2UyFXcd4kOmOsBIoYtZ2ik" + } + }, + "server_name": "localhost:8800", + "signatures": { + "localhost:8800": { + "ed25519:a_Obwu": "xkr4Z49ODoQnRi//ePfXlt8Q68vzd+DkzBNCt60NcwnLjNREx0qVQrw1iTFSoxkgGtz30NDkmyffDrCrmX5KBw" + } + }, + "tls_fingerprints": [ + { + "sha256": "I2ohBnqpb5m3HldWFwyA10WdjqDksukiKVUdZ690WzM" + } + ], + "valid_until_ts": 1493142432964, + "verify_keys": { + "ed25519:a_Obwu": { + "key": "2UwTWD4+tgTgENV7znGGNqhAOGY+BW1mRAnC6W6FBQg" + } + } +}` + +type testKeyDatabase struct{} + +func (db *testKeyDatabase) FetchKeys(requests map[PublicKeyRequest]Timestamp) (map[PublicKeyRequest]ServerKeys, error) { + results := map[PublicKeyRequest]ServerKeys{} + var keys ServerKeys + if err := json.Unmarshal([]byte(testKeys), &keys); err != nil { + return nil, err + } + + req1 := PublicKeyRequest{"localhost:8800", "ed25519:old"} + req2 := PublicKeyRequest{"localhost:8800", "ed25519:a_Obwu"} + + for req := range requests { + if req == req1 || req == req2 { + results[req] = keys + } + } + return results, nil +} + +func (db *testKeyDatabase) StoreKeys(requests map[PublicKeyRequest]ServerKeys) error { + return nil +} + +func TestVerifyJSONsSuccess(t *testing.T) { + // Check that trying to verify the server key JSON works. + k := KeyRing{nil, &testKeyDatabase{}} + results, err := k.VerifyJSONs([]VerifyJSONRequest{{ + ServerName: "localhost:8800", + Message: []byte(testKeys), + AtTS: 1493142432964, + }}) + if err != nil { + t.Fatal(err) + } + if len(results) != 1 || results[0].Result != nil { + t.Fatalf("VerifyJSON(): Wanted [{Result: nil}] got %#v", results) + } +} + +func TestVerifyJSONsUnknownServerFails(t *testing.T) { + // Check that trying to verify JSON for an unknown server fails. + k := KeyRing{nil, &testKeyDatabase{}} + results, err := k.VerifyJSONs([]VerifyJSONRequest{{ + ServerName: "unknown:8800", + Message: []byte(testKeys), + AtTS: 1493142432964, + }}) + if err != nil { + t.Fatal(err) + } + if len(results) != 1 || results[0].Result == nil { + t.Fatalf("VerifyJSON(): Wanted [{Result: }] got %#v", results) + } +} + +func TestVerifyJSONsDistantFutureFails(t *testing.T) { + // Check that trying to verify JSON from the distant future fails. + distantFuture := Timestamp(2000000000000) + k := KeyRing{nil, &testKeyDatabase{}} + results, err := k.VerifyJSONs([]VerifyJSONRequest{{ + ServerName: "unknown:8800", + Message: []byte(testKeys), + AtTS: distantFuture, + }}) + if err != nil { + t.Fatal(err) + } + if len(results) != 1 || results[0].Result == nil { + t.Fatalf("VerifyJSON(): Wanted [{Result: }] got %#v", results) + } +} + +func TestVerifyJSONsFetcherError(t *testing.T) { + // Check that if the database errors then the attempt to verify JSON fails. + k := KeyRing{nil, &erroringKeyDatabase{}} + results, err := k.VerifyJSONs([]VerifyJSONRequest{{ + ServerName: "localhost:8800", + Message: []byte(testKeys), + AtTS: 1493142432964, + }}) + if err != error(&testErrorFetch) || results != nil { + t.Fatalf("VerifyJSONs(): Wanted (nil, ) got (%#v, %q)", results, err) + } +} + +type erroringKeyDatabase struct{} + +type erroringKeyDatabaseError int + +func (e *erroringKeyDatabaseError) Error() string { return "An error with the key database" } + +var testErrorFetch = erroringKeyDatabaseError(1) +var testErrorStore = erroringKeyDatabaseError(2) + +func (e *erroringKeyDatabase) FetchKeys(requests map[PublicKeyRequest]Timestamp) (map[PublicKeyRequest]ServerKeys, error) { + return nil, &testErrorFetch +} + +func (e *erroringKeyDatabase) StoreKeys(keys map[PublicKeyRequest]ServerKeys) error { + return &testErrorStore +} diff --git a/vendor/src/github.com/matrix-org/gomatrixserverlib/keys.go b/vendor/src/github.com/matrix-org/gomatrixserverlib/keys.go index 173638ef..52889864 100644 --- a/vendor/src/github.com/matrix-org/gomatrixserverlib/keys.go +++ b/vendor/src/github.com/matrix-org/gomatrixserverlib/keys.go @@ -21,7 +21,6 @@ import ( "crypto/sha256" "crypto/tls" "encoding/json" - "io/ioutil" "net" "net/http" "strings" @@ -31,19 +30,70 @@ import ( // ServerKeys are the ed25519 signing keys published by a matrix server. // Contains SHA256 fingerprints of the TLS X509 certificates used by the server. type ServerKeys struct { - Raw []byte `json:"-"` // Copy of the raw JSON for signature checking. - ServerName string `json:"server_name"` // The name of the server. - TLSFingerprints []struct { // List of SHA256 fingerprints of X509 certificates. - SHA256 Base64String `json:"sha256"` - } `json:"tls_fingerprints"` - VerifyKeys map[string]struct { // The current signing keys in use on this server. - Key Base64String `json:"key"` // The public key. - } `json:"verify_keys"` - ValidUntilTS int64 `json:"valid_until_ts"` // When this result is valid until in milliseconds. - OldVerifyKeys map[string]struct { // Old keys that are now only valid for checking historic events. - Key Base64String `json:"key"` // The public key. - ExpiredTS uint64 `json:"expired_ts"` // When this key stopped being valid for event signing. - } `json:"old_verify_keys"` + // Copy of the raw JSON for signature checking. + Raw []byte + // The server the raw JSON was downloaded from. + FromServer string + // The decoded JSON fields. + ServerKeyFields +} + +// A TLSFingerprint is a SHA256 hash of an X509 certificate. +type TLSFingerprint struct { + SHA256 Base64String `json:"sha256"` +} + +// A VerifyKey is a ed25519 public key for a server. +type VerifyKey struct { + // The public key. + Key Base64String `json:"key"` +} + +// An OldVerifyKey is an old ed25519 public key that is no longer valid. +type OldVerifyKey struct { + VerifyKey + // When this key stopped being valid for event signing in milliseconds. + ExpiredTS Timestamp `json:"expired_ts"` +} + +// ServerKeyFields are the parsed JSON contents of the ed25519 signing keys published by a matrix server. +type ServerKeyFields struct { + // The name of the server + ServerName string `json:"server_name"` + // List of SHA256 fingerprints of X509 certificates used by this server. + TLSFingerprints []TLSFingerprint `json:"tls_fingerprints"` + // The current signing keys in use on this server. + // The keys of the map are the IDs of the keys. + // These are valid while this response is valid. + VerifyKeys map[KeyID]VerifyKey `json:"verify_keys"` + // When this result is valid until in milliseconds. + ValidUntilTS Timestamp `json:"valid_until_ts"` + // Old keys that are now only valid for checking historic events. + // The keys of the map are the IDs of the keys. + OldVerifyKeys map[KeyID]OldVerifyKey `json:"old_verify_keys"` +} + +// UnmarshalJSON implements json.Unmarshaler +func (keys *ServerKeys) UnmarshalJSON(data []byte) error { + keys.Raw = data + return json.Unmarshal(data, &keys.ServerKeyFields) +} + +// MarshalJSON implements json.Marshaler +func (keys ServerKeys) MarshalJSON() ([]byte, error) { + // We already have a copy of the serialised JSON for the keys so we can return that directly. + return keys.Raw, nil +} + +// PublicKey returns a public key with the given ID valid at the given TS or nil if no such key exists. +func (keys ServerKeys) PublicKey(keyID KeyID, atTS Timestamp) []byte { + if currentKey, ok := keys.VerifyKeys[keyID]; ok && (atTS <= keys.ValidUntilTS) { + return currentKey.Key + } + if oldKey, ok := keys.OldVerifyKeys[keyID]; ok && (atTS <= oldKey.ExpiredTS) { + return oldKey.Key + } + return nil } // FetchKeysDirect fetches the matrix keys directly from the given address. @@ -85,10 +135,8 @@ func FetchKeysDirect(serverName, addr, sni string) (*ServerKeys, *tls.Connection return nil, nil, err } var keys ServerKeys - if keys.Raw, err = ioutil.ReadAll(response.Body); err != nil { - return nil, nil, err - } - if err = json.Unmarshal(keys.Raw, &keys); err != nil { + keys.FromServer = serverName + if err = json.NewDecoder(response.Body).Decode(&keys); err != nil { return nil, nil, err } return &keys, &connectionState, nil @@ -107,25 +155,25 @@ type TLSFingerprintChecks struct { // KeyChecks are the checks that should be applied to ServerKey responses. type KeyChecks struct { - AllChecksOK bool // Did all the checks pass? - MatchingServerName bool // Does the server name match what was requested. - FutureValidUntilTS bool // The valid until TS is in the future. - HasEd25519Key bool // The server has at least one ed25519 key. - AllEd25519ChecksOK *bool // All the Ed25519 checks are ok. or null if there weren't any to check. - Ed25519Checks map[string]Ed25519Checks // Checks for Ed25519 keys. - HasTLSFingerprint bool // The server has at least one fingerprint. - AllTLSFingerprintChecksOK *bool // All the fingerpint checks are ok. - TLSFingerprintChecks []TLSFingerprintChecks // Checks for TLS fingerprints. - MatchingTLSFingerprint *bool // The TLS fingerprint for the connection matches one of the listed fingerprints. + AllChecksOK bool // Did all the checks pass? + MatchingServerName bool // Does the server name match what was requested. + FutureValidUntilTS bool // The valid until TS is in the future. + HasEd25519Key bool // The server has at least one ed25519 key. + AllEd25519ChecksOK *bool // All the Ed25519 checks are ok. or null if there weren't any to check. + Ed25519Checks map[KeyID]Ed25519Checks // Checks for Ed25519 keys. + HasTLSFingerprint bool // The server has at least one fingerprint. + AllTLSFingerprintChecksOK *bool // All the fingerpint checks are ok. + TLSFingerprintChecks []TLSFingerprintChecks // Checks for TLS fingerprints. + MatchingTLSFingerprint *bool // The TLS fingerprint for the connection matches one of the listed fingerprints. } // CheckKeys checks the keys returned from a server to make sure they are valid. // If the checks pass then also return a map of key_id to Ed25519 public key and a list of SHA256 TLS fingerprints. func CheckKeys(serverName string, now time.Time, keys ServerKeys, connState *tls.ConnectionState) ( - checks KeyChecks, ed25519Keys map[string]Base64String, sha256Fingerprints []Base64String, + checks KeyChecks, ed25519Keys map[KeyID]Base64String, sha256Fingerprints []Base64String, ) { checks.MatchingServerName = serverName == keys.ServerName - checks.FutureValidUntilTS = now.UnixNano() < keys.ValidUntilTS*1000000 + checks.FutureValidUntilTS = keys.ValidUntilTS.Time().After(now) checks.AllChecksOK = checks.MatchingServerName && checks.FutureValidUntilTS ed25519Keys = checkVerifyKeys(keys, &checks) @@ -160,12 +208,12 @@ func checkFingerprint(connState *tls.ConnectionState, sha256Fingerprints []Base6 return false } -func checkVerifyKeys(keys ServerKeys, checks *KeyChecks) map[string]Base64String { +func checkVerifyKeys(keys ServerKeys, checks *KeyChecks) map[KeyID]Base64String { allEd25519ChecksOK := true - checks.Ed25519Checks = map[string]Ed25519Checks{} - verifyKeys := map[string]Base64String{} + checks.Ed25519Checks = map[KeyID]Ed25519Checks{} + verifyKeys := map[KeyID]Base64String{} for keyID, keyData := range keys.VerifyKeys { - algorithm := strings.SplitN(keyID, ":", 2)[0] + algorithm := strings.SplitN(string(keyID), ":", 2)[0] publicKey := keyData.Key if algorithm == "ed25519" { checks.HasEd25519Key = true diff --git a/vendor/src/github.com/matrix-org/gomatrixserverlib/request.go b/vendor/src/github.com/matrix-org/gomatrixserverlib/request.go new file mode 100644 index 00000000..08219a71 --- /dev/null +++ b/vendor/src/github.com/matrix-org/gomatrixserverlib/request.go @@ -0,0 +1,307 @@ +package gomatrixserverlib + +import ( + "bytes" + "encoding/json" + "fmt" + "github.com/matrix-org/util" + "golang.org/x/crypto/ed25519" + "io" + "io/ioutil" + "net/http" + "strings" + "time" +) + +// A FederationRequest is a request to send to a remote server or a request +// received from a remote server. +// Federation requests are signed by building a JSON object and signing it +type FederationRequest struct { + // fields implement the JSON format needed for signing + // specified in https://matrix.org/docs/spec/server_server/unstable.html#request-authentication + fields struct { + Content rawJSON `json:"content,omitempty"` + Destination string `json:"destination"` + Method string `json:"method"` + Origin string `json:"origin"` + RequestURI string `json:"uri"` + Signatures map[string]map[string]string `json:"signatures,omitempty"` + } +} + +// NewFederationRequest creates a matrix request. Takes an HTTP method, a +// destination homeserver and a request path which can have a query string. +// The destination is the name of a matrix homeserver. +// The request path must begin with a slash. +// Eg. NewFederationRequest("GET", "matrix.org", "/_matrix/federation/v1/send/123") +func NewFederationRequest(method, destination, requestURI string) FederationRequest { + var r FederationRequest + r.fields.Destination = destination + r.fields.Method = strings.ToUpper(method) + r.fields.RequestURI = requestURI + return r +} + +// SetContent sets the JSON content for the request. +// Returns an error if there already is JSON content present on the request. +func (r *FederationRequest) SetContent(content interface{}) error { + if r.fields.Content != nil { + return fmt.Errorf("gomatrixserverlib: content already set on the request") + } + if r.fields.Signatures != nil { + return fmt.Errorf("gomatrixserverlib: the request is signed and cannot be modified") + } + data, err := json.Marshal(content) + if err != nil { + return err + } + r.fields.Content = rawJSON(data) + return nil +} + +// Method returns the JSON method for the request. +func (r *FederationRequest) Method() string { + return r.fields.Method +} + +// Content returns the JSON content for the request. +func (r *FederationRequest) Content() []byte { + return []byte(r.fields.Content) +} + +// Origin returns the server that the request originated on. +func (r *FederationRequest) Origin() string { + return r.fields.Origin +} + +// RequestURI returns the path and query sections of the HTTP request URL. +func (r *FederationRequest) RequestURI() string { + return r.fields.RequestURI +} + +// Sign the matrix request with an ed25519 key. +// Uses the algorithm specified https://matrix.org/docs/spec/server_server/unstable.html#request-authentication +// Updates the request with the signature in place. +// Returns an error if there was a problem signing the request. +func (r *FederationRequest) Sign(serverName string, keyID KeyID, privateKey ed25519.PrivateKey) error { + if r.fields.Origin != "" && r.fields.Origin != serverName { + return fmt.Errorf("gomatrixserverlib: the request is already signed by a different server") + } + r.fields.Origin = serverName + // The request fields are already in the form required by the specification + // So we can just serialise the request fields using the default marshaller + data, err := json.Marshal(r.fields) + if err != nil { + return err + } + signedData, err := SignJSON(serverName, keyID, privateKey, data) + if err != nil { + return err + } + // Now we can deserialise the signed request back into the request structure + // to set the Signatures field, (This will clobber the other fields but they + // will all round-trip through an encode/decode.) + return json.Unmarshal(signedData, &r.fields) +} + +// HTTPRequest constructs an net/http.Request for this matrix request. +// The request can be passed to net/http.Client.Do(). +func (r *FederationRequest) HTTPRequest() (*http.Request, error) { + urlStr := fmt.Sprintf("matrix://%s%s", r.fields.Destination, r.fields.RequestURI) + + var content io.Reader + if r.fields.Content != nil { + content = bytes.NewReader([]byte(r.fields.Content)) + } + + httpReq, err := http.NewRequest(r.fields.Method, urlStr, content) + if err != nil { + return nil, err + } + + // Sanity check that the request fields will round-trip properly. + if httpReq.URL.RequestURI() != r.fields.RequestURI { + return nil, fmt.Errorf( + "gomatrixserverlib: Request URI didn't encode properly. Wanted %q. Got %q", + r.fields.RequestURI, httpReq.URL.RequestURI(), + ) + } + + if r.fields.Content != nil { + httpReq.Header.Set("Content-Type", "application/json") + } + + for keyID, sig := range r.fields.Signatures[r.fields.Origin] { + // Check that we can safely include the origin and key ID in the header. + // We don't need to check the signature since we already know that it is + // base64. + if !isSafeInHTTPQuotedString(r.fields.Origin) { + return nil, fmt.Errorf("gomatrixserverlib: Request Origin isn't safe to include in an HTTP header") + } + if !isSafeInHTTPQuotedString(keyID) { + return nil, fmt.Errorf("gomatrixserverlib: Request key ID isn't safe to include in an HTTP header") + } + httpReq.Header.Add("Authorization", fmt.Sprintf( + "X-Matrix origin=\"%s\",key=\"%s\",sig=\"%s\"", r.fields.Origin, keyID, sig, + )) + } + + return httpReq, nil +} + +// isSafeInHTTPQuotedString checks whether the string is safe to include +// in an HTTP quoted-string without escaping. +// According to https://tools.ietf.org/html/rfc7230#section-3.2.6 the safe +// charcters are: +// +// qdtext = HTAB / SP / %x21 / %x23-5B / %x5D-7E / %x80-FF +// +func isSafeInHTTPQuotedString(text string) bool { + for i := 0; i < len(text); i++ { + c := text[i] + switch { + case c == '\t': + continue + case c == ' ': + continue + case c == 0x21: + continue + case 0x23 <= c && c <= 0x5B: + continue + case 0x5D <= c && c <= 0x7E: + continue + case 0x80 <= c && c <= 0xFF: + continue + default: + return false + } + } + return true +} + +// VerifyHTTPRequest extracts and verifies the contents of a net/http.Request. +// It consumes the body of the request. +// The JSON content can be accessed using FederationRequest.Content() +// Returns an 400 error if there was a problem parsing the request. +// It authenticates the request using an ed25519 signature using the KeyRing. +// The origin server can be accessed using FederationRequest.Origin() +// Returns a 401 error if there was a problem authenticating the request. +// HTTP handlers using this should be careful that they only use the parts of +// the request that have been authenticated: the method, the request path, +// the query parameters, and the JSON content. In particular the version of +// HTTP and the headers aren't protected by the signature. +func VerifyHTTPRequest( + req *http.Request, now time.Time, destination string, keys KeyRing, +) (*FederationRequest, util.JSONResponse) { + request, err := readHTTPRequest(req) + if err != nil { + util.GetLogger(req.Context()).WithError(err).Print("Error parsing HTTP headers") + return nil, util.MessageResponse(400, "Bad Request") + } + request.fields.Destination = destination + + // The request fields are already in the form required by the specification + // So we can just serialise the request fields using the default marshaller + toVerify, err := json.Marshal(request.fields) + if err != nil { + util.GetLogger(req.Context()).WithError(err).Print("Error parsing JSON") + return nil, util.MessageResponse(400, "Invalid JSON") + } + + if request.Origin() == "" { + message := "Missing \"Authorization: X-Matrix ...\" HTTP header" + util.GetLogger(req.Context()).WithError(err).Print(message) + return nil, util.MessageResponse(401, message) + } + + results, err := keys.VerifyJSONs([]VerifyJSONRequest{{ + ServerName: request.Origin(), + AtTS: AsTimestamp(now), + Message: toVerify, + }}) + if err != nil { + message := "Error authenticating request" + util.GetLogger(req.Context()).WithError(err).Print(message) + return nil, util.MessageResponse(500, message) + } + if results[0].Result != nil { + message := "Invalid request signature" + util.GetLogger(req.Context()).WithError(results[0].Result).Print(message) + return nil, util.MessageResponse(401, message) + } + + return request, util.JSONResponse{Code: 200, JSON: struct{}{}} +} + +// Returns an error if there was a problem reading the content of the request +func readHTTPRequest(req *http.Request) (*FederationRequest, error) { + var result FederationRequest + + result.fields.Method = req.Method + result.fields.RequestURI = req.URL.RequestURI() + + content, err := ioutil.ReadAll(req.Body) + if err != nil { + return nil, err + } + if len(content) != 0 { + if req.Header.Get("Content-Type") != "application/json" { + return nil, fmt.Errorf( + "gomatrixserverlib: The request must be \"application/json\" not %q", + req.Header.Get("Content-Type"), + ) + } + result.fields.Content = rawJSON(content) + } + + for _, authorization := range req.Header["Authorization"] { + scheme, origin, key, sig := parseAuthorization(authorization) + if scheme != "X-Matrix" { + // Ignore unknown types of Authorization. + continue + } + if origin == "" || key == "" || sig == "" { + return nil, fmt.Errorf("gomatrixserverlib: invalid X-Matrix authorization header") + } + if result.fields.Origin != "" && result.fields.Origin != origin { + return nil, fmt.Errorf("gomatrixserverlib: different origins in X-Matrix authorization headers") + } + result.fields.Origin = origin + if result.fields.Signatures == nil { + result.fields.Signatures = map[string]map[string]string{origin: map[string]string{key: sig}} + } else { + result.fields.Signatures[origin][key] = sig + } + } + + return &result, nil +} + +func parseAuthorization(header string) (scheme, origin, key, sig string) { + parts := strings.SplitN(header, " ", 2) + scheme = parts[0] + if scheme != "X-Matrix" { + return + } + if len(parts) != 2 { + return + } + for _, data := range strings.Split(parts[1], ",") { + pair := strings.SplitN(data, "=", 2) + if len(pair) != 2 { + continue + } + name := pair[0] + value := strings.Trim(pair[1], "\"") + if name == "origin" { + origin = value + } + if name == "key" { + key = value + } + if name == "sig" { + sig = value + } + } + return +} diff --git a/vendor/src/github.com/matrix-org/gomatrixserverlib/request_test.go b/vendor/src/github.com/matrix-org/gomatrixserverlib/request_test.go new file mode 100644 index 00000000..5c42a40c --- /dev/null +++ b/vendor/src/github.com/matrix-org/gomatrixserverlib/request_test.go @@ -0,0 +1,175 @@ +package gomatrixserverlib + +import ( + "bufio" + "bytes" + "encoding/base64" + "golang.org/x/crypto/ed25519" + "net/http" + "testing" + "time" +) + +// This GET request is taken from a request made by a synapse run by sytest. +// The headers have been reordered to match the order net/http writes them in. +const exampleGetRequest = "GET /_matrix/federation/v1/query/directory?room_alias=%23test%3Alocalhost%3A44033 HTTP/1.1\r\n" + + "Host: localhost:44033\r\n" + + "Authorization: X-Matrix" + + " origin=\"localhost:8800\"" + + ",key=\"ed25519:a_Obwu\"" + + ",sig=\"7vt4vP/w8zYB3Zg77nuTPwie3TxEy2OHZQMsSa4nsXZzL4/qw+DguXbyMy3BF77XvSJmBt+Gw+fU6T4HId7fBg\"" + + "\r\n" + + "\r\n" + +// This PUT request is taken from a request made by a synapse run by sytest. +// The headers have been reordered to match the order net/http writes them in. +const examplePutRequest = "PUT /_matrix/federation/v1/send/1493385816575/ HTTP/1.1\r\n" + + "Host: localhost:44033\r\n" + + "Content-Length: 321\r\n" + + "Authorization: X-Matrix" + + " origin=\"localhost:8800\"" + + ",key=\"ed25519:a_Obwu\"" + + ",sig=\"+hmW6UjEXx7vMt2+MXO/EImSfdEYdBsZEOmpiz3evYktAgGNpGuNMBYXIA969WGubmceREKA/r1phasUFHBpDg\"" + + "\r\n" + + "Content-Type: application/json\r\n" + + "\r\n" + + examplePutContent + +const examplePutContent = `{"edus":[{"content":{"device_id":"YHRUBZNPFS",` + + `"keys":{"device_id":"YHRUBZNPFS","device_keys":{},"user_id":` + + `"@ANON-22:localhost:8800"},"prev_id":[],"stream_id":30,"user_id":` + + `"@ANON-22:localhost:8800"},"edu_type":"m.device_list_update"}],"origin"` + + `:"localhost:8800","origin_server_ts":1493385822396,"pdu_failures":[],` + + `"pdus":[]}` + +func TestSignGetRequest(t *testing.T) { + request := NewFederationRequest( + "GET", "localhost:44033", + "/_matrix/federation/v1/query/directory?room_alias=%23test%3Alocalhost%3A44033", + ) + if err := request.Sign("localhost:8800", "ed25519:a_Obwu", privateKey1); err != nil { + t.Fatal(err) + } + + hr, err := request.HTTPRequest() + if err != nil { + t.Fatal(err) + } + hr.Header.Set("User-Agent", "") + + buf := bytes.NewBuffer(nil) + if err = hr.Write(buf); err != nil { + t.Fatal(err) + } + + got := string(buf.Bytes()) + want := exampleGetRequest + if want != got { + t.Errorf("Wanted %q got %q", want, got) + } +} + +func TestVerifyGetRequest(t *testing.T) { + hr, err := http.ReadRequest(bufio.NewReader(bytes.NewReader([]byte(exampleGetRequest)))) + if err != nil { + t.Fatal(err) + } + request, jsonResp := VerifyHTTPRequest( + hr, time.Unix(1493142432, 96400), "localhost:44033", KeyRing{nil, &testKeyDatabase{}}, + ) + if request == nil { + t.Errorf("Wanted non-nil request got nil. (request was %#v, response was %#v)", hr, jsonResp) + } + + if request.Method() != "GET" { + t.Errorf("Wanted request.Method() to be \"GET\" got %q", request.Method()) + } + + if request.Origin() != "localhost:8800" { + t.Errorf("Wanted request.Origin() to be \"localhost:8800\" got %q", request.Origin()) + } + + if request.Content() != nil { + t.Errorf("Wanted request.Content() to be nil got %q", string(request.Content())) + } + + wantPath := "/_matrix/federation/v1/query/directory?room_alias=%23test%3Alocalhost%3A44033" + if request.RequestURI() != wantPath { + t.Errorf("Wanted request.RequestURI() to be %q got %q", wantPath, request.RequestURI()) + } +} + +func TestSignPutRequest(t *testing.T) { + request := NewFederationRequest( + "PUT", "localhost:44033", "/_matrix/federation/v1/send/1493385816575/", + ) + if err := request.SetContent(rawJSON([]byte(examplePutContent))); err != nil { + t.Fatal(err) + } + if err := request.Sign("localhost:8800", "ed25519:a_Obwu", privateKey1); err != nil { + t.Fatal(err) + } + + hr, err := request.HTTPRequest() + if err != nil { + t.Fatal(err) + } + hr.Header.Set("User-Agent", "") + + buf := bytes.NewBuffer(nil) + if err = hr.Write(buf); err != nil { + t.Fatal(err) + } + + got := string(buf.Bytes()) + want := examplePutRequest + if want != got { + t.Errorf("Wanted %q got %q", want, got) + } +} + +func TestVerifyPutRequest(t *testing.T) { + hr, err := http.ReadRequest(bufio.NewReader(bytes.NewReader([]byte(examplePutRequest)))) + if err != nil { + t.Fatal(err) + } + request, jsonResp := VerifyHTTPRequest( + hr, time.Unix(1493142432, 96400), "localhost:44033", KeyRing{nil, &testKeyDatabase{}}, + ) + if request == nil { + t.Errorf("Wanted non-nil request got nil. (request was %#v, response was %#v)", hr, jsonResp) + } + + if request.Method() != "PUT" { + t.Errorf("Wanted request.Method() to be \"PUT\" got %q", request.Method()) + } + + if request.Origin() != "localhost:8800" { + t.Errorf("Wanted request.Origin() to be \"localhost:8800\" got %q", request.Origin()) + } + + if string(request.Content()) != examplePutContent { + t.Errorf("Wanted request.Content() to be %q got %q", examplePutContent, string(request.Content())) + } + + wantPath := "/_matrix/federation/v1/send/1493385816575/" + if request.RequestURI() != wantPath { + t.Errorf("Wanted request.RequestURI() to be %q got %q", wantPath, request.RequestURI()) + } +} + +var privateKey1 = mustLoadPrivateKey(privateKeySeed1) +var privateKey2 = mustLoadPrivateKey(privateKeySeed2) + +func mustLoadPrivateKey(seed string) ed25519.PrivateKey { + seedBytes, err := base64.RawStdEncoding.DecodeString(seed) + if err != nil { + panic(err) + } + random := bytes.NewBuffer(seedBytes) + _, privateKey, err := ed25519.GenerateKey(random) + if err != nil { + panic(err) + } + return privateKey +} diff --git a/vendor/src/github.com/matrix-org/gomatrixserverlib/signing.go b/vendor/src/github.com/matrix-org/gomatrixserverlib/signing.go index 2ae3ad0a..3607211f 100644 --- a/vendor/src/github.com/matrix-org/gomatrixserverlib/signing.go +++ b/vendor/src/github.com/matrix-org/gomatrixserverlib/signing.go @@ -21,11 +21,17 @@ import ( "golang.org/x/crypto/ed25519" ) +// A KeyID is the ID of a ed25519 key used to sign JSON. +// The key IDs have a format of "ed25519:[0-9A-Za-z]+" +// If we switch to using a different signing algorithm then we will change the +// prefix used. +type KeyID string + // SignJSON signs a JSON object returning a copy signed with the given key. // https://matrix.org/docs/spec/server_server/unstable.html#signing-json -func SignJSON(signingName, keyID string, privateKey ed25519.PrivateKey, message []byte) ([]byte, error) { +func SignJSON(signingName string, keyID KeyID, privateKey ed25519.PrivateKey, message []byte) ([]byte, error) { var object map[string]*json.RawMessage - var signatures map[string]map[string]Base64String + var signatures map[string]map[KeyID]Base64String if err := json.Unmarshal(message, &object); err != nil { return nil, err } @@ -39,7 +45,7 @@ func SignJSON(signingName, keyID string, privateKey ed25519.PrivateKey, message } delete(object, "signatures") } else { - signatures = map[string]map[string]Base64String{} + signatures = map[string]map[KeyID]Base64String{} } unsorted, err := json.Marshal(object) @@ -58,7 +64,7 @@ func SignJSON(signingName, keyID string, privateKey ed25519.PrivateKey, message if signaturesForEntity != nil { signaturesForEntity[keyID] = signature } else { - signatures[signingName] = map[string]Base64String{keyID: signature} + signatures[signingName] = map[KeyID]Base64String{keyID: signature} } var rawSignatures json.RawMessage @@ -75,10 +81,25 @@ func SignJSON(signingName, keyID string, privateKey ed25519.PrivateKey, message return json.Marshal(object) } +// ListKeyIDs lists the key IDs a given entity has signed a message with. +func ListKeyIDs(signingName string, message []byte) ([]KeyID, error) { + var object struct { + Signatures map[string]map[KeyID]json.RawMessage `json:"signatures"` + } + if err := json.Unmarshal(message, &object); err != nil { + return nil, err + } + var result []KeyID + for keyID := range object.Signatures[signingName] { + result = append(result, keyID) + } + return result, nil +} + // VerifyJSON checks that the entity has signed the message using a particular key. -func VerifyJSON(signingName, keyID string, publicKey ed25519.PublicKey, message []byte) error { +func VerifyJSON(signingName string, keyID KeyID, publicKey ed25519.PublicKey, message []byte) error { var object map[string]*json.RawMessage - var signatures map[string]map[string]Base64String + var signatures map[string]map[KeyID]Base64String if err := json.Unmarshal(message, &object); err != nil { return err } diff --git a/vendor/src/github.com/matrix-org/gomatrixserverlib/signing_test.go b/vendor/src/github.com/matrix-org/gomatrixserverlib/signing_test.go index 3f45df11..ca1f955a 100644 --- a/vendor/src/github.com/matrix-org/gomatrixserverlib/signing_test.go +++ b/vendor/src/github.com/matrix-org/gomatrixserverlib/signing_test.go @@ -32,7 +32,7 @@ func TestVerifyJSON(t *testing.T) { } random := bytes.NewBuffer(seed) entityName := "domain" - keyID := "ed25519:1" + keyID := KeyID("ed25519:1") publicKey, _, err := ed25519.GenerateKey(random) if err != nil { @@ -99,7 +99,7 @@ func TestVerifyJSON(t *testing.T) { func TestSignJSON(t *testing.T) { random := bytes.NewBuffer([]byte("Some 32 randomly generated bytes")) entityName := "example.com" - keyID := "ed25519:my_key_id" + keyID := KeyID("ed25519:my_key_id") input := []byte(`{"this":"is","my":"message"}`) publicKey, privateKey, err := ed25519.GenerateKey(random) @@ -139,7 +139,7 @@ func TestSignJSONTestVectors(t *testing.T) { } random := bytes.NewBuffer(seed) entityName := "domain" - keyID := "ed25519:1" + keyID := KeyID("ed25519:1") _, privateKey, err := ed25519.GenerateKey(random) if err != nil { @@ -185,7 +185,7 @@ type MyMessage struct { func TestSignJSONWithUnsigned(t *testing.T) { random := bytes.NewBuffer([]byte("Some 32 randomly generated bytes")) entityName := "example.com" - keyID := "ed25519:my_key_id" + keyID := KeyID("ed25519:my_key_id") content := json.RawMessage(`{"signed":"data"}`) unsigned := json.RawMessage(`{"unsigned":"data"}`) message := MyMessage{&unsigned, &content, nil} diff --git a/vendor/src/github.com/matrix-org/gomatrixserverlib/stateresolution.go b/vendor/src/github.com/matrix-org/gomatrixserverlib/stateresolution.go index 3091acd7..81cdafd7 100644 --- a/vendor/src/github.com/matrix-org/gomatrixserverlib/stateresolution.go +++ b/vendor/src/github.com/matrix-org/gomatrixserverlib/stateresolution.go @@ -1,3 +1,18 @@ +/* Copyright 2017 Vector Creations 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. + */ + package gomatrixserverlib import ( diff --git a/vendor/src/github.com/matrix-org/gomatrixserverlib/timestamp.go b/vendor/src/github.com/matrix-org/gomatrixserverlib/timestamp.go new file mode 100644 index 00000000..c8359b07 --- /dev/null +++ b/vendor/src/github.com/matrix-org/gomatrixserverlib/timestamp.go @@ -0,0 +1,18 @@ +package gomatrixserverlib + +import ( + "time" +) + +// A Timestamp is a millisecond posix timestamp. +type Timestamp uint64 + +// AsTimestamp turns a time.Time into a millisecond posix timestamp. +func AsTimestamp(t time.Time) Timestamp { + return Timestamp(t.UnixNano() / 1000000) +} + +// Time turns a millisecond posix timestamp into a UTC time.Time +func (t Timestamp) Time() time.Time { + return time.Unix(int64(t)/1000, (int64(t)%1000)*1000000).UTC() +}