bf7e85848b
* Rename serverkeyapi to signingkeyserver We use "api" for public facing stuff and "server" for internal stuff. As the server key API is internal only, we call it 'signing key server', which also clarifies the type of key (as opposed to TLS keys, E2E keys, etc) * Convert docker/scripts to use signing-key-server * Rename missed bits
319 lines
10 KiB
Go
319 lines
10 KiB
Go
package signingkeyserver
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"os"
|
|
"reflect"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/matrix-org/dendrite/federationapi/routing"
|
|
"github.com/matrix-org/dendrite/internal/caching"
|
|
"github.com/matrix-org/dendrite/internal/config"
|
|
"github.com/matrix-org/dendrite/signingkeyserver/api"
|
|
"github.com/matrix-org/gomatrixserverlib"
|
|
)
|
|
|
|
type server struct {
|
|
name gomatrixserverlib.ServerName // server name
|
|
validity time.Duration // key validity duration from now
|
|
config *config.SigningKeyServer // skeleton config, from TestMain
|
|
fedconfig *config.FederationAPI //
|
|
fedclient *gomatrixserverlib.FederationClient // uses MockRoundTripper
|
|
cache *caching.Caches // server-specific cache
|
|
api api.SigningKeyServerAPI // server-specific server key API
|
|
}
|
|
|
|
func (s *server) renew() {
|
|
// This updates the validity period to be an hour in the
|
|
// future, which is particularly useful in server A and
|
|
// server C's cases which have validity either as now or
|
|
// in the past.
|
|
s.validity = time.Hour
|
|
s.config.Matrix.KeyValidityPeriod = s.validity
|
|
}
|
|
|
|
var (
|
|
serverKeyID = gomatrixserverlib.KeyID("ed25519:auto")
|
|
serverA = &server{name: "a.com", validity: time.Duration(0)} // expires now
|
|
serverB = &server{name: "b.com", validity: time.Hour} // expires in an hour
|
|
serverC = &server{name: "c.com", validity: -time.Hour} // expired an hour ago
|
|
)
|
|
|
|
var servers = map[string]*server{
|
|
"a.com": serverA,
|
|
"b.com": serverB,
|
|
"c.com": serverC,
|
|
}
|
|
|
|
func TestMain(m *testing.M) {
|
|
// Set up the server key API for each "server" that we
|
|
// will use in our tests.
|
|
for _, s := range servers {
|
|
// Generate a new key.
|
|
_, testPriv, err := ed25519.GenerateKey(nil)
|
|
if err != nil {
|
|
panic("can't generate identity key: " + err.Error())
|
|
}
|
|
|
|
// Create a new cache but don't enable prometheus!
|
|
s.cache, err = caching.NewInMemoryLRUCache(false)
|
|
if err != nil {
|
|
panic("can't create cache: " + err.Error())
|
|
}
|
|
|
|
// Draw up just enough Dendrite config for the server key
|
|
// API to work.
|
|
cfg := &config.Dendrite{}
|
|
cfg.Defaults()
|
|
cfg.Global.ServerName = gomatrixserverlib.ServerName(s.name)
|
|
cfg.Global.PrivateKey = testPriv
|
|
cfg.Global.KeyID = serverKeyID
|
|
cfg.Global.KeyValidityPeriod = s.validity
|
|
cfg.SigningKeyServer.Database.ConnectionString = config.DataSource("file::memory:")
|
|
s.config = &cfg.SigningKeyServer
|
|
s.fedconfig = &cfg.FederationAPI
|
|
|
|
// Create a transport which redirects federation requests to
|
|
// the mock round tripper. Since we're not *really* listening for
|
|
// federation requests then this will return the key instead.
|
|
transport := &http.Transport{}
|
|
transport.RegisterProtocol("matrix", &MockRoundTripper{})
|
|
|
|
// Create the federation client.
|
|
s.fedclient = gomatrixserverlib.NewFederationClientWithTransport(
|
|
s.config.Matrix.ServerName, serverKeyID, testPriv, true, transport,
|
|
)
|
|
|
|
// Finally, build the server key APIs.
|
|
s.api = NewInternalAPI(s.config, s.fedclient, s.cache)
|
|
}
|
|
|
|
// Now that we have built our server key APIs, start the
|
|
// rest of the tests.
|
|
os.Exit(m.Run())
|
|
}
|
|
|
|
type MockRoundTripper struct{}
|
|
|
|
func (m *MockRoundTripper) RoundTrip(req *http.Request) (res *http.Response, err error) {
|
|
// Check if the request is looking for keys from a server that
|
|
// we know about in the test. The only reason this should go wrong
|
|
// is if the test is broken.
|
|
s, ok := servers[req.Host]
|
|
if !ok {
|
|
return nil, fmt.Errorf("server not known: %s", req.Host)
|
|
}
|
|
|
|
// We're intercepting /matrix/key/v2/server requests here, so check
|
|
// that the URL supplied in the request is for that.
|
|
if req.URL.Path != "/_matrix/key/v2/server" {
|
|
return nil, fmt.Errorf("unexpected request path: %s", req.URL.Path)
|
|
}
|
|
|
|
// Get the keys and JSON-ify them.
|
|
keys := routing.LocalKeys(s.fedconfig)
|
|
body, err := json.MarshalIndent(keys.JSON, "", " ")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// And respond.
|
|
res = &http.Response{
|
|
StatusCode: 200,
|
|
Body: ioutil.NopCloser(bytes.NewReader(body)),
|
|
}
|
|
return
|
|
}
|
|
|
|
func TestServersRequestOwnKeys(t *testing.T) {
|
|
// Each server will request its own keys. There's no reason
|
|
// for this to fail as each server should know its own keys.
|
|
|
|
for name, s := range servers {
|
|
req := gomatrixserverlib.PublicKeyLookupRequest{
|
|
ServerName: s.name,
|
|
KeyID: serverKeyID,
|
|
}
|
|
res, err := s.api.FetchKeys(
|
|
context.Background(),
|
|
map[gomatrixserverlib.PublicKeyLookupRequest]gomatrixserverlib.Timestamp{
|
|
req: gomatrixserverlib.AsTimestamp(time.Now()),
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("server could not fetch own key: %s", err)
|
|
}
|
|
if _, ok := res[req]; !ok {
|
|
t.Fatalf("server didn't return its own key in the results")
|
|
}
|
|
t.Logf("%s's key expires at %s\n", name, res[req].ValidUntilTS.Time())
|
|
}
|
|
}
|
|
|
|
func TestCachingBehaviour(t *testing.T) {
|
|
// Server A will request Server B's key, which has a validity
|
|
// period of an hour from now. We should retrieve the key and
|
|
// it should make it into the cache automatically.
|
|
|
|
req := gomatrixserverlib.PublicKeyLookupRequest{
|
|
ServerName: serverB.name,
|
|
KeyID: serverKeyID,
|
|
}
|
|
ts := gomatrixserverlib.AsTimestamp(time.Now())
|
|
|
|
res, err := serverA.api.FetchKeys(
|
|
context.Background(),
|
|
map[gomatrixserverlib.PublicKeyLookupRequest]gomatrixserverlib.Timestamp{
|
|
req: ts,
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("server A failed to retrieve server B key: %s", err)
|
|
}
|
|
if len(res) != 1 {
|
|
t.Fatalf("server B should have returned one key but instead returned %d keys", len(res))
|
|
}
|
|
if _, ok := res[req]; !ok {
|
|
t.Fatalf("server B isn't included in the key fetch response")
|
|
}
|
|
|
|
// At this point, if the previous key request was a success,
|
|
// then the cache should now contain the key. Check if that's
|
|
// the case - if it isn't then there's something wrong with
|
|
// the cache implementation or we failed to get the key.
|
|
|
|
cres, ok := serverA.cache.GetServerKey(req, ts)
|
|
if !ok {
|
|
t.Fatalf("server B key should be in cache but isn't")
|
|
}
|
|
if !reflect.DeepEqual(cres, res[req]) {
|
|
t.Fatalf("the cached result from server B wasn't what server B gave us")
|
|
}
|
|
|
|
// If we ask the cache for the same key but this time for an event
|
|
// that happened in +30 minutes. Since the validity period is for
|
|
// another hour, then we should get a response back from the cache.
|
|
|
|
_, ok = serverA.cache.GetServerKey(
|
|
req,
|
|
gomatrixserverlib.AsTimestamp(time.Now().Add(time.Minute*30)),
|
|
)
|
|
if !ok {
|
|
t.Fatalf("server B key isn't in cache when it should be (+30 minutes)")
|
|
}
|
|
|
|
// If we ask the cache for the same key but this time for an event
|
|
// that happened in +90 minutes then we should expect to get no
|
|
// cache result. This is because the cache shouldn't return a result
|
|
// that is obviously past the validity of the event.
|
|
|
|
_, ok = serverA.cache.GetServerKey(
|
|
req,
|
|
gomatrixserverlib.AsTimestamp(time.Now().Add(time.Minute*90)),
|
|
)
|
|
if ok {
|
|
t.Fatalf("server B key is in cache when it shouldn't be (+90 minutes)")
|
|
}
|
|
}
|
|
|
|
func TestRenewalBehaviour(t *testing.T) {
|
|
// Server A will request Server C's key but their validity period
|
|
// is an hour in the past. We'll retrieve the key as, even though it's
|
|
// past its validity, it will be able to verify past events.
|
|
|
|
req := gomatrixserverlib.PublicKeyLookupRequest{
|
|
ServerName: serverC.name,
|
|
KeyID: serverKeyID,
|
|
}
|
|
|
|
res, err := serverA.api.FetchKeys(
|
|
context.Background(),
|
|
map[gomatrixserverlib.PublicKeyLookupRequest]gomatrixserverlib.Timestamp{
|
|
req: gomatrixserverlib.AsTimestamp(time.Now()),
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("server A failed to retrieve server C key: %s", err)
|
|
}
|
|
if len(res) != 1 {
|
|
t.Fatalf("server C should have returned one key but instead returned %d keys", len(res))
|
|
}
|
|
if _, ok := res[req]; !ok {
|
|
t.Fatalf("server C isn't included in the key fetch response")
|
|
}
|
|
|
|
// If we ask the cache for the server key for an event that happened
|
|
// 90 minutes ago then we should get a cache result, as the key hadn't
|
|
// passed its validity by that point. The fact that the key is now in
|
|
// the cache is, in itself, proof that we successfully retrieved the
|
|
// key before.
|
|
|
|
oldcached, ok := serverA.cache.GetServerKey(
|
|
req,
|
|
gomatrixserverlib.AsTimestamp(time.Now().Add(-time.Minute*90)),
|
|
)
|
|
if !ok {
|
|
t.Fatalf("server C key isn't in cache when it should be (-90 minutes)")
|
|
}
|
|
|
|
// If we now ask the cache for the same key but this time for an event
|
|
// that only happened 30 minutes ago then we shouldn't get a cached
|
|
// result, as the event happened after the key validity expired. This
|
|
// is really just for sanity checking.
|
|
|
|
_, ok = serverA.cache.GetServerKey(
|
|
req,
|
|
gomatrixserverlib.AsTimestamp(time.Now().Add(-time.Minute*30)),
|
|
)
|
|
if ok {
|
|
t.Fatalf("server B key is in cache when it shouldn't be (-30 minutes)")
|
|
}
|
|
|
|
// We're now going to kick server C into renewing its key. Since we're
|
|
// happy at this point that the key that we already have is from the past
|
|
// then repeating a key fetch should cause us to try and renew the key.
|
|
// If so, then the new key will end up in our cache.
|
|
|
|
serverC.renew()
|
|
|
|
res, err = serverA.api.FetchKeys(
|
|
context.Background(),
|
|
map[gomatrixserverlib.PublicKeyLookupRequest]gomatrixserverlib.Timestamp{
|
|
req: gomatrixserverlib.AsTimestamp(time.Now()),
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("server A failed to retrieve server C key: %s", err)
|
|
}
|
|
if len(res) != 1 {
|
|
t.Fatalf("server C should have returned one key but instead returned %d keys", len(res))
|
|
}
|
|
if _, ok = res[req]; !ok {
|
|
t.Fatalf("server C isn't included in the key fetch response")
|
|
}
|
|
|
|
// We're now going to ask the cache what the new key validity is. If
|
|
// it is still the same as the previous validity then we've failed to
|
|
// retrieve the renewed key. If it's newer then we've successfully got
|
|
// the renewed key.
|
|
|
|
newcached, ok := serverA.cache.GetServerKey(
|
|
req,
|
|
gomatrixserverlib.AsTimestamp(time.Now().Add(-time.Minute*30)),
|
|
)
|
|
if !ok {
|
|
t.Fatalf("server B key isn't in cache when it shouldn't be (post-renewal)")
|
|
}
|
|
if oldcached.ValidUntilTS >= newcached.ValidUntilTS {
|
|
t.Fatalf("the server B key should have been renewed but wasn't")
|
|
}
|
|
t.Log(res)
|
|
}
|