Skip to content

Commit

Permalink
Merge pull request #1036 from vector-im/cross-signing/self-sign
Browse files Browse the repository at this point in the history
Allow to sign own device once MSK is trusted
  • Loading branch information
bwindels authored Feb 14, 2023
2 parents 2a6baef + 71d7dcb commit 1113f2f
Show file tree
Hide file tree
Showing 9 changed files with 219 additions and 33 deletions.
12 changes: 12 additions & 0 deletions src/domain/session/settings/KeyBackupViewModel.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,18 @@ export class KeyBackupViewModel extends ViewModel {
return this._session.crossSigning?.isMasterKeyTrusted ?? false;
}

get canSignOwnDevice() {
return !!this._session.crossSigning;
}

async signOwnDevice() {
if (this._session.crossSigning) {
await this.logger.run("KeyBackupViewModel.signOwnDevice", async log => {
await this._session.crossSigning.signOwnDevice(log);
});
}
}

get backupWriteStatus() {
const keyBackup = this._session.keyBackup.get();
if (!keyBackup) {
Expand Down
3 changes: 2 additions & 1 deletion src/logging/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@ export interface ILogItem {
/*** This is sort of low-level, you probably want to use wrap. If you do use it, it should only be called once. */
run<T>(callback: LogCallback<T>): T;
log(labelOrValues: LabelOrValues, logLevel?: LogLevel): ILogItem;
set(key: string | object, value: unknown): ILogItem;
set(key: string, value: unknown): ILogItem;
set(key: object): ILogItem;
runDetached(labelOrValues: LabelOrValues, callback: LogCallback<unknown>, logLevel?: LogLevel, filterCreator?: FilterCreator): ILogItem;
wrapDetached(labelOrValues: LabelOrValues, callback: LogCallback<unknown>, logLevel?: LogLevel, filterCreator?: FilterCreator): void;
refDetached(logItem: ILogItem, logLevel?: LogLevel): void;
Expand Down
3 changes: 2 additions & 1 deletion src/matrix/Session.js
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,8 @@ export class Session {
olm: this._olm,
deviceTracker: this._deviceTracker,
hsApi: this._hsApi,
ownUserId: this.userId
ownUserId: this.userId,
e2eeAccount: this._e2eeAccount
});
await log.wrap("enable cross-signing", log => {
return this._crossSigning.init(log);
Expand Down
12 changes: 11 additions & 1 deletion src/matrix/e2ee/Account.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export class Account {
}
}

_deviceKeysPayload(identityKeys) {
_keysAsSignableObject(identityKeys) {
const obj = {
user_id: this._userId,
device_id: this._deviceId,
Expand All @@ -256,6 +256,16 @@ export class Account {
for (const [algorithm, pubKey] of Object.entries(identityKeys)) {
obj.keys[`${algorithm}:${this._deviceId}`] = pubKey;
}
return obj;
}

getDeviceKeysToSignWithCrossSigning() {
const identityKeys = JSON.parse(this._account.identity_keys());
return this._keysAsSignableObject(identityKeys);
}

_deviceKeysPayload(identityKeys) {
const obj = this._keysAsSignableObject(identityKeys);
this.signObject(obj);
return obj;
}
Expand Down
55 changes: 44 additions & 11 deletions src/matrix/e2ee/DeviceTracker.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ function createUserIdentity(userId, initialRoomId = undefined) {
return {
userId: userId,
roomIds: initialRoomId ? [initialRoomId] : [],
masterKey: undefined,
crossSigningKeys: undefined,
deviceTrackingStatus: TRACKING_STATUS_OUTDATED,
};
}
Expand Down Expand Up @@ -153,23 +153,23 @@ export class DeviceTracker {
}
}

async getMasterKeyForUser(userId, hsApi, log) {
async getCrossSigningKeysForUser(userId, hsApi, log) {
return await log.wrap("DeviceTracker.getMasterKeyForUser", async log => {
let txn = await this._storage.readTxn([
this._storage.storeNames.userIdentities
]);
let userIdentity = await txn.userIdentities.get(userId);
if (userIdentity && userIdentity.deviceTrackingStatus !== TRACKING_STATUS_OUTDATED) {
return userIdentity.masterKey;
}
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?.masterKey;
return userIdentity?.crossSigningKeys;
});
}

Expand Down Expand Up @@ -246,6 +246,7 @@ export class DeviceTracker {
}, {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 txn = await this._storage.readWriteTxn([
this._storage.storeNames.userIdentities,
Expand All @@ -255,7 +256,11 @@ export class DeviceTracker {
try {
const devicesIdentitiesPerUser = await Promise.all(verifiedKeysPerUser.map(async ({userId, verifiedKeys}) => {
const deviceIdentities = verifiedKeys.map(deviceKeysAsDeviceIdentity);
return await this._storeQueriedDevicesForUserId(userId, masterKeys.get(userId), deviceIdentities, txn);
const crossSigningKeys = {
masterKey: masterKeys.get(userId),
selfSigningKey: selfSigningKeys.get(userId),
};
return await this._storeQueriedDevicesForUserId(userId, crossSigningKeys, deviceIdentities, txn);
}));
deviceIdentities = devicesIdentitiesPerUser.reduce((all, devices) => all.concat(devices), []);
log.set("devices", deviceIdentities.length);
Expand All @@ -267,7 +272,7 @@ export class DeviceTracker {
return deviceIdentities;
}

async _storeQueriedDevicesForUserId(userId, masterKey, deviceIdentities, txn) {
async _storeQueriedDevicesForUserId(userId, crossSigningKeys, 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,
Expand Down Expand Up @@ -308,13 +313,13 @@ export class DeviceTracker {
identity = createUserIdentity(userId);
}
identity.deviceTrackingStatus = TRACKING_STATUS_UPTODATE;
identity.masterKey = masterKey;
identity.crossSigningKeys = crossSigningKeys;
txn.userIdentities.set(identity);

return allDeviceIdentities;
}

_filterValidMasterKeys(keyQueryResponse, parentLog) {
_filterValidMasterKeys(keyQueryResponse, log) {
const masterKeys = new Map();
const masterKeysResponse = keyQueryResponse["master_keys"];
if (!masterKeysResponse) {
Expand All @@ -341,6 +346,34 @@ export class DeviceTracker {
return masterKeys;
}

_filterVerifiedCrossSigningKeys(crossSigningKeysResponse, usage, masterKeys, 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);
return keys;
}

/**
* @return {Array<{userId, verifiedKeys: Array<DeviceSection>>}
*/
Expand Down Expand Up @@ -681,14 +714,14 @@ export function tests() {
const txn = await storage.readTxn([storage.storeNames.userIdentities]);
assert.deepEqual(await txn.userIdentities.get("@alice:hs.tld"), {
userId: "@alice:hs.tld",
masterKey: undefined,
crossSigningKeys: undefined,
roomIds: [roomId],
deviceTrackingStatus: TRACKING_STATUS_OUTDATED
});
assert.deepEqual(await txn.userIdentities.get("@bob:hs.tld"), {
userId: "@bob:hs.tld",
roomIds: [roomId],
masterKey: undefined,
crossSigningKeys: undefined,
deviceTrackingStatus: TRACKING_STATUS_OUTDATED
});
assert.equal(await txn.userIdentities.get("@charly:hs.tld"), undefined);
Expand Down
4 changes: 4 additions & 0 deletions src/matrix/net/HomeServerApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,10 @@ export class HomeServerApi {
return this._post(path, {}, payload, options);
}

uploadSignatures(payload: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
return this._post("/keys/signatures/upload", {}, payload, options);
}

queryKeys(queryRequest: Record<string, any>, options?: BaseRequestOptions): IHomeServerRequest {
return this._post("/keys/query", {}, queryRequest, options);
}
Expand Down
78 changes: 61 additions & 17 deletions src/matrix/verification/CrossSigning.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ import type {Platform} from "../../platform/web/Platform";
import type {DeviceTracker} from "../e2ee/DeviceTracker";
import type * as OlmNamespace from "@matrix-org/olm";
import type {HomeServerApi} from "../net/HomeServerApi";
import type {Account} from "../e2ee/Account";
import { ILogItem } from "../../lib";
import {pkSign} from "./common";
import type {ISignatures} from "./common";

type Olm = typeof OlmNamespace;

export class CrossSigning {
Expand All @@ -30,35 +35,74 @@ export class CrossSigning {
private readonly olm: Olm;
private readonly hsApi: HomeServerApi;
private readonly ownUserId: string;
private readonly e2eeAccount: Account;
private _isMasterKeyTrusted: boolean = false;

constructor(options: {storage: Storage, secretStorage: SecretStorage, deviceTracker: DeviceTracker, platform: Platform, olm: Olm, ownUserId: string, hsApi: HomeServerApi}) {
constructor(options: {
storage: Storage,
secretStorage: SecretStorage,
deviceTracker: DeviceTracker,
platform: Platform,
olm: Olm,
ownUserId: string,
hsApi: HomeServerApi,
e2eeAccount: Account
}) {
this.storage = options.storage;
this.secretStorage = options.secretStorage;
this.platform = options.platform;
this.deviceTracker = options.deviceTracker;
this.olm = options.olm;
this.hsApi = options.hsApi;
this.ownUserId = options.ownUserId;
this.e2eeAccount = options.e2eeAccount
}

async init(log: ILogItem) {
log.wrap("CrossSigning.init", async log => {
// TODO: use errorboundary here
const txn = await this.storage.readTxn([this.storage.storeNames.accountData]);

const mskSeed = await this.secretStorage.readSecret("m.cross_signing.master", txn);
const signing = new this.olm.PkSigning();
let derivedPublicKey;
try {
const seed = new Uint8Array(this.platform.encoding.base64.decode(mskSeed));
derivedPublicKey = signing.init_with_seed(seed);
} 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;
log.set("isMasterKeyTrusted", this.isMasterKeyTrusted);
});
}

async signOwnDevice(log: ILogItem) {
log.wrap("CrossSigning.signOwnDevice", async log => {
if (!this._isMasterKeyTrusted) {
log.set("mskNotTrusted", true);
return;
}
const deviceKey = this.e2eeAccount.getDeviceKeysToSignWithCrossSigning();
const signedDeviceKey = await this.signDevice(deviceKey);
const payload = {
[signedDeviceKey["user_id"]]: {
[signedDeviceKey["device_id"]]: signedDeviceKey
}
};
const request = this.hsApi.uploadSignatures(payload, {log});
await request.response();
});
}

async init(log) {
// use errorboundary here
private async signDevice<T extends object>(data: T): Promise<T & { signatures: ISignatures }> {
const txn = await this.storage.readTxn([this.storage.storeNames.accountData]);

const mskSeed = await this.secretStorage.readSecret("m.cross_signing.master", txn);
const signing = new this.olm.PkSigning();
let derivedPublicKey;
try {
const seed = new Uint8Array(this.platform.encoding.base64.decode(mskSeed));
derivedPublicKey = signing.init_with_seed(seed);
} finally {
signing.free();
}
const publishedMasterKey = await this.deviceTracker.getMasterKeyForUser(this.ownUserId, this.hsApi, log);
log.set({publishedMasterKey, derivedPublicKey});
this._isMasterKeyTrusted = publishedMasterKey === derivedPublicKey;
log.set("isMasterKeyTrusted", this.isMasterKeyTrusted);
const seedStr = await this.secretStorage.readSecret(`m.cross_signing.self_signing`, txn);
const seed = new Uint8Array(this.platform.encoding.base64.decode(seedStr));
pkSign(this.olm, data, seed, this.ownUserId, "");
return data as T & { signatures: ISignatures };
}

get isMasterKeyTrusted(): boolean {
Expand Down
72 changes: 72 additions & 0 deletions src/matrix/verification/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
Copyright 2016-2023 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 { PkSigning } from "@matrix-org/olm";
import anotherjson from "another-json";
import type * as OlmNamespace from "@matrix-org/olm";
type Olm = typeof OlmNamespace;

export interface IObject {
unsigned?: object;
signatures?: ISignatures;
}

export interface ISignatures {
[entity: string]: {
[keyId: string]: string;
};
}

export interface ISigned {
signatures?: ISignatures;
}

// from matrix-js-sdk
/**
* Sign a JSON object using public key cryptography
* @param obj - Object to sign. The object will be modified to include
* the new signature
* @param key - the signing object or the private key
* seed
* @param userId - The user ID who owns the signing key
* @param pubKey - The public key (ignored if key is a seed)
* @returns the signature for the object
*/
export function pkSign(olmUtil: Olm, obj: object & IObject, key: Uint8Array | PkSigning, userId: string, pubKey: string): string {
let createdKey = false;
if (key instanceof Uint8Array) {
const keyObj = new olmUtil.PkSigning();
pubKey = keyObj.init_with_seed(key);
key = keyObj;
createdKey = true;
}
const sigs = obj.signatures || {};
delete obj.signatures;
const unsigned = obj.unsigned;
if (obj.unsigned) delete obj.unsigned;
try {
const mysigs = sigs[userId] || {};
sigs[userId] = mysigs;

return (mysigs["ed25519:" + pubKey] = key.sign(anotherjson.stringify(obj)));
} finally {
obj.signatures = sigs;
if (unsigned) obj.unsigned = unsigned;
if (createdKey) {
key.free();
}
}
}
Loading

0 comments on commit 1113f2f

Please sign in to comment.