Add cross-signing TOFU support

This commit is contained in:
Tulir Asokan 2022-06-23 14:42:04 +03:00
commit 9162944672
12 changed files with 128 additions and 83 deletions

View file

@ -363,14 +363,32 @@ func (mx *MatrixHandler) sendCryptoStatusError(evt *event.Event, editEvent id.Ev
return ""
}
var errDeviceNotVerified = errors.New("your device is not verified")
var errDeviceNotTrusted = errors.New("your device is not trusted")
var errMessageNotEncrypted = errors.New("unencrypted message")
func deviceUnverifiedErrorWithExplanation(trust id.TrustState) error {
var explanation string
switch trust {
case id.TrustStateBlacklisted:
explanation = "device is blacklisted"
case id.TrustStateUnknownDevice:
explanation = "device info not found"
case id.TrustStateForwarded:
explanation = "keys were forwarded from an unknown device"
case id.TrustStateCrossSignedUntrusted:
explanation = "cross-signing keys changed after setting up the bridge"
default:
return errDeviceNotTrusted
}
return fmt.Errorf("%w (%s)", errDeviceNotTrusted, explanation)
}
func (mx *MatrixHandler) postDecrypt(decrypted *event.Event, retryCount int, errorEventID id.EventID) {
minLevel := mx.bridge.Config.Bridge.GetEncryptionConfig().VerificationLevels.Send
if decrypted.Mautrix.TrustState < minLevel {
mx.log.Warnfln("Dropping %s due to insufficient verification level (event: %s, required: %s)", decrypted.ID, decrypted.Mautrix.TrustState.Description(), minLevel.Description())
go mx.sendCryptoStatusError(decrypted, errorEventID, errDeviceNotVerified, retryCount, true)
mx.log.Warnfln("Dropping %s due to insufficient verification level (event: %s, required: %s)", decrypted.ID, decrypted.Mautrix.TrustState, minLevel)
err := deviceUnverifiedErrorWithExplanation(decrypted.Mautrix.TrustState)
go mx.sendCryptoStatusError(decrypted, errorEventID, err, retryCount, true)
return
}

View file

@ -1,4 +1,4 @@
// Copyright (c) 2020 Tulir Asokan
// Copyright (c) 2022 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
@ -51,9 +51,9 @@ func (mach *OlmMachine) GetCrossSigningPublicKeys(userID id.UserID) (*CrossSigni
selfSigning, _ := dbKeys[id.XSUsageSelfSigning]
userSigning, _ := dbKeys[id.XSUsageUserSigning]
return &CrossSigningPublicKeysCache{
MasterKey: masterKey,
SelfSigningKey: selfSigning,
UserSigningKey: userSigning,
MasterKey: masterKey.Key,
SelfSigningKey: selfSigning.Key,
UserSigningKey: userSigning.Key,
}, nil
}
}

View file

@ -1,4 +1,5 @@
// Copyright (c) 2020 Nikos Filippakis
// Copyright (c) 2022 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
@ -41,20 +42,20 @@ func (mach *OlmMachine) fetchMasterKey(device *DeviceIdentity, content *event.Ve
if !ok {
return "", ErrCrossSigningMasterKeyNotFound
}
masterKeyID := id.NewKeyID(id.KeyAlgorithmEd25519, masterKey.String())
masterKeyID := id.NewKeyID(id.KeyAlgorithmEd25519, masterKey.Key.String())
masterKeyMAC, ok := content.Mac[masterKeyID]
if !ok {
return masterKey, ErrMasterKeyMACNotFound
return masterKey.Key, ErrMasterKeyMACNotFound
}
expectedMasterKeyMAC, _, err := mach.getPKAndKeysMAC(verState.sas, device.UserID, device.DeviceID,
mach.Client.UserID, mach.Client.DeviceID, transactionID, masterKey, masterKeyID, content.Mac)
mach.Client.UserID, mach.Client.DeviceID, transactionID, masterKey.Key, masterKeyID, content.Mac)
if err != nil {
return masterKey, fmt.Errorf("failed to calculate expected MAC for master key: %w", err)
return masterKey.Key, fmt.Errorf("failed to calculate expected MAC for master key: %w", err)
}
if masterKeyMAC != expectedMasterKeyMAC {
err = fmt.Errorf("%w: expected %s, got %s", ErrMismatchingMasterKeyMAC, expectedMasterKeyMAC, masterKeyMAC)
}
return masterKey, err
return masterKey.Key, err
}
// SignUser creates a cross-signing signature for a user, stores it and uploads it to the server.

