diff --git a/dendrite-config.yaml b/dendrite-config.yaml index 8c737692..b71fb509 100644 --- a/dendrite-config.yaml +++ b/dendrite-config.yaml @@ -38,6 +38,14 @@ global: # The path to the signing private key file, used to sign requests and events. private_key: matrix_key.pem + # The paths and expiry timestamps (as a UNIX timestamp in millisecond precision) + # to old signing private keys that were formerly in use on this domain. These + # keys will not be used for federation request or event signing, but will be + # provided to any other homeserver that asks when trying to verify old events. + # old_private_keys: + # - private_key: old_matrix_key.pem + # expired_at: 1601024554498 + # How long a remote server can cache our server signing key before requesting it # again. Increasing this number will reduce the number of requests made by other # servers for our key but increases the period that a compromised key will be diff --git a/federationapi/routing/keys.go b/federationapi/routing/keys.go index 785be090..17762b03 100644 --- a/federationapi/routing/keys.go +++ b/federationapi/routing/keys.go @@ -136,6 +136,8 @@ func localKeys(cfg *config.FederationAPI, validUntil time.Time) (*gomatrixserver var keys gomatrixserverlib.ServerKeys keys.ServerName = cfg.Matrix.ServerName + keys.TLSFingerprints = cfg.TLSFingerPrints + keys.ValidUntilTS = gomatrixserverlib.AsTimestamp(validUntil) publicKey := cfg.Matrix.PrivateKey.Public().(ed25519.PublicKey) @@ -145,9 +147,15 @@ func localKeys(cfg *config.FederationAPI, validUntil time.Time) (*gomatrixserver }, } - keys.TLSFingerprints = cfg.TLSFingerPrints keys.OldVerifyKeys = map[gomatrixserverlib.KeyID]gomatrixserverlib.OldVerifyKey{} - keys.ValidUntilTS = gomatrixserverlib.AsTimestamp(validUntil) + for _, oldVerifyKey := range cfg.Matrix.OldVerifyKeys { + keys.OldVerifyKeys[oldVerifyKey.KeyID] = gomatrixserverlib.OldVerifyKey{ + VerifyKey: gomatrixserverlib.VerifyKey{ + Key: gomatrixserverlib.Base64Bytes(oldVerifyKey.PrivateKey), + }, + ExpiredTS: oldVerifyKey.ExpiredAt, + } + } toSign, err := json.Marshal(keys.ServerKeyFields) if err != nil { diff --git a/internal/config/config.go b/internal/config/config.go index d75500db..7528aa23 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -228,10 +228,30 @@ func loadConfig( return nil, err } - if c.Global.KeyID, c.Global.PrivateKey, err = readKeyPEM(privateKeyPath, privateKeyData); err != nil { + if c.Global.KeyID, c.Global.PrivateKey, err = readKeyPEM(privateKeyPath, privateKeyData, true); err != nil { return nil, err } + for i, oldPrivateKey := range c.Global.OldVerifyKeys { + var oldPrivateKeyData []byte + + oldPrivateKeyPath := absPath(basePath, oldPrivateKey.PrivateKeyPath) + oldPrivateKeyData, err = readFile(oldPrivateKeyPath) + if err != nil { + return nil, err + } + + // NOTSPEC: Ordinarily we should enforce key ID formatting, but since there are + // a number of private keys out there with non-compatible symbols in them due + // to lack of validation in Synapse, we won't enforce that for old verify keys. + keyID, privateKey, perr := readKeyPEM(oldPrivateKeyPath, oldPrivateKeyData, false) + if perr != nil { + return nil, perr + } + + c.Global.OldVerifyKeys[i].KeyID, c.Global.OldVerifyKeys[i].PrivateKey = keyID, privateKey + } + for _, certPath := range c.FederationAPI.FederationCertificatePaths { absCertPath := absPath(basePath, certPath) var pemData []byte @@ -444,7 +464,7 @@ func absPath(dir string, path Path) string { return filepath.Join(dir, string(path)) } -func readKeyPEM(path string, data []byte) (gomatrixserverlib.KeyID, ed25519.PrivateKey, error) { +func readKeyPEM(path string, data []byte, enforceKeyIDFormat bool) (gomatrixserverlib.KeyID, ed25519.PrivateKey, error) { for { var keyBlock *pem.Block keyBlock, data = pem.Decode(data) @@ -462,7 +482,7 @@ func readKeyPEM(path string, data []byte) (gomatrixserverlib.KeyID, ed25519.Priv if !strings.HasPrefix(keyID, "ed25519:") { return "", nil, fmt.Errorf("key ID %q doesn't start with \"ed25519:\" in %q", keyID, path) } - if !keyIDRegexp.MatchString(keyID) { + if enforceKeyIDFormat && !keyIDRegexp.MatchString(keyID) { return "", nil, fmt.Errorf("key ID %q in %q contains illegal characters (use a-z, A-Z, 0-9 and _ only)", keyID, path) } _, privKey, err := ed25519.GenerateKey(bytes.NewReader(keyBlock.Bytes)) diff --git a/internal/config/config_global.go b/internal/config/config_global.go index 03f522be..d210a3ac 100644 --- a/internal/config/config_global.go +++ b/internal/config/config_global.go @@ -22,6 +22,11 @@ type Global struct { // prefix "ed25519:". KeyID gomatrixserverlib.KeyID `yaml:"-"` + // Information about old private keys that used to be used to sign requests and + // events on this domain. They will not be used but will be advertised to other + // servers that ask for them to help verify old events. + OldVerifyKeys []OldVerifyKeys `yaml:"old_private_keys"` + // How long a remote server can cache our server key for before requesting it again. // Increasing this number will reduce the number of requests made by remote servers // for our key, but increases the period a compromised key will be considered valid @@ -60,6 +65,21 @@ func (c *Global) Verify(configErrs *ConfigErrors, isMonolith bool) { c.Metrics.Verify(configErrs, isMonolith) } +type OldVerifyKeys struct { + // Path to the private key. + PrivateKeyPath Path `yaml:"private_key"` + + // The private key itself. + PrivateKey ed25519.PrivateKey `yaml:"-"` + + // The key ID of the private key. + KeyID gomatrixserverlib.KeyID `yaml:"-"` + + // When the private key was designed as "expired", as a UNIX timestamp + // in millisecond precision. + ExpiredAt gomatrixserverlib.Timestamp `yaml:"expired_at"` +} + // The configuration to use for Prometheus metrics type Metrics struct { // Whether or not the metrics are enabled diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 39b3ee47..7549fa02 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -234,7 +234,7 @@ func (m mockReadFile) readFile(path string) ([]byte, error) { } func TestReadKey(t *testing.T) { - keyID, _, err := readKeyPEM("path/to/key", []byte(testKey)) + keyID, _, err := readKeyPEM("path/to/key", []byte(testKey), true) if err != nil { t.Error("failed to load private key:", err) }