Update gomatrixserverlib (#89)

This commit is contained in:
Mark Haines 2017-05-05 16:19:48 +01:00 committed by GitHub
parent a1ce351d36
commit 0309035aad
17 changed files with 1355 additions and 71 deletions

View File

@ -14,7 +14,10 @@
package config 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. // ClientAPI contains the config information necessary to spin up a clientapi process.
type ClientAPI struct { type ClientAPI struct {
@ -24,7 +27,7 @@ type ClientAPI struct {
PrivateKey ed25519.PrivateKey PrivateKey ed25519.PrivateKey
// An arbitrary string used to uniquely identify the PrivateKey. Must start with the // An arbitrary string used to uniquely identify the PrivateKey. Must start with the
// prefix "ed25519:". // 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. // A list of URIs to send events to. These kafka logs should be consumed by a Room Server.
KafkaProducerURIs []string KafkaProducerURIs []string
// The topic for events which are written to the logs. // The topic for events which are written to the logs.

View File

@ -111,7 +111,7 @@ func buildAndOutput() gomatrixserverlib.EventReference {
eventID++ eventID++
id := fmt.Sprintf("$%d:%s", eventID, *serverName) id := fmt.Sprintf("$%d:%s", eventID, *serverName)
now = time.Unix(0, 0) 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 { if err != nil {
panic(err) panic(err)
} }

4
vendor/manifest vendored
View File

@ -92,7 +92,7 @@
{ {
"importpath": "github.com/matrix-org/gomatrixserverlib", "importpath": "github.com/matrix-org/gomatrixserverlib",
"repository": "https://github.com/matrix-org/gomatrixserverlib", "repository": "https://github.com/matrix-org/gomatrixserverlib",
"revision": "8e847d5bdb5cc0dd11e0846188eb403b70ae6bb3", "revision": "fd3b9faf8492989e8f782592d37dcbdc01c44fea",
"branch": "master" "branch": "master"
}, },
{ {
@ -212,4 +212,4 @@
"branch": "v2" "branch": "v2"
} }
] ]
} }

View File

@ -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

View File

@ -16,6 +16,7 @@
package gomatrixserverlib package gomatrixserverlib
import ( import (
"bytes"
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"fmt" "fmt"
@ -141,3 +142,81 @@ func (fc *Client) LookupUserInfo(matrixServer, token string) (u UserInfo, err er
return 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": { "<server_name>": { "<key_id>": { "minimum_valid_until_ts": <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
}

View File

@ -109,14 +109,14 @@ var emptyEventReferenceList = []EventReference{}
// Call this after filling out the necessary fields. // Call this after filling out the necessary fields.
// This can be called mutliple times on the same builder. // This can be called mutliple times on the same builder.
// A different event ID must be supplied each time this is called. // 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 { var event struct {
EventBuilder EventBuilder
EventID string `json:"event_id"` EventID string `json:"event_id"`
RawContent rawJSON `json:"content"` RawContent rawJSON `json:"content"`
RawUnsigned rawJSON `json:"unsigned,omitempty"` RawUnsigned rawJSON `json:"unsigned,omitempty"`
OriginServerTS int64 `json:"origin_server_ts"` OriginServerTS Timestamp `json:"origin_server_ts"`
Origin string `json:"origin"` Origin string `json:"origin"`
} }
event.EventBuilder = *eb event.EventBuilder = *eb
if event.PrevEvents == nil { 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.RawContent = rawJSON(event.content)
event.RawUnsigned = rawJSON(event.unsigned) event.RawUnsigned = rawJSON(event.unsigned)
event.OriginServerTS = now.UnixNano() / 1000000 event.OriginServerTS = AsTimestamp(now)
event.Origin = origin event.Origin = origin
event.EventID = eventID event.EventID = eventID
@ -245,7 +245,7 @@ func (e Event) EventReference() EventReference {
} }
// Sign returns a copy of the event with an additional signature. // 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) eventJSON, err := signEvent(signingName, keyID, privateKey, e.eventJSON)
if err != nil { if err != nil {
// This is unreachable for events created with EventBuilder.Build or NewEventFromUntrustedJSON // 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. // KeyIDs returns a list of key IDs that the named entity has signed the event with.
func (e Event) KeyIDs(signingName string) []string { func (e Event) KeyIDs(signingName string) []KeyID {
var event struct { keyIDs, err := ListKeyIDs(signingName, e.eventJSON)
Signatures map[string]map[string]rawJSON `json:"signatures"` if err != nil {
}
if err := json.Unmarshal(e.eventJSON, &event); err != nil {
// This should unreachable for events created with EventBuilder.Build or NewEventFromUntrustedJSON // This should unreachable for events created with EventBuilder.Build or NewEventFromUntrustedJSON
panic(fmt.Errorf("gomatrixserverlib: invalid event %v", err)) panic(fmt.Errorf("gomatrixserverlib: invalid event %v", err))
} }
var keyIDs []string
for keyID := range event.Signatures[signingName] {
keyIDs = append(keyIDs, keyID)
}
return keyIDs return keyIDs
} }
// Verify checks a ed25519 signature // 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) return verifyEventSignature(signingName, keyID, publicKey, e.eventJSON)
} }

View File

@ -142,7 +142,7 @@ func referenceOfEvent(eventJSON []byte) (EventReference, error) {
} }
// SignEvent adds a ED25519 signature to the event for the given key. // 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. // Redact the event before signing so signature that will remain valid even if the event is redacted.
redactedJSON, err := redactEvent(eventJSON) 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. // 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) redactedJSON, err := redactEvent(eventJSON)
if err != nil { if err != nil {
return err return err

View File

@ -30,7 +30,7 @@ func TestVerifyEventSignatureTestVectors(t *testing.T) {
} }
random := bytes.NewBuffer(seed) random := bytes.NewBuffer(seed)
entityName := "domain" entityName := "domain"
keyID := "ed25519:1" keyID := KeyID("ed25519:1")
publicKey, _, err := ed25519.GenerateKey(random) publicKey, _, err := ed25519.GenerateKey(random)
if err != nil { if err != nil {
@ -173,7 +173,7 @@ func TestSignEventTestVectors(t *testing.T) {
} }
random := bytes.NewBuffer(seed) random := bytes.NewBuffer(seed)
entityName := "domain" entityName := "domain"
keyID := "ed25519:1" keyID := KeyID("ed25519:1")
_, privateKey, err := ed25519.GenerateKey(random) _, privateKey, err := ed25519.GenerateKey(random)
if err != nil { if err != nil {

View File

@ -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
}

View File

@ -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: <some error>}] 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: <some error>}] 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, <some error>) 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
}

