From 151090527b909f18620faddf6ded729038dd8872 Mon Sep 17 00:00:00 2001 From: Bruno Windels <274386+bwindels@users.noreply.github.com> Date: Fri, 24 Feb 2023 17:45:56 +0100 Subject: [PATCH] Store cross-signing keys in format as returned from server, in separate store This will make it easier to sign and verify signatures with these keys, as the signed value needs to have the same layout when signing and for every verification. --- src/matrix/e2ee/DeviceTracker.js | 151 ++++++++++-------- src/matrix/e2ee/common.js | 13 +- src/matrix/storage/common.ts | 3 +- src/matrix/storage/idb/Transaction.ts | 5 + src/matrix/storage/idb/schema.ts | 8 +- .../idb/stores/CrossSigningKeyStore.ts | 70 ++++++++ src/matrix/verification/CrossSigning.ts | 51 +++++- 7 files changed, 221 insertions(+), 80 deletions(-) create mode 100644 src/matrix/storage/idb/stores/CrossSigningKeyStore.ts diff --git a/src/matrix/e2ee/DeviceTracker.js b/src/matrix/e2ee/DeviceTracker.js index b669629e..5d991fcb 100644 --- a/src/matrix/e2ee/DeviceTracker.js +++ b/src/matrix/e2ee/DeviceTracker.js @@ -14,9 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {verifyEd25519Signature, SIGNATURE_ALGORITHM} from "./common.js"; +import {verifyEd25519Signature, getEd25519Signature, SIGNATURE_ALGORITHM} from "./common.js"; import {HistoryVisibility, shouldShareKey} from "./common.js"; import {RoomMember} from "../room/members/RoomMember.js"; +import {getKeyUsage, getKeyEd25519Key, getKeyUserId, KeyUsage} from "../verification/CrossSigning"; const TRACKING_STATUS_OUTDATED = 0; const TRACKING_STATUS_UPTODATE = 1; @@ -153,7 +154,7 @@ export class DeviceTracker { } } - async getCrossSigningKeysForUser(userId, hsApi, log) { + async getCrossSigningKeyForUser(userId, usage, hsApi, log) { return await log.wrap("DeviceTracker.getMasterKeyForUser", async log => { let txn = await this._storage.readTxn([ this._storage.storeNames.userIdentities @@ -163,13 +164,16 @@ export class DeviceTracker { return userIdentity.crossSigningKeys; } // fetch from hs - await this._queryKeys([userId], hsApi, log); - // Retreive from storage now - txn = await this._storage.readTxn([ - this._storage.storeNames.userIdentities - ]); - userIdentity = await txn.userIdentities.get(userId); - return userIdentity?.crossSigningKeys; + const keys = await this._queryKeys([userId], hsApi, log); + switch (usage) { + case KeyUsage.Master: + return keys.masterKeys.get(userId); + case KeyUsage.SelfSigning: + return keys.selfSigningKeys.get(userId); + case KeyUsage.UserSigning: + return keys.userSigningKeys.get(userId); + + } }); } @@ -245,22 +249,29 @@ export class DeviceTracker { "token": this._getSyncToken() }, {log}).response(); - const masterKeys = log.wrap("master keys", log => this._filterValidMasterKeys(deviceKeyResponse, log)); - const selfSigningKeys = log.wrap("self-signing keys", log => this._filterVerifiedCrossSigningKeys(deviceKeyResponse["self_signing_keys"], "self_signing", masterKeys, log)) - const verifiedKeysPerUser = log.wrap("verify", log => this._filterVerifiedDeviceKeys(deviceKeyResponse["device_keys"], log)); + const masterKeys = log.wrap("master keys", log => this._filterVerifiedCrossSigningKeys(deviceKeyResponse["master_keys"], KeyUsage.Master, undefined, log)); + const selfSigningKeys = log.wrap("self-signing keys", log => this._filterVerifiedCrossSigningKeys(deviceKeyResponse["self_signing_keys"], KeyUsage.SelfSigning, masterKeys, log)); + const userSigningKeys = log.wrap("user-signing keys", log => this._filterVerifiedCrossSigningKeys(deviceKeyResponse["user_signing_keys"], KeyUsage.UserSigning, masterKeys, log)); + const verifiedKeysPerUser = log.wrap("device keys", log => this._filterVerifiedDeviceKeys(deviceKeyResponse["device_keys"], log)); const txn = await this._storage.readWriteTxn([ this._storage.storeNames.userIdentities, this._storage.storeNames.deviceIdentities, + this._storage.storeNames.crossSigningKeys, ]); let deviceIdentities; try { + for (const key of masterKeys.values()) { + txn.crossSigningKeys.set(key); + } + for (const key of selfSigningKeys.values()) { + txn.crossSigningKeys.set(key); + } + for (const key of userSigningKeys.values()) { + txn.crossSigningKeys.set(key); + } const devicesIdentitiesPerUser = await Promise.all(verifiedKeysPerUser.map(async ({userId, verifiedKeys}) => { const deviceIdentities = verifiedKeys.map(deviceKeysAsDeviceIdentity); - const crossSigningKeys = { - masterKey: masterKeys.get(userId), - selfSigningKey: selfSigningKeys.get(userId), - }; - return await this._storeQueriedDevicesForUserId(userId, crossSigningKeys, deviceIdentities, txn); + return await this._storeQueriedDevicesForUserId(userId, deviceIdentities, txn); })); deviceIdentities = devicesIdentitiesPerUser.reduce((all, devices) => all.concat(devices), []); log.set("devices", deviceIdentities.length); @@ -269,10 +280,15 @@ export class DeviceTracker { throw err; } await txn.complete(); - return deviceIdentities; + return { + deviceIdentities, + masterKeys, + selfSigningKeys, + userSigningKeys + }; } - async _storeQueriedDevicesForUserId(userId, crossSigningKeys, deviceIdentities, txn) { + async _storeQueriedDevicesForUserId(userId, deviceIdentities, txn) { const knownDeviceIds = await txn.deviceIdentities.getAllDeviceIds(userId); // delete any devices that we know off but are not in the response anymore. // important this happens before checking if the ed25519 key changed, @@ -313,67 +329,63 @@ export class DeviceTracker { identity = createUserIdentity(userId); } identity.deviceTrackingStatus = TRACKING_STATUS_UPTODATE; - identity.crossSigningKeys = crossSigningKeys; txn.userIdentities.set(identity); return allDeviceIdentities; } - _filterValidMasterKeys(keyQueryResponse, log) { - const masterKeys = new Map(); - const masterKeysResponse = keyQueryResponse["master_keys"]; - if (!masterKeysResponse) { - return masterKeys; - } - const validMasterKeyResponses = Object.entries(masterKeysResponse).filter(([userId, keyInfo]) => { - if (keyInfo["user_id"] !== userId) { - return false; - } - if (!Array.isArray(keyInfo.usage) || !keyInfo.usage.includes("master")) { - return false; - } - return true; - }); - validMasterKeyResponses.reduce((msks, [userId, keyInfo]) => { - const keyIds = Object.keys(keyInfo.keys); - if (keyIds.length !== 1) { - return false; - } - const masterKey = keyInfo.keys[keyIds[0]]; - msks.set(userId, masterKey); - return msks; - }, masterKeys); - return masterKeys; - } - - _filterVerifiedCrossSigningKeys(crossSigningKeysResponse, usage, masterKeys, log) { + _filterVerifiedCrossSigningKeys(crossSigningKeysResponse, usage, parentKeys, log) { const keys = new Map(); if (!crossSigningKeysResponse) { return keys; } - const validKeysResponses = Object.entries(crossSigningKeysResponse).filter(([userId, keyInfo]) => { - if (keyInfo["user_id"] !== userId) { - return false; - } - if (!Array.isArray(keyInfo.usage) || !keyInfo.usage.includes(usage)) { - return false; - } - // verify with master key - const masterKey = masterKeys.get(userId); - return verifyEd25519Signature(this._olmUtil, userId, masterKey, masterKey, keyInfo, log); - }); - validKeysResponses.reduce((keys, [userId, keyInfo]) => { - const keyIds = Object.keys(keyInfo.keys); - if (keyIds.length !== 1) { - return false; - } - const key = keyInfo.keys[keyIds[0]]; - keys.set(userId, key); - return keys; - }, keys); + for (const [userId, keyInfo] of Object.entries(crossSigningKeysResponse)) { + log.wrap({l: userId}, log => { + const parentKeyInfo = parentKeys?.get(userId); + const parentKey = parentKeyInfo && getKeyEd25519Key(parentKeyInfo); + if (this._validateCrossSigningKey(userId, keyInfo, usage, parentKey, log)) { + keys.set(getKeyUserId(keyInfo), keyInfo); + } + }); + } return keys; } + _validateCrossSigningKey(userId, keyInfo, usage, parentKey, log) { + if (getKeyUserId(keyInfo) !== userId) { + log.log({l: "user_id mismatch", userId: keyInfo["user_id"]}); + return false; + } + if (getKeyUsage(keyInfo) !== usage) { + log.log({l: "usage mismatch", usage: keyInfo.usage}); + return false; + } + const publicKey = getKeyEd25519Key(keyInfo); + if (!publicKey) { + log.log({l: "no ed25519 key", keys: keyInfo.keys}); + return false; + } + const isSelfSigned = usage === "master"; + const keyToVerifyWith = isSelfSigned ? publicKey : parentKey; + if (!keyToVerifyWith) { + log.log("signing_key not found"); + return false; + } + const hasSignature = !!getEd25519Signature(keyInfo, userId, keyToVerifyWith); + // self-signature is optional for now, not all keys seem to have it + if (!hasSignature && keyToVerifyWith !== publicKey) { + log.log({l: "signature not found", key: keyToVerifyWith}); + return false; + } + if (hasSignature) { + if(!verifyEd25519Signature(this._olmUtil, userId, keyToVerifyWith, keyToVerifyWith, keyInfo, log)) { + log.log("signature mismatch"); + return false; + } + } + return true; + } + /** * @return {Array<{userId, verifiedKeys: Array>} */ @@ -580,7 +592,8 @@ export class DeviceTracker { // TODO: ignore the race between /sync and /keys/query for now, // where users could get marked as outdated or added/removed from the room while // querying keys - queriedDevices = await this._queryKeys(outdatedUserIds, hsApi, log); + const {deviceIdentities} = await this._queryKeys(outdatedUserIds, hsApi, log); + queriedDevices = deviceIdentities; } const deviceTxn = await this._storage.readTxn([ diff --git a/src/matrix/e2ee/common.js b/src/matrix/e2ee/common.js index cc3bfff5..9c5fe66c 100644 --- a/src/matrix/e2ee/common.js +++ b/src/matrix/e2ee/common.js @@ -35,16 +35,21 @@ export class DecryptionError extends Error { export const SIGNATURE_ALGORITHM = "ed25519"; +export function getEd25519Signature(signedValue, userId, deviceOrKeyId) { + return signedValue?.signatures?.[userId]?.[`${SIGNATURE_ALGORITHM}:${deviceOrKeyId}`]; +} + export function verifyEd25519Signature(olmUtil, userId, deviceOrKeyId, ed25519Key, value, log = undefined) { + const signature = getEd25519Signature(value, userId, deviceOrKeyId); + if (!signature) { + log?.set("no_signature", true); + return false; + } const clone = Object.assign({}, value); delete clone.unsigned; delete clone.signatures; const canonicalJson = anotherjson.stringify(clone); - const signature = value?.signatures?.[userId]?.[`${SIGNATURE_ALGORITHM}:${deviceOrKeyId}`]; try { - if (!signature) { - throw new Error("no signature"); - } // throws when signature is invalid olmUtil.ed25519_verify(ed25519Key, canonicalJson, signature); return true; diff --git a/src/matrix/storage/common.ts b/src/matrix/storage/common.ts index e1e34917..adebcdd6 100644 --- a/src/matrix/storage/common.ts +++ b/src/matrix/storage/common.ts @@ -33,7 +33,8 @@ export enum StoreNames { groupSessionDecryptions = "groupSessionDecryptions", operations = "operations", accountData = "accountData", - calls = "calls" + calls = "calls", + crossSigningKeys = "crossSigningKeys" } export const STORE_NAMES: Readonly = Object.values(StoreNames); diff --git a/src/matrix/storage/idb/Transaction.ts b/src/matrix/storage/idb/Transaction.ts index 7a8de420..532ffd1d 100644 --- a/src/matrix/storage/idb/Transaction.ts +++ b/src/matrix/storage/idb/Transaction.ts @@ -30,6 +30,7 @@ import {TimelineFragmentStore} from "./stores/TimelineFragmentStore"; import {PendingEventStore} from "./stores/PendingEventStore"; import {UserIdentityStore} from "./stores/UserIdentityStore"; import {DeviceIdentityStore} from "./stores/DeviceIdentityStore"; +import {CrossSigningKeyStore} from "./stores/CrossSigningKeyStore"; import {OlmSessionStore} from "./stores/OlmSessionStore"; import {InboundGroupSessionStore} from "./stores/InboundGroupSessionStore"; import {OutboundGroupSessionStore} from "./stores/OutboundGroupSessionStore"; @@ -145,6 +146,10 @@ export class Transaction { return this._store(StoreNames.deviceIdentities, idbStore => new DeviceIdentityStore(idbStore)); } + get crossSigningKeys(): CrossSigningKeyStore { + return this._store(StoreNames.crossSigningKeys, idbStore => new CrossSigningKeyStore(idbStore)); + } + get olmSessions(): OlmSessionStore { return this._store(StoreNames.olmSessions, idbStore => new OlmSessionStore(idbStore)); } diff --git a/src/matrix/storage/idb/schema.ts b/src/matrix/storage/idb/schema.ts index d88f535e..3d1e714f 100644 --- a/src/matrix/storage/idb/schema.ts +++ b/src/matrix/storage/idb/schema.ts @@ -34,7 +34,8 @@ export const schema: MigrationFunc[] = [ clearAllStores, addInboundSessionBackupIndex, migrateBackupStatus, - createCallStore + createCallStore, + createCrossSigningKeyStore ]; // TODO: how to deal with git merge conflicts of this array? @@ -275,3 +276,8 @@ async function migrateBackupStatus(db: IDBDatabase, txn: IDBTransaction, localSt function createCallStore(db: IDBDatabase) : void { db.createObjectStore("calls", {keyPath: "key"}); } + +//v18 create calls store +function createCrossSigningKeyStore(db: IDBDatabase) : void { + db.createObjectStore("crossSigningKeys", {keyPath: "key"}); +} diff --git a/src/matrix/storage/idb/stores/CrossSigningKeyStore.ts b/src/matrix/storage/idb/stores/CrossSigningKeyStore.ts new file mode 100644 index 00000000..a2fa9ecb --- /dev/null +++ b/src/matrix/storage/idb/stores/CrossSigningKeyStore.ts @@ -0,0 +1,70 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +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. +*/ + +import {MAX_UNICODE, MIN_UNICODE} from "./common"; +import {Store} from "../Store"; + +// we store cross-signing keys in the format we get them from the server +// as that is what the signature is calculated on, so to verify, we need +// it in this format anyway. +export type CrossSigningKey = { + readonly user_id: string; + readonly usage: ReadonlyArray; + readonly keys: {[keyId: string]: string}; + readonly signatures: {[userId: string]: {[keyId: string]: string}} +} + +type CrossSigningKeyEntry = CrossSigningKey & { + key: string; // key in storage, not a crypto key +} + +function encodeKey(userId: string, usage: string): string { + return `${userId}|${usage}`; +} + +function decodeKey(key: string): { userId: string, usage: string } { + const [userId, usage] = key.split("|"); + return {userId, usage}; +} + +export class CrossSigningKeyStore { + private _store: Store; + + constructor(store: Store) { + this._store = store; + } + + get(userId: string, deviceId: string): Promise { + return this._store.get(encodeKey(userId, deviceId)); + } + + set(crossSigningKey: CrossSigningKey): void { + const deviceIdentityEntry = crossSigningKey as CrossSigningKeyEntry; + deviceIdentityEntry.key = encodeKey(crossSigningKey["user_id"], crossSigningKey.usage[0]); + this._store.put(deviceIdentityEntry); + } + + remove(userId: string, usage: string): void { + this._store.delete(encodeKey(userId, usage)); + } + + removeAllForUser(userId: string): void { + // exclude both keys as they are theoretical min and max, + // but we should't have a match for just the room id, or room id with max + const range = this._store.IDBKeyRange.bound(encodeKey(userId, MIN_UNICODE), encodeKey(userId, MAX_UNICODE), true, true); + this._store.delete(range); + } +} diff --git a/src/matrix/verification/CrossSigning.ts b/src/matrix/verification/CrossSigning.ts index db480dd0..d3b6bc90 100644 --- a/src/matrix/verification/CrossSigning.ts +++ b/src/matrix/verification/CrossSigning.ts @@ -27,6 +27,12 @@ import type {ISignatures} from "./common"; type Olm = typeof OlmNamespace; +export enum KeyUsage { + Master = "master", + SelfSigning = "self_signing", + UserSigning = "user_signing" +}; + export class CrossSigning { private readonly storage: Storage; private readonly secretStorage: SecretStorage; @@ -72,9 +78,9 @@ export class CrossSigning { } finally { signing.free(); } - const publishedKeys = await this.deviceTracker.getCrossSigningKeysForUser(this.ownUserId, this.hsApi, log); - log.set({publishedMasterKey: publishedKeys.masterKey, derivedPublicKey}); - this._isMasterKeyTrusted = publishedKeys.masterKey === derivedPublicKey; + const masterKey = await this.deviceTracker.getCrossSigningKeyForUser(this.ownUserId, KeyUsage.Master, this.hsApi, log); + log.set({publishedMasterKey: masterKey, derivedPublicKey}); + this._isMasterKeyTrusted = masterKey === derivedPublicKey; log.set("isMasterKeyTrusted", this.isMasterKeyTrusted); }); } @@ -86,7 +92,7 @@ export class CrossSigning { return; } const deviceKey = this.e2eeAccount.getDeviceKeysToSignWithCrossSigning(); - const signedDeviceKey = await this.signDevice(deviceKey); + const signedDeviceKey = await this.signDeviceData(deviceKey); const payload = { [signedDeviceKey["user_id"]]: { [signedDeviceKey["device_id"]]: signedDeviceKey @@ -97,7 +103,15 @@ export class CrossSigning { }); } - private async signDevice(data: T): Promise { + signDevice(deviceId: string) { + // need to get the device key for the device + } + + signUser(userId: string) { + // need to be able to get the msk for the user + } + + private async signDeviceData(data: T): Promise { const txn = await this.storage.readTxn([this.storage.storeNames.accountData]); const seedStr = await this.secretStorage.readSecret(`m.cross_signing.self_signing`, txn); const seed = new Uint8Array(this.platform.encoding.base64.decode(seedStr)); @@ -110,3 +124,30 @@ export class CrossSigning { } } +export function getKeyUsage(keyInfo): KeyUsage | undefined { + if (!Array.isArray(keyInfo.usage) || keyInfo.usage.length !== 1) { + return undefined; + } + const usage = keyInfo.usage[0]; + if (usage !== KeyUsage.Master && usage !== KeyUsage.SelfSigning && usage !== KeyUsage.UserSigning) { + return undefined; + } + return usage; +} + +const algorithm = "ed25519"; +const prefix = `${algorithm}:`; + +export function getKeyEd25519Key(keyInfo): string | undefined { + const ed25519KeyIds = Object.keys(keyInfo.keys).filter(keyId => keyId.startsWith(prefix)); + if (ed25519KeyIds.length !== 1) { + return undefined; + } + const keyId = ed25519KeyIds[0]; + const publicKey = keyInfo.keys[keyId]; + return publicKey; +} + +export function getKeyUserId(keyInfo): string | undefined { + return keyInfo["user_id"]; +}