You've already forked matrix-js-sdk
mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-11-26 17:03:12 +03:00
offer to upgrade device verifications to cross-signing
This commit is contained in:
@@ -46,6 +46,30 @@ describe("Cross Signing", function() {
|
||||
await global.Olm.init();
|
||||
});
|
||||
|
||||
it("should sign the master key with the device key", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
);
|
||||
alice.uploadDeviceSigningKeys = expect.createSpy()
|
||||
.andCall(async (auth, keys) => {
|
||||
await olmlib.verifySignature(
|
||||
alice._crypto._olmDevice, keys.master_key, "@alice:example.com",
|
||||
"Osborne2", alice._crypto._olmDevice.deviceEd25519Key,
|
||||
);
|
||||
});
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
let privateKeys;
|
||||
alice.on("cross-signing.savePrivateKeys", function(e) {
|
||||
privateKeys = e;
|
||||
});
|
||||
alice.on("cross-signing.getKey", function(e) {
|
||||
e.done(privateKeys[e.type]);
|
||||
});
|
||||
await alice.resetCrossSigningKeys();
|
||||
expect(alice.uploadDeviceSigningKeys).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should upload a signature when a user is verified", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"},
|
||||
@@ -116,6 +140,23 @@ describe("Cross Signing", function() {
|
||||
e.done(selfSigningKey);
|
||||
});
|
||||
|
||||
const uploadSigsPromise = new Promise((resolve, reject) => {
|
||||
alice.uploadKeySignatures = expect.createSpy().andCall(async (content) => {
|
||||
await olmlib.verifySignature(
|
||||
alice._crypto._olmDevice,
|
||||
content["@alice:example.com"]["nqOvzeuGWT/sRx3h7+MHoInYj3Uk2LD/unI9kDYcHwk"],
|
||||
"@alice:example.com",
|
||||
"Osborne2", alice._crypto._olmDevice.deviceEd25519Key,
|
||||
);
|
||||
olmlib.pkVerify(
|
||||
content["@alice:example.com"]["Osborne2"],
|
||||
"EmkqvokUn8p+vQAGZitOk4PWjp7Ukp3txV2TbMPEiBQ",
|
||||
"@alice:example.com",
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
|
||||
.Osborne2;
|
||||
const aliceDevice = {
|
||||
@@ -203,11 +244,6 @@ describe("Cross Signing", function() {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
method: "POST",
|
||||
path: "/keys/signatures/upload",
|
||||
data: {},
|
||||
},
|
||||
];
|
||||
setHttpResponses(alice, responses, true, true);
|
||||
|
||||
@@ -215,6 +251,7 @@ describe("Cross Signing", function() {
|
||||
|
||||
// once ssk is confirmed, device key should be trusted
|
||||
await keyChangePromise;
|
||||
await uploadSigsPromise;
|
||||
expect(alice.checkUserTrust("@alice:example.com")).toBe(6);
|
||||
expect(alice.checkDeviceTrust("@alice:example.com", "Osborne2")).toBe(7);
|
||||
});
|
||||
@@ -654,4 +691,83 @@ describe("Cross Signing", function() {
|
||||
expect(alice.checkUserTrust("@bob:example.com")).toBe(4);
|
||||
expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).toBe(4);
|
||||
});
|
||||
|
||||
it("should offer to upgrade device verifications to cross-signing", async function() {
|
||||
const alice = await makeTestClient(
|
||||
{userId: "@alice:example.com", deviceId: "Osborne2"}
|
||||
);
|
||||
const bob = await makeTestClient(
|
||||
{userId: "@bob:example.com", deviceId: "Dynabook"},
|
||||
);
|
||||
const privateKeys = {};
|
||||
|
||||
bob.uploadDeviceSigningKeys = async () => {};
|
||||
bob.uploadKeySignatures = async () => {};
|
||||
// set Bob's cross-signing key
|
||||
bob.on("cross-signing.savePrivateKeys", function(e) {
|
||||
privateKeys.bob = e;
|
||||
});
|
||||
bob.on("cross-signing.getKey", function(e) {
|
||||
e.done(privateKeys.bob[e.type]);
|
||||
});
|
||||
await bob.resetCrossSigningKeys();
|
||||
alice._crypto._deviceList.storeDevicesForUser("@bob:example.com", {
|
||||
Dynabook: {
|
||||
algorithms: ["m.olm.curve25519-aes-sha256", "m.megolm.v1.aes-sha"],
|
||||
keys: {
|
||||
"curve25519:Dynabook": bob._crypto._olmDevice.deviceCurve25519Key,
|
||||
"ed25519:Dynabook": bob._crypto._olmDevice.deviceEd25519Key,
|
||||
},
|
||||
verified: 1,
|
||||
known: true,
|
||||
}
|
||||
});
|
||||
alice._crypto._deviceList.storeCrossSigningForUser(
|
||||
"@bob:example.com",
|
||||
bob._crypto._crossSigningInfo.toStorage(),
|
||||
);
|
||||
|
||||
alice.uploadDeviceSigningKeys = async () => {};
|
||||
alice.uploadKeySignatures = async () => {};
|
||||
// set Alice's cross-signing key
|
||||
alice.on("cross-signing.savePrivateKeys", function(e) {
|
||||
privateKeys.alice = e;
|
||||
});
|
||||
alice.on("cross-signing.getKey", function(e) {
|
||||
e.done(privateKeys.alice[e.type]);
|
||||
});
|
||||
// when alice sets up cross-signing, she should notice that bob's
|
||||
// cross-signing key is signed by his Dynabook, which alice has
|
||||
// verified, and ask if the device verification should be upgraded to a
|
||||
// cross-signing verification
|
||||
let upgradePromise = new Promise((resolve, reject) => {
|
||||
alice.once("cross-signing.upgradeDeviceVerifications", async (e) => {
|
||||
expect(e.users["@bob:example.com"]).toExist();
|
||||
await e.accept(["@bob:example.com"]);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
await alice.resetCrossSigningKeys();
|
||||
await upgradePromise;
|
||||
|
||||
expect(alice.checkUserTrust("@bob:example.com")).toBe(6);
|
||||
|
||||
// "forget" that Bob is trusted
|
||||
delete alice._crypto._deviceList._crossSigningInfo["@bob:example.com"]
|
||||
.keys.master.signatures["@alice:example.com"];
|
||||
|
||||
expect(alice.checkUserTrust("@bob:example.com")).toBe(2);
|
||||
|
||||
upgradePromise = new Promise((resolve, reject) => {
|
||||
alice.once("cross-signing.upgradeDeviceVerifications", async (e) => {
|
||||
expect(e.users["@bob:example.com"]).toExist();
|
||||
await e.accept(["@bob:example.com"]);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
alice._crypto._deviceList.emit("userCrossSigningUpdated", "@bob:example.com");
|
||||
await upgradePromise;
|
||||
|
||||
expect(alice.checkUserTrust("@bob:example.com")).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -561,6 +561,7 @@ MatrixClient.prototype.initCrypto = async function() {
|
||||
"crypto.devicesUpdated",
|
||||
"cross-signing.savePrivateKeys",
|
||||
"cross-signing.getKey",
|
||||
"cross-signing.upgradeDeviceVerifications",
|
||||
]);
|
||||
|
||||
logger.log("Crypto: initialising crypto object...");
|
||||
@@ -4751,6 +4752,22 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
|
||||
* provided if a previously provided key was invalid.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when a device verification can be upgraded to a cross-signing
|
||||
* verification. The handler should call the `accept` callback in order to
|
||||
* perform the upgrade.
|
||||
* @event module:client~MatrixClient#"cross-signing.upgradeDeviceVerifications"
|
||||
* @param {object} data
|
||||
* @param {object} data.users The users whose device verifications can be
|
||||
* upgraded to cross-signing verifications. This will be a map of user IDs
|
||||
* to objects with the properties `devices` (array of the user's devices
|
||||
* that verified their cross-signing key), and `crossSigningInfo` (the
|
||||
* user's cross-signing information)
|
||||
* @param {object} data.accept a function to call to upgrade the device
|
||||
* verifications. It should be called with an array of the user IDs who
|
||||
* should be cross-signed.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when a secret has been requested by another client. Clients should
|
||||
* ensure that the requesting device is allowed to have the secret. For
|
||||
|
||||
@@ -306,6 +306,7 @@ Crypto.prototype.init = async function() {
|
||||
*/
|
||||
Crypto.prototype.resetCrossSigningKeys = async function(authDict, level) {
|
||||
await this._crossSigningInfo.resetKeys(level);
|
||||
await this._signObject(this._crossSigningInfo.keys.master);
|
||||
await this._cryptoStore.doTxn(
|
||||
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
|
||||
(txn) => {
|
||||
@@ -329,6 +330,91 @@ Crypto.prototype.resetCrossSigningKeys = async function(authDict, level) {
|
||||
[this._deviceId]: signedDevice,
|
||||
},
|
||||
});
|
||||
|
||||
// check all users for signatures
|
||||
// FIXME: do this in batches
|
||||
const users = {};
|
||||
for (const [userId, crossSigningInfo]
|
||||
of Object.entries(this._deviceList._crossSigningInfo)) {
|
||||
const upgradeInfo = await this._checkForDeviceVerificationUpgrade(
|
||||
userId, CrossSigningInfo.fromStorage(crossSigningInfo, userId),
|
||||
);
|
||||
if (upgradeInfo) {
|
||||
users[userId] = upgradeInfo;
|
||||
}
|
||||
}
|
||||
this.emit("cross-signing.upgradeDeviceVerifications", {
|
||||
users,
|
||||
accept: async (upgradeUsers) => {
|
||||
for (const userId of upgradeUsers) {
|
||||
if (userId in users) {
|
||||
await this._baseApis.setDeviceVerified(
|
||||
userId, users[userId].crossSigningInfo.getId(),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if a user's cross-signing key is a candidate for upgrading from device
|
||||
* verification.
|
||||
*
|
||||
* @param {string} userId the user whose cross-signing information is to be checked
|
||||
* @param {object} crossSigningInfo the cross-signing information to check
|
||||
*/
|
||||
Crypto.prototype._checkForDeviceVerificationUpgrade = async function(
|
||||
userId, crossSigningInfo,
|
||||
) {
|
||||
// only upgrade if this is the first cross-signing key that we've seen for
|
||||
// them, and if their cross-signing key isn't already verified
|
||||
if (crossSigningInfo.fu
|
||||
&& !(this._crossSigningInfo.checkUserTrust(crossSigningInfo) & 2)) {
|
||||
const devices = this._deviceList.getRawStoredDevicesForUser(userId);
|
||||
const deviceIds = await this._checkForValidDeviceSignature(
|
||||
userId, crossSigningInfo.keys.master, devices,
|
||||
);
|
||||
if (deviceIds.length) {
|
||||
return {
|
||||
devices: deviceIds.map(
|
||||
deviceId => DeviceInfo.fromStorage(devices[deviceId], deviceId),
|
||||
),
|
||||
crossSigningInfo,
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the cross-signing key is signed by a verified device.
|
||||
*
|
||||
* @param {string} userId the user ID whose key is being checked
|
||||
* @param {object} key the key that is being checked
|
||||
* @param {object} devices the user's devices. Should be a map from device ID
|
||||
* to device info
|
||||
*/
|
||||
Crypto.prototype._checkForValidDeviceSignature = async function(userId, key, devices) {
|
||||
const deviceIds = [];
|
||||
if (devices && key.signatures && key.signatures[userId]) {
|
||||
for (const signame of Object.keys(key.signatures[userId])) {
|
||||
const [, deviceId] = signame.split(':', 2);
|
||||
if (deviceId in devices
|
||||
&& devices[deviceId].verified === DeviceVerification.VERIFIED) {
|
||||
try {
|
||||
await olmlib.verifySignature(
|
||||
this._olmDevice,
|
||||
key,
|
||||
userId,
|
||||
deviceId,
|
||||
devices[deviceId].keys[signame],
|
||||
);
|
||||
deviceIds.push(deviceId);
|
||||
} catch (e) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
return deviceIds;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -410,7 +496,9 @@ Crypto.prototype.checkDeviceTrust = function(userId, deviceId) {
|
||||
*/
|
||||
Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) {
|
||||
if (userId === this._userId) {
|
||||
this.checkOwnCrossSigningTrust();
|
||||
await this._checkOwnCrossSigningTrust();
|
||||
} else {
|
||||
await this._checkDeviceVerifications(userId);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -418,7 +506,7 @@ Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) {
|
||||
* Check the copy of our cross-signing key that we have in the device list and
|
||||
* see if we can get the private key. If so, mark it as trusted.
|
||||
*/
|
||||
Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
||||
Crypto.prototype._checkOwnCrossSigningTrust = async function() {
|
||||
const userId = this._userId;
|
||||
|
||||
// If we see an update to our own master key, check it against the master
|
||||
@@ -489,6 +577,8 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
||||
},
|
||||
);
|
||||
|
||||
const keySignatures = {};
|
||||
|
||||
if (oldSelfSigningId !== newCrossSigning.getId("self_signing")) {
|
||||
logger.info("Got new self-signing key", newCrossSigning.getId("self_signing"));
|
||||
|
||||
@@ -496,26 +586,60 @@ Crypto.prototype.checkOwnCrossSigningTrust = async function() {
|
||||
const signedDevice = await this._crossSigningInfo.signDevice(
|
||||
this._userId, device,
|
||||
);
|
||||
await this._baseApis.uploadKeySignatures({
|
||||
[this._userId]: {
|
||||
[this._deviceId]: signedDevice,
|
||||
},
|
||||
});
|
||||
keySignatures[this._deviceId] = signedDevice;
|
||||
}
|
||||
if (oldUserSigningId !== newCrossSigning.getId("user_signing")) {
|
||||
logger.info("Got new user-signing key", newCrossSigning.getId("user_signing"));
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
await this._signObject(this._crossSigningInfo.keys.master);
|
||||
keySignatures[this._crossSigningInfo.getId()]
|
||||
= this._crossSigningInfo.keys.master;
|
||||
this._baseApis.emit("cross-signing.keysChanged", {});
|
||||
}
|
||||
|
||||
if (Object.keys(keySignatures).length) {
|
||||
await this._baseApis.uploadKeySignatures({[this._userId]: keySignatures});
|
||||
}
|
||||
|
||||
// Now we may be able to trust our key backup
|
||||
await this.checkKeyBackup();
|
||||
// FIXME: if we previously trusted the backup, should we automatically sign
|
||||
// the backup with the new key (if not already signed)?
|
||||
};
|
||||
|
||||
/**
|
||||
* Check if the master key is signed by a verified device, and if so, prompt
|
||||
* the application to mark it as verified.
|
||||
*
|
||||
* @param {string} userId the user ID whose key should be checked
|
||||
*/
|
||||
Crypto.prototype._checkDeviceVerifications = async function(userId) {
|
||||
if (this._crossSigningInfo.keys.user_signing) {
|
||||
const crossSigningInfo = this._deviceList.getStoredCrossSigningForUser(userId);
|
||||
if (crossSigningInfo) {
|
||||
const upgradeInfo = await this._checkForDeviceVerificationUpgrade(
|
||||
userId, crossSigningInfo,
|
||||
);
|
||||
if (upgradeInfo) {
|
||||
this.emit("cross-signing.upgradeDeviceVerifications", {
|
||||
users: {
|
||||
[userId]: upgradeInfo,
|
||||
},
|
||||
accept: async (users) => {
|
||||
if (users.includes(userId)) {
|
||||
await this._baseApis.setDeviceVerified(
|
||||
userId, crossSigningInfo.getId(),
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Check the server for an active key backup and
|
||||
* if one is present and has a valid signature from
|
||||
|
||||
Reference in New Issue
Block a user