mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 14:40:43 +00:00
fix: complete Matrix self-verification trust
This commit is contained in:
@@ -370,8 +370,9 @@ describe("matrix verification actions", () => {
|
||||
expect(crypto.confirmVerificationSas).toHaveBeenCalledWith("verification-1");
|
||||
expect(bootstrapOwnDeviceVerification).toHaveBeenCalledWith({
|
||||
allowAutomaticCrossSigningReset: false,
|
||||
strict: false,
|
||||
});
|
||||
expect(getOwnDeviceVerificationStatus).toHaveBeenCalled();
|
||||
expect(getOwnDeviceVerificationStatus).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not complete self-verification until the OpenClaw device has full Matrix identity trust", async () => {
|
||||
@@ -407,10 +408,16 @@ describe("matrix verification actions", () => {
|
||||
.mockResolvedValueOnce(mockVerifiedOwnerStatus());
|
||||
const bootstrapOwnDeviceVerification = vi.fn(async () => ({
|
||||
success: true,
|
||||
verification: mockVerifiedOwnerStatus(),
|
||||
verification: mockUnverifiedOwnerStatus(),
|
||||
}));
|
||||
const trustOwnIdentityAfterSelfVerification = vi.fn(async () => {});
|
||||
withStartedActionClientMock.mockImplementation(async (_opts, run) => {
|
||||
return await run({ bootstrapOwnDeviceVerification, crypto, getOwnDeviceVerificationStatus });
|
||||
return await run({
|
||||
bootstrapOwnDeviceVerification,
|
||||
crypto,
|
||||
getOwnDeviceVerificationStatus,
|
||||
trustOwnIdentityAfterSelfVerification,
|
||||
});
|
||||
});
|
||||
|
||||
await expect(
|
||||
@@ -424,6 +431,7 @@ describe("matrix verification actions", () => {
|
||||
});
|
||||
|
||||
expect(getOwnDeviceVerificationStatus).toHaveBeenCalledTimes(2);
|
||||
expect(trustOwnIdentityAfterSelfVerification).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("waits for SAS data without restarting an already-started self-verification", async () => {
|
||||
@@ -650,10 +658,14 @@ describe("matrix verification actions", () => {
|
||||
await expect(
|
||||
runMatrixSelfVerification({ confirmSas: vi.fn(async () => true), timeoutMs: 30 }),
|
||||
).rejects.toThrow(
|
||||
"Matrix self-verification completed, but full Matrix identity trust is still incomplete",
|
||||
"Timed out waiting for Matrix self-verification to establish full Matrix identity trust",
|
||||
);
|
||||
|
||||
expect(crypto.cancelVerification).not.toHaveBeenCalled();
|
||||
expect(bootstrapOwnDeviceVerification).toHaveBeenCalledWith({
|
||||
allowAutomaticCrossSigningReset: false,
|
||||
strict: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("cancels the pending self-verification request when acceptance times out", async () => {
|
||||
|
||||
@@ -210,18 +210,17 @@ async function completeMatrixSelfVerification(params: {
|
||||
}): Promise<MatrixSelfVerificationResult> {
|
||||
const bootstrap = await params.client.bootstrapOwnDeviceVerification({
|
||||
allowAutomaticCrossSigningReset: false,
|
||||
strict: false,
|
||||
});
|
||||
if (!bootstrap.verification.verified) {
|
||||
throw new Error(
|
||||
`Matrix self-verification completed, but full Matrix identity trust is still incomplete: ${
|
||||
bootstrap.error ?? formatMatrixOwnerVerificationDiagnostics(bootstrap.verification)
|
||||
}`,
|
||||
);
|
||||
await params.client.trustOwnIdentityAfterSelfVerification?.();
|
||||
}
|
||||
const ownerVerification = await waitForMatrixOwnerVerificationStatus({
|
||||
client: params.client,
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
const ownerVerification = bootstrap.verification.verified
|
||||
? bootstrap.verification
|
||||
: await waitForMatrixOwnerVerificationStatus({
|
||||
client: params.client,
|
||||
timeoutMs: params.timeoutMs,
|
||||
});
|
||||
return {
|
||||
...params.completed,
|
||||
deviceOwnerVerified: ownerVerification.verified,
|
||||
|
||||
@@ -1222,6 +1222,29 @@ describe("MatrixClient crypto bootstrapping", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("trusts the own Matrix identity after completed self-verification", async () => {
|
||||
const verifyOwnIdentity = vi.fn(async () => ({}));
|
||||
const freeOwnIdentity = vi.fn();
|
||||
matrixJsClient.getCrypto = vi.fn(() => ({
|
||||
on: vi.fn(),
|
||||
getOwnIdentity: vi.fn(async () => ({
|
||||
free: freeOwnIdentity,
|
||||
isVerified: () => false,
|
||||
verify: verifyOwnIdentity,
|
||||
})),
|
||||
requestOwnUserVerification: vi.fn(async () => null),
|
||||
}));
|
||||
|
||||
const client = new MatrixClient("https://matrix.example.org", "token", {
|
||||
encryption: true,
|
||||
});
|
||||
|
||||
await client.trustOwnIdentityAfterSelfVerification();
|
||||
|
||||
expect(verifyOwnIdentity).toHaveBeenCalledTimes(1);
|
||||
expect(freeOwnIdentity).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("retries bootstrap with forced reset when initial publish/verification is incomplete", async () => {
|
||||
matrixJsClient.getCrypto = vi.fn(() => ({ on: vi.fn() }));
|
||||
const client = new MatrixClient("https://matrix.example.org", "token", {
|
||||
|
||||
@@ -165,12 +165,13 @@ const MATRIX_AUTOMATIC_REPAIR_BOOTSTRAP_OPTIONS = {
|
||||
function createMatrixExplicitBootstrapOptions(params?: {
|
||||
allowAutomaticCrossSigningReset?: boolean;
|
||||
forceResetCrossSigning?: boolean;
|
||||
strict?: boolean;
|
||||
}): MatrixCryptoBootstrapOptions {
|
||||
return {
|
||||
forceResetCrossSigning: params?.forceResetCrossSigning === true,
|
||||
allowAutomaticCrossSigningReset: params?.allowAutomaticCrossSigningReset !== false,
|
||||
allowSecretStorageRecreateWithoutRecoveryKey: true,
|
||||
strict: true,
|
||||
strict: params?.strict !== false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -367,7 +368,15 @@ export class MatrixClient {
|
||||
return;
|
||||
}
|
||||
|
||||
this.verificationManager ??= new runtime.MatrixVerificationManager();
|
||||
this.verificationManager ??= new runtime.MatrixVerificationManager({
|
||||
trustOwnDeviceAfterSas: async (deviceId: string) => {
|
||||
const crypto = this.client.getCrypto() as MatrixCryptoBootstrapApi | undefined;
|
||||
if (typeof crypto?.crossSignDevice !== "function") {
|
||||
throw new Error("Matrix crypto backend does not support cross-signing devices");
|
||||
}
|
||||
await crypto.crossSignDevice(deviceId);
|
||||
},
|
||||
});
|
||||
this.cryptoBootstrapper ??= new runtime.MatrixCryptoBootstrapper<MatrixRawEvent>({
|
||||
getUserId: () => this.getUserId(),
|
||||
getPassword: () => this.password,
|
||||
@@ -1127,6 +1136,35 @@ export class MatrixClient {
|
||||
};
|
||||
}
|
||||
|
||||
async trustOwnIdentityAfterSelfVerification(): Promise<void> {
|
||||
if (!this.encryptionEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.ensureStartedForCryptoControlPlane();
|
||||
await this.ensureCryptoSupportInitialized();
|
||||
const crypto = this.client.getCrypto() as MatrixCryptoBootstrapApi | undefined;
|
||||
const ownIdentity =
|
||||
crypto && typeof crypto.getOwnIdentity === "function"
|
||||
? await crypto.getOwnIdentity().catch(() => undefined)
|
||||
: undefined;
|
||||
if (!ownIdentity) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (typeof ownIdentity.isVerified === "function" && ownIdentity.isVerified()) {
|
||||
return;
|
||||
}
|
||||
if (typeof ownIdentity.verify !== "function") {
|
||||
throw new Error("Matrix crypto backend does not support trusting own identity");
|
||||
}
|
||||
await ownIdentity.verify();
|
||||
} finally {
|
||||
ownIdentity.free?.();
|
||||
}
|
||||
}
|
||||
|
||||
async verifyWithRecoveryKey(
|
||||
rawRecoveryKey: string,
|
||||
): Promise<MatrixRecoveryKeyVerificationResult> {
|
||||
@@ -1486,6 +1524,7 @@ export class MatrixClient {
|
||||
allowAutomaticCrossSigningReset?: boolean;
|
||||
recoveryKey?: string;
|
||||
forceResetCrossSigning?: boolean;
|
||||
strict?: boolean;
|
||||
}): Promise<MatrixVerificationBootstrapResult> {
|
||||
const pendingVerifications = async (): Promise<number> =>
|
||||
this.crypto ? (await this.crypto.listVerifications()).length : 0;
|
||||
|
||||
@@ -259,6 +259,49 @@ describe("MatrixVerificationManager", () => {
|
||||
expect(mismatch).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("cross-signs the other own device after confirmed self-verification SAS", async () => {
|
||||
const { confirm, verifier } = createSasVerifierFixture({
|
||||
decimal: [111, 222, 333],
|
||||
emoji: [["cat", "cat"]],
|
||||
});
|
||||
const trustOwnDeviceAfterSas = vi.fn(async () => {});
|
||||
const request = new MockVerificationRequest({
|
||||
isSelfVerification: true,
|
||||
otherDeviceId: "OTHERDEVICE",
|
||||
transactionId: "txn-self-sas",
|
||||
verifier,
|
||||
});
|
||||
const manager = new MatrixVerificationManager({ trustOwnDeviceAfterSas });
|
||||
const tracked = manager.trackVerificationRequest(request);
|
||||
|
||||
await manager.startVerification(tracked.id, "sas");
|
||||
await manager.confirmVerificationSas(tracked.id);
|
||||
|
||||
expect(confirm).toHaveBeenCalledTimes(1);
|
||||
expect(trustOwnDeviceAfterSas).toHaveBeenCalledWith("OTHERDEVICE");
|
||||
});
|
||||
|
||||
it("does not cross-sign non-self SAS verifications", async () => {
|
||||
const { verifier } = createSasVerifierFixture({
|
||||
decimal: [111, 222, 333],
|
||||
emoji: [["cat", "cat"]],
|
||||
});
|
||||
const trustOwnDeviceAfterSas = vi.fn(async () => {});
|
||||
const request = new MockVerificationRequest({
|
||||
isSelfVerification: false,
|
||||
otherDeviceId: "OTHERDEVICE",
|
||||
transactionId: "txn-remote-sas",
|
||||
verifier,
|
||||
});
|
||||
const manager = new MatrixVerificationManager({ trustOwnDeviceAfterSas });
|
||||
const tracked = manager.trackVerificationRequest(request);
|
||||
|
||||
await manager.startVerification(tracked.id, "sas");
|
||||
await manager.confirmVerificationSas(tracked.id);
|
||||
|
||||
expect(trustOwnDeviceAfterSas).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("auto-starts an incoming verifier exposed via request change events", async () => {
|
||||
const { verifier, verify } = createSasVerifierFixture({
|
||||
decimal: [6158, 1986, 3513],
|
||||
@@ -438,6 +481,33 @@ describe("MatrixVerificationManager", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("cross-signs the other own device after auto-confirmed self-verification SAS", async () => {
|
||||
vi.useFakeTimers();
|
||||
const { confirm, verifier } = createSasVerifierFixture({
|
||||
decimal: [6158, 1986, 3513],
|
||||
emoji: [["gift", "Gift"]],
|
||||
});
|
||||
const trustOwnDeviceAfterSas = vi.fn(async () => {});
|
||||
const request = new MockVerificationRequest({
|
||||
isSelfVerification: true,
|
||||
otherDeviceId: "OTHERDEVICE",
|
||||
transactionId: "txn-auto-confirm-self",
|
||||
initiatedByMe: false,
|
||||
verifier,
|
||||
});
|
||||
try {
|
||||
const manager = new MatrixVerificationManager({ trustOwnDeviceAfterSas });
|
||||
manager.trackVerificationRequest(request);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(30_100);
|
||||
|
||||
expect(confirm).toHaveBeenCalledTimes(1);
|
||||
expect(trustOwnDeviceAfterSas).toHaveBeenCalledWith("OTHERDEVICE");
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not auto-confirm SAS for verifications initiated by this device", async () => {
|
||||
vi.useFakeTimers();
|
||||
const confirm = vi.fn(async () => {});
|
||||
|
||||
@@ -52,6 +52,7 @@ export type MatrixVerificationSummary = {
|
||||
};
|
||||
|
||||
type MatrixVerificationSummaryListener = (summary: MatrixVerificationSummary) => void;
|
||||
type MatrixVerificationOwnerTrustCallback = (deviceId: string) => Promise<void>;
|
||||
|
||||
export type MatrixShowSasCallbacks = {
|
||||
sas: {
|
||||
@@ -153,6 +154,12 @@ export class MatrixVerificationManager {
|
||||
private readonly trackedVerificationVerifiers = new WeakSet<object>();
|
||||
private readonly summaryListeners = new Set<MatrixVerificationSummaryListener>();
|
||||
|
||||
constructor(
|
||||
private readonly opts: {
|
||||
trustOwnDeviceAfterSas?: MatrixVerificationOwnerTrustCallback;
|
||||
} = {},
|
||||
) {}
|
||||
|
||||
private readRequestValue<T>(
|
||||
request: MatrixVerificationRequestLike,
|
||||
reader: () => T,
|
||||
@@ -493,8 +500,7 @@ export class MatrixVerificationManager {
|
||||
return;
|
||||
}
|
||||
session.sasAutoConfirmStarted = true;
|
||||
void callbacks
|
||||
.confirm()
|
||||
void this.confirmSasForSession(session, callbacks)
|
||||
.then(() => {
|
||||
this.touchVerificationSession(session);
|
||||
})
|
||||
@@ -505,6 +511,14 @@ export class MatrixVerificationManager {
|
||||
}, SAS_AUTO_CONFIRM_DELAY_MS);
|
||||
}
|
||||
|
||||
private async confirmSasForSession(
|
||||
session: MatrixVerificationSession,
|
||||
callbacks: MatrixShowSasCallbacks,
|
||||
): Promise<void> {
|
||||
await callbacks.confirm();
|
||||
await this.trustOwnDeviceAfterConfirmedSas(session);
|
||||
}
|
||||
|
||||
private ensureVerificationStarted(session: MatrixVerificationSession): void {
|
||||
if (!session.activeVerifier || session.verifyStarted) {
|
||||
return;
|
||||
@@ -522,6 +536,21 @@ export class MatrixVerificationManager {
|
||||
});
|
||||
}
|
||||
|
||||
private async trustOwnDeviceAfterConfirmedSas(session: MatrixVerificationSession): Promise<void> {
|
||||
if (!this.readRequestValue(session.request, () => session.request.isSelfVerification, false)) {
|
||||
return;
|
||||
}
|
||||
const deviceId = this.readRequestValue(
|
||||
session.request,
|
||||
() => session.request.otherDeviceId?.trim(),
|
||||
"",
|
||||
);
|
||||
if (!deviceId || !this.opts.trustOwnDeviceAfterSas) {
|
||||
return;
|
||||
}
|
||||
await this.opts.trustOwnDeviceAfterSas(deviceId);
|
||||
}
|
||||
|
||||
onSummaryChanged(listener: MatrixVerificationSummaryListener): () => void {
|
||||
this.summaryListeners.add(listener);
|
||||
return () => {
|
||||
@@ -695,7 +724,7 @@ export class MatrixVerificationManager {
|
||||
this.clearSasAutoConfirmTimer(session);
|
||||
session.sasCallbacks = callbacks;
|
||||
session.sasAutoConfirmStarted = true;
|
||||
await callbacks.confirm();
|
||||
await this.confirmSasForSession(session, callbacks);
|
||||
this.touchVerificationSession(session);
|
||||
return this.buildVerificationSummary(session);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user