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.
This commit is contained in:
Bruno Windels 2023-02-24 17:45:56 +01:00
parent 8c74e54f9d
commit 151090527b
7 changed files with 221 additions and 80 deletions

View File

@ -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,65 +329,61 @@ 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;
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);
}
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 keys;
}
_validateCrossSigningKey(userId, keyInfo, usage, parentKey, log) {
if (getKeyUserId(keyInfo) !== userId) {
log.log({l: "user_id mismatch", userId: keyInfo["user_id"]});
return false;
}
const key = keyInfo.keys[keyIds[0]];
keys.set(userId, key);
return keys;
}, keys);
return keys;
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;
}
/**
@ -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([

View File

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

View File

@ -33,7 +33,8 @@ export enum StoreNames {
groupSessionDecryptions = "groupSessionDecryptions",
operations = "operations",
accountData = "accountData",
calls = "calls"
calls = "calls",
crossSigningKeys = "crossSigningKeys"
}
export const STORE_NAMES: Readonly<StoreNames[]> = Object.values(StoreNames);

View File

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

View File

@ -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"});
}

View File

@ -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<string>;
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<CrossSigningKeyEntry>;
constructor(store: Store<CrossSigningKeyEntry>) {
this._store = store;
}
get(userId: string, deviceId: string): Promise<CrossSigningKey | undefined> {
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);
}
}

View File

@ -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<T extends object>(data: T): Promise<T & { signatures: ISignatures }> {
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<T extends object>(data: T): Promise<T & { signatures: ISignatures }> {
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"];
}