1
0
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:
Hubert Chathi
2019-07-08 12:26:00 -04:00
parent 761f22b63d
commit 7f8b9de560
3 changed files with 269 additions and 12 deletions

View File

@@ -46,6 +46,30 @@ describe("Cross Signing", function() {
await global.Olm.init(); 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() { it("should upload a signature when a user is verified", async function() {
const alice = await makeTestClient( const alice = await makeTestClient(
{userId: "@alice:example.com", deviceId: "Osborne2"}, {userId: "@alice:example.com", deviceId: "Osborne2"},
@@ -116,6 +140,23 @@ describe("Cross Signing", function() {
e.done(selfSigningKey); 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"] const deviceInfo = alice._crypto._deviceList._devices["@alice:example.com"]
.Osborne2; .Osborne2;
const aliceDevice = { const aliceDevice = {
@@ -203,11 +244,6 @@ describe("Cross Signing", function() {
}, },
}, },
}, },
{
method: "POST",
path: "/keys/signatures/upload",
data: {},
},
]; ];
setHttpResponses(alice, responses, true, true); setHttpResponses(alice, responses, true, true);
@@ -215,6 +251,7 @@ describe("Cross Signing", function() {
// once ssk is confirmed, device key should be trusted // once ssk is confirmed, device key should be trusted
await keyChangePromise; await keyChangePromise;
await uploadSigsPromise;
expect(alice.checkUserTrust("@alice:example.com")).toBe(6); expect(alice.checkUserTrust("@alice:example.com")).toBe(6);
expect(alice.checkDeviceTrust("@alice:example.com", "Osborne2")).toBe(7); 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.checkUserTrust("@bob:example.com")).toBe(4);
expect(alice.checkDeviceTrust("@bob:example.com", "Dynabook")).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);
});
}); });

View File

@@ -561,6 +561,7 @@ MatrixClient.prototype.initCrypto = async function() {
"crypto.devicesUpdated", "crypto.devicesUpdated",
"cross-signing.savePrivateKeys", "cross-signing.savePrivateKeys",
"cross-signing.getKey", "cross-signing.getKey",
"cross-signing.upgradeDeviceVerifications",
]); ]);
logger.log("Crypto: initialising crypto object..."); logger.log("Crypto: initialising crypto object...");
@@ -4751,6 +4752,22 @@ module.exports.CRYPTO_ENABLED = CRYPTO_ENABLED;
* provided if a previously provided key was invalid. * 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 * Fires when a secret has been requested by another client. Clients should
* ensure that the requesting device is allowed to have the secret. For * ensure that the requesting device is allowed to have the secret. For

View File

@@ -306,6 +306,7 @@ Crypto.prototype.init = async function() {
*/ */
Crypto.prototype.resetCrossSigningKeys = async function(authDict, level) { Crypto.prototype.resetCrossSigningKeys = async function(authDict, level) {
await this._crossSigningInfo.resetKeys(level); await this._crossSigningInfo.resetKeys(level);
await this._signObject(this._crossSigningInfo.keys.master);
await this._cryptoStore.doTxn( await this._cryptoStore.doTxn(
'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT], 'readwrite', [IndexedDBCryptoStore.STORE_ACCOUNT],
(txn) => { (txn) => {
@@ -329,6 +330,91 @@ Crypto.prototype.resetCrossSigningKeys = async function(authDict, level) {
[this._deviceId]: signedDevice, [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) { Crypto.prototype._onDeviceListUserCrossSigningUpdated = async function(userId) {
if (userId === this._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 * 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. * 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; const userId = this._userId;
// If we see an update to our own master key, check it against the master // 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")) { if (oldSelfSigningId !== newCrossSigning.getId("self_signing")) {
logger.info("Got new self-signing key", 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( const signedDevice = await this._crossSigningInfo.signDevice(
this._userId, device, this._userId, device,
); );
await this._baseApis.uploadKeySignatures({ keySignatures[this._deviceId] = signedDevice;
[this._userId]: {
[this._deviceId]: signedDevice,
},
});
} }
if (oldUserSigningId !== newCrossSigning.getId("user_signing")) { if (oldUserSigningId !== newCrossSigning.getId("user_signing")) {
logger.info("Got new user-signing key", newCrossSigning.getId("user_signing")); logger.info("Got new user-signing key", newCrossSigning.getId("user_signing"));
} }
if (changed) { if (changed) {
await this._signObject(this._crossSigningInfo.keys.master);
keySignatures[this._crossSigningInfo.getId()]
= this._crossSigningInfo.keys.master;
this._baseApis.emit("cross-signing.keysChanged", {}); 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 // Now we may be able to trust our key backup
await this.checkKeyBackup(); await this.checkKeyBackup();
// FIXME: if we previously trusted the backup, should we automatically sign // FIXME: if we previously trusted the backup, should we automatically sign
// the backup with the new key (if not already signed)? // 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 * Check the server for an active key backup and
* if one is present and has a valid signature from * if one is present and has a valid signature from