View File

@ -21,7 +21,6 @@ import (
"crypto/sha256" "crypto/sha256"
"crypto/tls" "crypto/tls"
"encoding/json" "encoding/json"
"io/ioutil"
"net" "net"
"net/http" "net/http"
"strings" "strings"
@ -31,19 +30,70 @@ import (
// ServerKeys are the ed25519 signing keys published by a matrix server. // ServerKeys are the ed25519 signing keys published by a matrix server.
// Contains SHA256 fingerprints of the TLS X509 certificates used by the server. // Contains SHA256 fingerprints of the TLS X509 certificates used by the server.
type ServerKeys struct { type ServerKeys struct {
Raw []byte `json:"-"` // Copy of the raw JSON for signature checking. // Copy of the raw JSON for signature checking.
ServerName string `json:"server_name"` // The name of the server. Raw []byte
TLSFingerprints []struct { // List of SHA256 fingerprints of X509 certificates. // The server the raw JSON was downloaded from.
SHA256 Base64String `json:"sha256"` FromServer string
} `json:"tls_fingerprints"` // The decoded JSON fields.
VerifyKeys map[string]struct { // The current signing keys in use on this server. ServerKeyFields
Key Base64String `json:"key"` // The public key. }
} `json:"verify_keys"`
ValidUntilTS int64 `json:"valid_until_ts"` // When this result is valid until in milliseconds. // A TLSFingerprint is a SHA256 hash of an X509 certificate.
OldVerifyKeys map[string]struct { // Old keys that are now only valid for checking historic events. type TLSFingerprint struct {
Key Base64String `json:"key"` // The public key. SHA256 Base64String `json:"sha256"`
ExpiredTS uint64 `json:"expired_ts"` // When this key stopped being valid for event signing. }
} `json:"old_verify_keys"`
// 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. // 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 return nil, nil, err
} }
var keys ServerKeys var keys ServerKeys
if keys.Raw, err = ioutil.ReadAll(response.Body); err != nil { keys.FromServer = serverName
return nil, nil, err if err = json.NewDecoder(response.Body).Decode(&keys); err != nil {
}
if err = json.Unmarshal(keys.Raw, &keys); err != nil {
return nil, nil, err return nil, nil, err
} }
return &keys, &connectionState, nil return &keys, &connectionState, nil
@ -107,25 +155,25 @@ type TLSFingerprintChecks struct {
// KeyChecks are the checks that should be applied to ServerKey responses. // KeyChecks are the checks that should be applied to ServerKey responses.
type KeyChecks struct { type KeyChecks struct {
AllChecksOK bool // Did all the checks pass? AllChecksOK bool // Did all the checks pass?
MatchingServerName bool // Does the server name match what was requested. MatchingServerName bool // Does the server name match what was requested.
FutureValidUntilTS bool // The valid until TS is in the future. FutureValidUntilTS bool // The valid until TS is in the future.
HasEd25519Key bool // The server has at least one ed25519 key. 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. 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. Ed25519Checks map[KeyID]Ed25519Checks // Checks for Ed25519 keys.
HasTLSFingerprint bool // The server has at least one fingerprint. HasTLSFingerprint bool // The server has at least one fingerprint.
AllTLSFingerprintChecksOK *bool // All the fingerpint checks are ok. AllTLSFingerprintChecksOK *bool // All the fingerpint checks are ok.
TLSFingerprintChecks []TLSFingerprintChecks // Checks for TLS fingerprints. TLSFingerprintChecks []TLSFingerprintChecks // Checks for TLS fingerprints.
MatchingTLSFingerprint *bool // The TLS fingerprint for the connection matches one of the listed 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. // 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. // 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) ( 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.MatchingServerName = serverName == keys.ServerName
checks.FutureValidUntilTS = now.UnixNano() < keys.ValidUntilTS*1000000 checks.FutureValidUntilTS = keys.ValidUntilTS.Time().After(now)
checks.AllChecksOK = checks.MatchingServerName && checks.FutureValidUntilTS checks.AllChecksOK = checks.MatchingServerName && checks.FutureValidUntilTS
ed25519Keys = checkVerifyKeys(keys, &checks) ed25519Keys = checkVerifyKeys(keys, &checks)
@ -160,12 +208,12 @@ func checkFingerprint(connState *tls.ConnectionState, sha256Fingerprints []Base6
return false return false
} }
func checkVerifyKeys(keys ServerKeys, checks *KeyChecks) map[string]Base64String { func checkVerifyKeys(keys ServerKeys, checks *KeyChecks) map[KeyID]Base64String {
allEd25519ChecksOK := true allEd25519ChecksOK := true
checks.Ed25519Checks = map[string]Ed25519Checks{} checks.Ed25519Checks = map[KeyID]Ed25519Checks{}
verifyKeys := map[string]Base64String{} verifyKeys := map[KeyID]Base64String{}
for keyID, keyData := range keys.VerifyKeys { for keyID, keyData := range keys.VerifyKeys {
algorithm := strings.SplitN(keyID, ":", 2)[0] algorithm := strings.SplitN(string(keyID), ":", 2)[0]
publicKey := keyData.Key publicKey := keyData.Key
if algorithm == "ed25519" { if algorithm == "ed25519" {
checks.HasEd25519Key = true checks.HasEd25519Key = true

View File

@ -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
}

View File

@ -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
}

View File

@ -21,11 +21,17 @@ import (
"golang.org/x/crypto/ed25519" "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. // SignJSON signs a JSON object returning a copy signed with the given key.
// https://matrix.org/docs/spec/server_server/unstable.html#signing-json // 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 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 { if err := json.Unmarshal(message, &object); err != nil {
return nil, err return nil, err
} }
@ -39,7 +45,7 @@ func SignJSON(signingName, keyID string, privateKey ed25519.PrivateKey, message
} }
delete(object, "signatures") delete(object, "signatures")
} else { } else {
signatures = map[string]map[string]Base64String{} signatures = map[string]map[KeyID]Base64String{}
} }
unsorted, err := json.Marshal(object) unsorted, err := json.Marshal(object)
@ -58,7 +64,7 @@ func SignJSON(signingName, keyID string, privateKey ed25519.PrivateKey, message
if signaturesForEntity != nil { if signaturesForEntity != nil {
signaturesForEntity[keyID] = signature signaturesForEntity[keyID] = signature
} else { } else {
signatures[signingName] = map[string]Base64String{keyID: signature} signatures[signingName] = map[KeyID]Base64String{keyID: signature}
} }
var rawSignatures json.RawMessage var rawSignatures json.RawMessage
@ -75,10 +81,25 @@ func SignJSON(signingName, keyID string, privateKey ed25519.PrivateKey, message
return json.Marshal(object) 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. // 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 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 { if err := json.Unmarshal(message, &object); err != nil {
return err return err
} }

View File

@ -32,7 +32,7 @@ func TestVerifyJSON(t *testing.T) {
} }
random := bytes.NewBuffer(seed) random := bytes.NewBuffer(seed)
entityName := "domain" entityName := "domain"
keyID := "ed25519:1" keyID := KeyID("ed25519:1")
publicKey, _, err := ed25519.GenerateKey(random) publicKey, _, err := ed25519.GenerateKey(random)
if err != nil { if err != nil {
@ -99,7 +99,7 @@ func TestVerifyJSON(t *testing.T) {
func TestSignJSON(t *testing.T) { func TestSignJSON(t *testing.T) {
random := bytes.NewBuffer([]byte("Some 32 randomly generated bytes")) random := bytes.NewBuffer([]byte("Some 32 randomly generated bytes"))
entityName := "example.com" entityName := "example.com"
keyID := "ed25519:my_key_id" keyID := KeyID("ed25519:my_key_id")
input := []byte(`{"this":"is","my":"message"}`) input := []byte(`{"this":"is","my":"message"}`)
publicKey, privateKey, err := ed25519.GenerateKey(random) publicKey, privateKey, err := ed25519.GenerateKey(random)
@ -139,7 +139,7 @@ func TestSignJSONTestVectors(t *testing.T) {
} }
random := bytes.NewBuffer(seed) random := bytes.NewBuffer(seed)
entityName := "domain" entityName := "domain"
keyID := "ed25519:1" keyID := KeyID("ed25519:1")
_, privateKey, err := ed25519.GenerateKey(random) _, privateKey, err := ed25519.GenerateKey(random)
if err != nil { if err != nil {
@ -185,7 +185,7 @@ type MyMessage struct {
func TestSignJSONWithUnsigned(t *testing.T) { func TestSignJSONWithUnsigned(t *testing.T) {
random := bytes.NewBuffer([]byte("Some 32 randomly generated bytes")) random := bytes.NewBuffer([]byte("Some 32 randomly generated bytes"))
entityName := "example.com" entityName := "example.com"
keyID := "ed25519:my_key_id" keyID := KeyID("ed25519:my_key_id")
content := json.RawMessage(`{"signed":"data"}`) content := json.RawMessage(`{"signed":"data"}`)
unsigned := json.RawMessage(`{"unsigned":"data"}`) unsigned := json.RawMessage(`{"unsigned":"data"}`)
message := MyMessage{&unsigned, &content, nil} message := MyMessage{&unsigned, &content, nil}

View File

@ -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 package gomatrixserverlib
import ( import (

View File

@ -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()
}