Skip to content

Add support for cross signing the bot's device - #389

Open
uhoreg wants to merge 15 commits into
turt2live:mainfrom
uhoreg:cross-signing
Open

uhoreg wants to merge 15 commits into
turt2live:mainfrom
uhoreg:cross-signing

Conversation

@uhoreg

@uhoreg uhoreg commented Mar 31, 2026

Copy link
Copy Markdown

Adds functions to cross-sign the bot's device using a recovery key.

Depends on matrix-org/matrix-rust-sdk-crypto-nodejs#67, so merging will need to wait for that, plus a release of matrix-rust-sdk-crypto-nodejs, plus bumping package.json.

Cross-signing can be done by doing something like:

if (config.recoveryKey) {
    client.on("crypto.ready", async () => {
        await client.crypto.confirmIdentityWithRecoveryKey(config.recoveryKey);
    });
}

Checklist

  • Tests written for all new code
  • Linter has been satisfied
  • Sign-off given on the changes (see CONTRIBUTING.md)

Signed-off-by: Hubert Chathi hubertc@element.io

@uhoreg

uhoreg commented Mar 31, 2026

Copy link
Copy Markdown
Author

tests incoming, but this PR should be otherwise reviewable

Comment thread src/e2ee/RustEngine.ts Outdated
Co-authored-by: Andrew Ferrazzutti <af_0_af@hotmail.com>
@AndrewFerr AndrewFerr mentioned this pull request Apr 8, 2026
3 tasks done
Comment thread src/e2ee/CryptoClient.ts Outdated
Comment thread src/e2ee/CryptoClient.ts Outdated
Comment thread src/e2ee/CryptoClient.ts Outdated
Comment thread src/e2ee/CryptoClient.ts Outdated
Comment thread src/e2ee/CryptoClient.ts
Comment thread src/e2ee/CryptoClient.ts
LogService.debug("CryptoClient", "Running with device Ed25519 identity:", this.deviceEd25519); // info so all bots know for debugging

this.ready = true;
this.client.emit("crypto.ready");

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like there are currently no listeners for this. Are there plans for there to be? IMO it's not all that useful to emit here, since the preparation of a MatrixClient / Intent already includes an await on crypto to be ready, i.e. there are already checks in place to make sure that crypto is prepared before it gets used.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so that the bot can verify itself once the crypto is set up, i.e.

client.on("crypto.ready", async () => {
    await client.crypto.confirmIdentityWithRecoveryKey(config.recoveryKey);
});

Comment thread src/e2ee/CryptoClient.ts Outdated
Comment thread src/e2ee/CryptoClient.ts Outdated
Comment thread src/e2ee/CryptoClient.ts Outdated
uhoreg and others added 2 commits April 13, 2026 16:50
@AndrewFerr

Copy link
Copy Markdown
Contributor

To compliment this PR, I'm preparing a separate PR that adds a helper class for using the Secret Storage API, which will resemble the JS-SDK's ServerSideSecretStorage. It may then be used by CryptoClient as an alternative to calling getAccountData directly for retrieving cross-signing secrets.

@uhoreg

uhoreg commented Apr 19, 2026

Copy link
Copy Markdown
Author

tests incoming, but this PR should be otherwise reviewable

I've added a test. Though CI of course will fail until we do a release of crypto-nodejs.

@AndrewFerr do you have any more changes planned for matrix-rust-sdk-crypto-nodejs? If not, I'll try to make a new release soon.

@AndrewFerr

Copy link
Copy Markdown
Contributor

@AndrewFerr do you have any more changes planned for matrix-rust-sdk-crypto-nodejs? If not, I'll try to make a new release soon.

No, I'm done. Thanks for asking! Go ahead with that release.

Comment thread src/e2ee/CryptoClient.ts Outdated
Comment on lines +308 to +309
const ownDevice = await machine.getDevice(machine.userId, machine.deviceId);
if (ownDevice.isCrossSigningTrusted()) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

machine.getDevice can apparently return null, though I'd expect that to not happen after crypto is ready. Nevertheless, consider guarding against it here.

ownDevice is also never used elsewhere, so consider inlining it into the if condition.

Suggested change
const ownDevice = await machine.getDevice(machine.userId, machine.deviceId);
if (ownDevice.isCrossSigningTrusted()) {
if ((await machine.getDevice(machine.userId, machine.deviceId))?.isCrossSigningTrusted()) {

@uhoreg uhoreg Apr 23, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, if we can get our own device, then cross-signing won't work, so I'm going to throw an error in that case. (Though I wonder why TypeScript didn't complain about ownDevice possibly being null.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though I wonder why TypeScript didn't complain about ownDevice possibly being null.

It's because the TSConfig file doesn't have the strict flag enabled. It can't be turned on yet, but I've been using it temporarily in local development to help test new code.

Comment thread src/e2ee/CryptoClient.ts
Comment thread src/e2ee/CryptoClient.ts Outdated
Comment thread src/e2ee/CryptoClient.ts
Comment on lines +387 to +391
if (bootstrapRequests.uploadKeysReq) {
this.engine.processOutgoingRequests([bootstrapRequests.uploadKeysReq]);
}
await client.doRequest("POST", "/_matrix/client/v3/keys/device_signing/upload", null, JSON.parse(bootstrapRequests.uploadSigningKeysReq));
await this.engine.processOutgoingRequests([bootstrapRequests.uploadSignaturesReq]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the processing of uploadKeysReq does need to be awaited, it could be processed together with the uploadSignaturesReq:

Suggested change
if (bootstrapRequests.uploadKeysReq) {
this.engine.processOutgoingRequests([bootstrapRequests.uploadKeysReq]);
}
await client.doRequest("POST", "/_matrix/client/v3/keys/device_signing/upload", null, JSON.parse(bootstrapRequests.uploadSigningKeysReq));
await this.engine.processOutgoingRequests([bootstrapRequests.uploadSignaturesReq]);
const outgoingRequests = [
bootstrapRequests.uploadSignaturesReq,
...(bootstrapRequests.uploadKeysReq ? [bootstrapRequests.uploadKeysReq] : []),
];
await client.doRequest("POST", "/_matrix/client/v3/keys/device_signing/upload", null, JSON.parse(bootstrapRequests.uploadSigningKeysReq));
await this.engine.processOutgoingRequests(outgoingRequests);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I was wrong -- I hadn't seen the docstrings on CrossSigningBootstrapRequests that explain the order in which the requests should be sent out. The order you have was correct.

The processing of uploadKeysReq should be awaited, though:

Suggested change
if (bootstrapRequests.uploadKeysReq) {
this.engine.processOutgoingRequests([bootstrapRequests.uploadKeysReq]);
}
await client.doRequest("POST", "/_matrix/client/v3/keys/device_signing/upload", null, JSON.parse(bootstrapRequests.uploadSigningKeysReq));
await this.engine.processOutgoingRequests([bootstrapRequests.uploadSignaturesReq]);
if (bootstrapRequests.uploadKeysReq) {
await this.engine.processOutgoingRequests([bootstrapRequests.uploadKeysReq]);
}
await client.doRequest("POST", "/_matrix/client/v3/keys/device_signing/upload", null, JSON.parse(bootstrapRequests.uploadSigningKeysReq));
await this.engine.processOutgoingRequests([bootstrapRequests.uploadSignaturesReq]);

Comment thread src/e2ee/CryptoClient.ts Outdated
Comment on lines +379 to +384
let secretStorageKey: SecretStorageKey;
if (key === undefined) {
secretStorageKey = SecretStorageKey.createRandomKey();
} else {
secretStorageKey = await this.getDefaultSecretStorageKey(key);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

secretStorageKey can be made const and without a typehint by using a ternary operator:

Suggested change
let secretStorageKey: SecretStorageKey;
if (key === undefined) {
secretStorageKey = SecretStorageKey.createRandomKey();
} else {
secretStorageKey = await this.getDefaultSecretStorageKey(key);
}
const secretStorageKey =
key === undefined
? SecretStorageKey.createRandomKey()
: await this.getDefaultSecretStorageKey(key);

Comment on lines +666 to +667
for (let i = 0; i < 5; i++) {
http1.when("PUT", "/user/%40alice%3Aexample.org/account_data/").respond(200, (path, obj) => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would benefit from a comment to explain why this loop needs to run 5 times. AIUI it's because createIdentity hits this endpoint that many times, to upload account data for the default key ID, the metadata of that key (i.e. algorithm & passphrase), and the 3 cross-signing secrets.

@AndrewFerr

Copy link
Copy Markdown
Contributor

matrix-org/matrix-rust-sdk-crypto-nodejs#67 is finally available in v0.5.1.

Note that the tests may fail when run in bulk. If that gives CI trouble, these PRs may help:

@uhoreg

uhoreg commented Apr 27, 2026

Copy link
Copy Markdown
Author

Changes made. I've also updated element-hq#87 to match the current changes in this PR

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants