mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:40:44 +00:00
fix: trust fresh Matrix owner identity
Trust freshly reset Matrix cross-signing identities only after this device created them, so owner-trusted verification completes locally. Also accept staged recovery keys when they actually establish verified identity trust and usable backup material, even if secret-storage validity does not echo the key id.
This commit is contained in:
@@ -1639,6 +1639,58 @@ describe("MatrixClient crypto bootstrapping", () => {
|
||||
expect(checkKeyBackupAndEnable).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("accepts a staged recovery key when it establishes identity trust and backup usability", async () => {
|
||||
const encoded = encodeRecoveryKey(new Uint8Array(Array.from({ length: 32 }, (_, i) => i + 1)));
|
||||
|
||||
matrixJsClient.getUserId = vi.fn(() => "@bot:example.org");
|
||||
matrixJsClient.getDeviceId = vi.fn(() => "DEVICE123");
|
||||
matrixJsClient.getCrypto = vi.fn(() => ({
|
||||
on: vi.fn(),
|
||||
bootstrapCrossSigning: vi.fn(async () => {}),
|
||||
bootstrapSecretStorage: vi.fn(consumeMatrixSecretStorageKey),
|
||||
requestOwnUserVerification: vi.fn(async () => null),
|
||||
getSecretStorageStatus: vi.fn(async () => ({
|
||||
ready: true,
|
||||
defaultKeyId: "SSSSKEY",
|
||||
secretStorageKeyValidityMap: {},
|
||||
})),
|
||||
getDeviceVerificationStatus: vi.fn(async () => ({
|
||||
isVerified: () => true,
|
||||
localVerified: true,
|
||||
crossSigningVerified: true,
|
||||
signedByOwner: true,
|
||||
})),
|
||||
checkKeyBackupAndEnable: vi.fn(async () => {}),
|
||||
getActiveSessionBackupVersion: vi.fn(async () => "11"),
|
||||
getSessionBackupPrivateKey: vi.fn(async () => new Uint8Array([1])),
|
||||
getKeyBackupInfo: vi.fn(async () => ({
|
||||
algorithm: "m.megolm_backup.v1.curve25519-aes-sha2",
|
||||
auth_data: {},
|
||||
version: "11",
|
||||
})),
|
||||
isKeyBackupTrusted: vi.fn(async () => ({
|
||||
trusted: true,
|
||||
matchesDecryptionKey: true,
|
||||
})),
|
||||
}));
|
||||
|
||||
const recoveryDir = fs.mkdtempSync(path.join(os.tmpdir(), "matrix-sdk-verify-used-key-"));
|
||||
const recoveryKeyPath = path.join(recoveryDir, "recovery-key.json");
|
||||
const client = new MatrixClient("https://matrix.example.org", "token", {
|
||||
encryption: true,
|
||||
recoveryKeyPath,
|
||||
});
|
||||
|
||||
const result = await client.verifyWithRecoveryKey(encoded as string);
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.recoveryKeyAccepted).toBe(true);
|
||||
expect(result.backupUsable).toBe(true);
|
||||
expect(result.deviceOwnerVerified).toBe(true);
|
||||
expect(result.recoveryKeyStored).toBe(true);
|
||||
expect(fs.existsSync(recoveryKeyPath)).toBe(true);
|
||||
});
|
||||
|
||||
it("fails recovery-key verification when the device lacks full cross-signing identity trust", async () => {
|
||||
const encoded = encodeRecoveryKey(new Uint8Array(Array.from({ length: 32 }, (_, i) => i + 1)));
|
||||
|
||||
|
||||
@@ -1237,11 +1237,12 @@ export class MatrixClient {
|
||||
typeof crypto.getSecretStorageStatus === "function"
|
||||
? await crypto.getSecretStorageStatus().catch(() => null)
|
||||
: null;
|
||||
const stagedRecoveryKeyValidated = Boolean(
|
||||
const stagedRecoveryKeyConfirmedBySecretStorage =
|
||||
Boolean(stagedKeyId) &&
|
||||
secretStorageStatus?.secretStorageKeyValidityMap?.[stagedKeyId ?? ""] === true;
|
||||
const stagedRecoveryKeyValidated =
|
||||
stagedRecoveryKeyUsed &&
|
||||
stagedKeyId &&
|
||||
secretStorageStatus?.secretStorageKeyValidityMap?.[stagedKeyId] === true,
|
||||
);
|
||||
(stagedRecoveryKeyConfirmedBySecretStorage || (status.verified && backupUsable));
|
||||
const recoveryKeyAccepted = stagedRecoveryKeyValidated && (status.verified || backupUsable);
|
||||
if (!status.verified) {
|
||||
if (backupUsable && stagedRecoveryKeyValidated) {
|
||||
|
||||
@@ -461,6 +461,47 @@ describe("MatrixCryptoBootstrapper", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("trusts the fresh own identity after a forced cross-signing reset", async () => {
|
||||
const verifyOwnIdentity = vi.fn(async () => ({}));
|
||||
const freeOwnIdentity = vi.fn();
|
||||
const { crypto, bootstrapper } = createForcedResetHarness(vi.fn(async () => {}));
|
||||
crypto.getOwnIdentity = vi.fn(async () => ({
|
||||
free: freeOwnIdentity,
|
||||
isVerified: () => false,
|
||||
verify: verifyOwnIdentity,
|
||||
}));
|
||||
|
||||
await bootstrapper.bootstrap(crypto, {
|
||||
strict: true,
|
||||
forceResetCrossSigning: true,
|
||||
});
|
||||
|
||||
expect(verifyOwnIdentity).toHaveBeenCalledTimes(1);
|
||||
expect(freeOwnIdentity).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not trust an existing unpublished identity without a reset", async () => {
|
||||
const verifyOwnIdentity = vi.fn(async () => ({}));
|
||||
const { crypto, bootstrapper } = createBootstrapperHarness({
|
||||
bootstrapCrossSigning: vi.fn(async () => {}),
|
||||
getDeviceVerificationStatus: vi.fn(async () => createVerifiedDeviceStatus()),
|
||||
getOwnIdentity: vi.fn(async () => ({
|
||||
isVerified: () => false,
|
||||
verify: verifyOwnIdentity,
|
||||
})),
|
||||
isCrossSigningReady: vi.fn(async () => false),
|
||||
userHasCrossSigningKeys: vi.fn(async () => false),
|
||||
});
|
||||
|
||||
const result = await bootstrapper.bootstrap(crypto, {
|
||||
allowAutomaticCrossSigningReset: false,
|
||||
strict: false,
|
||||
});
|
||||
|
||||
expect(result.crossSigningPublished).toBe(false);
|
||||
expect(verifyOwnIdentity).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("fails in strict mode when cross-signing keys are still unpublished", async () => {
|
||||
const deps = createBootstrapperDeps();
|
||||
const crypto = createCryptoApi({
|
||||
|
||||
@@ -189,6 +189,7 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
|
||||
};
|
||||
try {
|
||||
await resetCrossSigning();
|
||||
await this.trustFreshOwnIdentity(crypto);
|
||||
} catch (err) {
|
||||
const shouldRepairSecretStorage =
|
||||
options.allowSecretStorageRecreateWithoutRecoveryKey &&
|
||||
@@ -204,6 +205,7 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
|
||||
forceNewSecretStorage: true,
|
||||
});
|
||||
await resetCrossSigning();
|
||||
await this.trustFreshOwnIdentity(crypto);
|
||||
} catch (repairErr) {
|
||||
LogService.warn("MatrixClientLite", "Forced cross-signing reset failed:", repairErr);
|
||||
if (options.strict) {
|
||||
@@ -289,6 +291,7 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
|
||||
setupNewCrossSigning: true,
|
||||
authUploadDeviceSigningKeys,
|
||||
});
|
||||
await this.trustFreshOwnIdentity(crypto);
|
||||
} catch (err) {
|
||||
LogService.warn("MatrixClientLite", "Fallback cross-signing bootstrap failed:", err);
|
||||
if (options.strict) {
|
||||
@@ -300,6 +303,25 @@ export class MatrixCryptoBootstrapper<TRawEvent extends MatrixRawEvent> {
|
||||
return await finalize();
|
||||
}
|
||||
|
||||
private async trustFreshOwnIdentity(crypto: MatrixCryptoBootstrapApi): Promise<void> {
|
||||
const ownIdentity =
|
||||
typeof crypto.getOwnIdentity === "function"
|
||||
? await crypto.getOwnIdentity().catch(() => undefined)
|
||||
: undefined;
|
||||
if (!ownIdentity) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof ownIdentity.isVerified === "function" && ownIdentity.isVerified()) {
|
||||
return;
|
||||
}
|
||||
await ownIdentity.verify?.();
|
||||
} finally {
|
||||
ownIdentity.free?.();
|
||||
}
|
||||
}
|
||||
|
||||
private async bootstrapSecretStorage(
|
||||
crypto: MatrixCryptoBootstrapApi,
|
||||
options: {
|
||||
|
||||
Reference in New Issue
Block a user