View file

@ -1,4 +1,5 @@
// Copyright (c) 2020 Nikos Filippakis
// Copyright (c) 2022 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
@ -23,9 +24,9 @@ func (mach *OlmMachine) storeCrossSigningKeys(crossSigningKeys map[id.UserID]mau
// got a new key with the same usage as an existing key
for _, newKeyUsage := range userKeys.Usage {
if newKeyUsage == curKeyUsage {
if _, ok := userKeys.Keys[id.NewKeyID(id.KeyAlgorithmEd25519, curKey.String())]; !ok {
if _, ok := userKeys.Keys[id.NewKeyID(id.KeyAlgorithmEd25519, curKey.Key.String())]; !ok {
// old key is not in the new key map, so we drop signatures made by it
if count, err := mach.CryptoStore.DropSignaturesByKey(userID, curKey); err != nil {
if count, err := mach.CryptoStore.DropSignaturesByKey(userID, curKey.Key); err != nil {
mach.Log.Error("Error deleting old signatures made by %s (%s): %v", curKey, curKeyUsage, err)
} else {
mach.Log.Debug("Dropped %d signatures made by key %s (%s) as it has been replaced", count, curKey, curKeyUsage)

View file

@ -1,4 +1,5 @@
// Copyright (c) 2020 Nikos Filippakis
// Copyright (c) 2022 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
@ -10,49 +11,47 @@ import (
"maunium.net/go/mautrix/id"
)
func (mach *OlmMachine) IsKeyCrossSigned(userID id.UserID, deviceSigningKey id.SigningKey) bool {
theirKeys, err := mach.CryptoStore.GetCrossSigningKeys(userID)
if err != nil {
mach.Log.Error("Error retrieving cross-singing key of user %v from database: %v", userID, err)
return false
}
theirMSK, ok := theirKeys[id.XSUsageMaster]
if !ok {
mach.Log.Error("Master key of user %v not found", userID)
return false
}
theirSSK, ok := theirKeys[id.XSUsageSelfSigning]
if !ok {
mach.Log.Error("Self-signing key of user %v not found", userID)
return false
}
sskSigExists, err := mach.CryptoStore.IsKeySignedBy(userID, theirSSK, userID, theirMSK)
if err != nil {
mach.Log.Error("Error retrieving cross-singing signatures for master key of user %v from database: %v", userID, err)
return false
}
if !sskSigExists {
mach.Log.Warn("Self-signing key of user %v is not signed by their master key", userID)
return false
}
deviceSigExists, err := mach.CryptoStore.IsKeySignedBy(userID, deviceSigningKey, userID, theirSSK)
if err != nil {
mach.Log.Error("Error retrieving cross-singing signatures for master key of user %v from database: %v", userID, err)
return false
}
return deviceSigExists
}
// ResolveTrust resolves the trust state of the device from cross-signing.
func (mach *OlmMachine) ResolveTrust(device *DeviceIdentity) id.TrustState {
if device.Trust == id.TrustStateVerified || device.Trust == id.TrustStateBlacklisted {
return device.Trust
}
if mach.IsKeyCrossSigned(device.UserID, device.SigningKey) {
theirKeys, err := mach.CryptoStore.GetCrossSigningKeys(device.UserID)
if err != nil {
mach.Log.Error("Error retrieving cross-singing key of user %v from database: %v", device.UserID, err)
return id.TrustStateUnset
}
theirMSK, ok := theirKeys[id.XSUsageMaster]
if !ok {
mach.Log.Error("Master key of user %v not found", device.UserID)
return id.TrustStateUnset
}
theirSSK, ok := theirKeys[id.XSUsageSelfSigning]
if !ok {
mach.Log.Error("Self-signing key of user %v not found", device.UserID)
return id.TrustStateUnset
}
sskSigExists, err := mach.CryptoStore.IsKeySignedBy(device.UserID, theirSSK.Key, device.UserID, theirMSK.Key)
if err != nil {
mach.Log.Error("Error retrieving cross-singing signatures for master key of user %v from database: %v", device.UserID, err)
return id.TrustStateUnset
}
if !sskSigExists {
mach.Log.Warn("Self-signing key of user %v is not signed by their master key", device.UserID)
return id.TrustStateUnset
}
deviceSigExists, err := mach.CryptoStore.IsKeySignedBy(device.UserID, device.SigningKey, device.UserID, theirSSK.Key)
if err != nil {
mach.Log.Error("Error retrieving cross-singing signatures for master key of user %v from database: %v", device.UserID, err)
return id.TrustStateUnset
}
if deviceSigExists {
if mach.IsUserTrusted(device.UserID) {
return id.TrustStateCrossSignedTrusted
return id.TrustStateCrossSignedVerified
} else if theirMSK.Key == theirMSK.First {
return id.TrustStateCrossSignedTOFU
}
return id.TrustStateCrossSigned
return id.TrustStateCrossSignedUntrusted
}
return id.TrustStateUnset
}
@ -60,7 +59,7 @@ func (mach *OlmMachine) ResolveTrust(device *DeviceIdentity) id.TrustState {
// IsDeviceTrusted returns whether a device has been determined to be trusted either through verification or cross-signing.
func (mach *OlmMachine) IsDeviceTrusted(device *DeviceIdentity) bool {
switch mach.ResolveTrust(device) {
case id.TrustStateVerified, id.TrustStateCrossSigned, id.TrustStateCrossSignedTrusted:
case id.TrustStateVerified, id.TrustStateCrossSignedTOFU, id.TrustStateCrossSignedVerified:
return true
default:
return false
@ -97,7 +96,7 @@ func (mach *OlmMachine) IsUserTrusted(userID id.UserID) bool {
mach.Log.Error("Master key of user %v not found", userID)
return false
}
sigExists, err := mach.CryptoStore.IsKeySignedBy(userID, theirMskKey, mach.Client.UserID, csPubkeys.UserSigningKey)
sigExists, err := mach.CryptoStore.IsKeySignedBy(userID, theirMskKey.Key, mach.Client.UserID, csPubkeys.UserSigningKey)
if err != nil {
mach.Log.Error("Error retrieving cross-singing signatures for master key of user %v from database: %v", userID, err)
return false

View file

@ -258,7 +258,7 @@ func (mach *OlmMachine) findOlmSessionsForUser(session *OutboundGroupSession, us
} else if trustState := mach.ResolveTrust(device); trustState < mach.SendKeysMinTrust {
mach.Log.Debug(
"Not encrypting group session %s for %s of %s: device is not verified (minimum: %s, device: %s)",
session.ID(), deviceID, userID, mach.SendKeysMinTrust.Description(), trustState.Description(),
session.ID(), deviceID, userID, mach.SendKeysMinTrust, trustState,
)
withheld[deviceID] = &event.Content{Parsed: &event.RoomKeyWithheldEventContent{
RoomID: session.RoomID,

View file

@ -194,10 +194,10 @@ func (mach *OlmMachine) defaultAllowKeyShare(device *DeviceIdentity, _ event.Req
mach.Log.Debug("Ignoring key request from blacklisted device %s", device.DeviceID)
return &KeyShareRejectBlacklisted
} else if trustState := mach.ResolveTrust(device); trustState >= mach.ShareKeysMinTrust {
mach.Log.Debug("Accepting key request from device %s (trust state: %s)", device.DeviceID, trustState.Description())
mach.Log.Debug("Accepting key request from device %s (trust state: %s)", device.DeviceID, trustState)
return nil
} else {
mach.Log.Debug("Ignoring key request from unverified device %s (trust state: %s)", device.DeviceID, trustState.Description())
mach.Log.Debug("Ignoring key request from unverified device %s (trust state: %s)", device.DeviceID, trustState)
return &KeyShareRejectUnverified
}
}

View file

@ -88,7 +88,7 @@ func NewOlmMachine(client *mautrix.Client, log Logger, cryptoStore Store, stateS
StateStore: stateStore,
SendKeysMinTrust: id.TrustStateUnset,
ShareKeysMinTrust: id.TrustStateCrossSigned,
ShareKeysMinTrust: id.TrustStateCrossSignedTOFU,
DefaultSASTimeout: 10 * time.Minute,
AcceptVerificationFrom: func(string, *DeviceIdentity, id.RoomID) (VerificationRequestResponse, VerificationHooks) {

View file

@ -630,27 +630,27 @@ func (store *SQLCryptoStore) FilterTrackedUsers(users []id.UserID) []id.UserID {
// PutCrossSigningKey stores a cross-signing key of some user along with its usage.
func (store *SQLCryptoStore) PutCrossSigningKey(userID id.UserID, usage id.CrossSigningUsage, key id.Ed25519) error {
_, err := store.DB.Exec(`
INSERT INTO crypto_cross_signing_keys (user_id, usage, key) VALUES ($1, $2, $3)
INSERT INTO crypto_cross_signing_keys (user_id, usage, key, first_seen_key) VALUES ($1, $2, $3, $4)
ON CONFLICT (user_id, usage) DO UPDATE SET key=excluded.key
`, userID, usage, key)
`, userID, usage, key, key)
return err
}
// GetCrossSigningKeys retrieves a user's stored cross-signing keys.
func (store *SQLCryptoStore) GetCrossSigningKeys(userID id.UserID) (map[id.CrossSigningUsage]id.Ed25519, error) {
rows, err := store.DB.Query("SELECT usage, key FROM crypto_cross_signing_keys WHERE user_id=$1", userID)
func (store *SQLCryptoStore) GetCrossSigningKeys(userID id.UserID) (map[id.CrossSigningUsage]CrossSigningKey, error) {
rows, err := store.DB.Query("SELECT usage, key, first_seen_key FROM crypto_cross_signing_keys WHERE user_id=$1", userID)
if err != nil {
return nil, err
}
data := make(map[id.CrossSigningUsage]id.Ed25519)
data := make(map[id.CrossSigningUsage]CrossSigningKey)
for rows.Next() {
var usage id.CrossSigningUsage
var key id.Ed25519
err := rows.Scan(&usage, &key)
var key, first id.Ed25519
err := rows.Scan(&usage, &key, &first)
if err != nil {
return nil, err
}
data[usage] = key
data[usage] = CrossSigningKey{key, first}
}
return data, nil

View file

@ -0,0 +1,5 @@
-- v8: Add expired field to cross signing keys
ALTER TABLE crypto_cross_signing_keys ADD COLUMN first_seen_key CHAR(43);
UPDATE crypto_cross_signing_keys SET first_seen_key=key;
-- only: postgres
ALTER TABLE crypto_cross_signing_keys ALTER COLUMN first_seen_key SET NOT NULL;

View file

@ -1,4 +1,4 @@
// Copyright (c) 2020 Tulir Asokan
// Copyright (c) 2022 Tulir Asokan
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
@ -34,6 +34,11 @@ func (device *DeviceIdentity) Fingerprint() string {
return Fingerprint(device.SigningKey)
}
type CrossSigningKey struct {
Key id.Ed25519
First id.Ed25519
}
var ErrGroupSessionWithheld = errors.New("group session has been withheld")
// Store is used by OlmMachine to store Olm and Megolm sessions, user device lists and message indices.
@ -122,7 +127,7 @@ type Store interface {
// PutCrossSigningKey stores a cross-signing key of some user along with its usage.
PutCrossSigningKey(id.UserID, id.CrossSigningUsage, id.Ed25519) error
// GetCrossSigningKeys retrieves a user's stored cross-signing keys.
GetCrossSigningKeys(id.UserID) (map[id.CrossSigningUsage]id.Ed25519, error)
GetCrossSigningKeys(id.UserID) (map[id.CrossSigningUsage]CrossSigningKey, error)
// PutSignature stores a signature of a cross-signing or device key along with the signer's user ID and key.
PutSignature(id.UserID, id.Ed25519, id.UserID, id.Ed25519, string) error
// GetSignaturesForKeyBy returns the signatures for a cross-signing or device key by the given signer.
@ -158,7 +163,7 @@ type GobStore struct {
OutGroupSessions map[id.RoomID]*OutboundGroupSession
MessageIndices map[messageIndexKey]messageIndexValue
Devices map[id.UserID]map[id.DeviceID]*DeviceIdentity
CrossSigningKeys map[id.UserID]map[id.CrossSigningUsage]id.Ed25519
CrossSigningKeys map[id.UserID]map[id.CrossSigningUsage]CrossSigningKey
KeySignatures map[id.UserID]map[id.Ed25519]map[id.UserID]map[id.Ed25519]string
}
@ -176,7 +181,7 @@ func NewGobStore(path string) (*GobStore, error) {
OutGroupSessions: make(map[id.RoomID]*OutboundGroupSession),
MessageIndices: make(map[messageIndexKey]messageIndexValue),
Devices: make(map[id.UserID]map[id.DeviceID]*DeviceIdentity),
CrossSigningKeys: make(map[id.UserID]map[id.CrossSigningUsage]id.Ed25519),
CrossSigningKeys: make(map[id.UserID]map[id.CrossSigningUsage]CrossSigningKey),
KeySignatures: make(map[id.UserID]map[id.Ed25519]map[id.UserID]map[id.Ed25519]string),
}
return gs, gs.load()
@ -502,21 +507,30 @@ func (gs *GobStore) PutCrossSigningKey(userID id.UserID, usage id.CrossSigningUs
gs.lock.RLock()
userKeys, ok := gs.CrossSigningKeys[userID]
if !ok {
userKeys = make(map[id.CrossSigningUsage]id.Ed25519)
userKeys = make(map[id.CrossSigningUsage]CrossSigningKey)
gs.CrossSigningKeys[userID] = userKeys
}
userKeys[usage] = key
existing, ok := userKeys[usage]
if ok {
existing.Key = key
userKeys[usage] = existing
} else {
userKeys[usage] = CrossSigningKey{
Key: key,
First: key,
}
}
err := gs.save()
gs.lock.RUnlock()
return err
}
func (gs *GobStore) GetCrossSigningKeys(userID id.UserID) (map[id.CrossSigningUsage]id.Ed25519, error) {
func (gs *GobStore) GetCrossSigningKeys(userID id.UserID) (map[id.CrossSigningUsage]CrossSigningKey, error) {
gs.lock.RLock()
defer gs.lock.RUnlock()
keys, ok := gs.CrossSigningKeys[userID]
if !ok {
return map[id.CrossSigningUsage]id.Ed25519{}, nil
return map[id.CrossSigningUsage]CrossSigningKey{}, nil
}
return keys, nil
}

View file

@ -15,14 +15,15 @@ import (
type TrustState int
const (
TrustStateBlacklisted TrustState = -100
TrustStateUnset TrustState = 0
TrustStateUnknownDevice TrustState = 10
TrustStateForwarded TrustState = 20
TrustStateCrossSigned TrustState = 100
TrustStateCrossSignedTrusted TrustState = 200
TrustStateVerified TrustState = 300
TrustStateInvalid TrustState = (1 << 31) - 1
TrustStateBlacklisted TrustState = -100
TrustStateUnset TrustState = 0
TrustStateUnknownDevice TrustState = 10
TrustStateForwarded TrustState = 20
TrustStateCrossSignedUntrusted TrustState = 50
TrustStateCrossSignedTOFU TrustState = 100
TrustStateCrossSignedVerified TrustState = 200
TrustStateVerified TrustState = 300
TrustStateInvalid TrustState = (1 << 31) - 1
)
func (ts *TrustState) UnmarshalText(data []byte) error {
@ -45,14 +46,16 @@ func ParseTrustState(val string) TrustState {
return TrustStateBlacklisted
case "unverified":
return TrustStateUnset
case "cross-signed-untrusted", "cross-signed, untrusted":
return TrustStateCrossSignedUntrusted
case "unknown-device", "unknown device":
return TrustStateUnknownDevice
case "forwarded":
return TrustStateForwarded
case "cross-signed", "tofu", "verified (via cross-signing, tofu)":
return TrustStateCrossSigned
case "cross-signed-trusted", "verified (via cross-signing, trusted user)":
return TrustStateCrossSignedTrusted
case "cross-signed-tofu", "cross-signed", "cross-signed, trusted on first use":
return TrustStateCrossSignedTOFU
case "cross-signed-verified", "cross-signed-trusted", "cross-signed, verified user user":
return TrustStateCrossSignedVerified
case "verified":
return TrustStateVerified
default:
@ -66,14 +69,16 @@ func (ts TrustState) String() string {
return "blacklisted"
case TrustStateUnset:
return "unverified"
case TrustStateCrossSignedUntrusted:
return "cross-signed-untrusted"
case TrustStateUnknownDevice:
return "unknown-device"
case TrustStateForwarded:
return "forwarded"
case TrustStateCrossSigned:
return "cross-signed"
case TrustStateCrossSignedTrusted:
return "cross-signed-trusted"
case TrustStateCrossSignedTOFU:
return "cross-signed-tofu"
case TrustStateCrossSignedVerified:
return "cross-signed-verified"
case TrustStateVerified:
return "verified"
default:
@ -87,14 +92,16 @@ func (ts TrustState) Description() string {
return "blacklisted"
case TrustStateUnset:
return "unverified"
case TrustStateCrossSignedUntrusted:
return "cross-signed, untrusted"
case TrustStateUnknownDevice:
return "unknown device"
case TrustStateForwarded:
return "forwarded"
case TrustStateCrossSigned:
return "cross-signed, tofu"
case TrustStateCrossSignedTrusted:
return "cross-signed, trusted user"
case TrustStateCrossSignedTOFU:
return "cross-signed, trusted on first use"
case TrustStateCrossSignedVerified:
return "cross-signed, verified user"
case TrustStateVerified:
return "verified locally"
default: