fix: require Matrix cross-signing publication

This commit is contained in:
Gustavo Madeira Santana
2026-04-23 09:15:04 -04:00
parent ac276e0470
commit ce6b53cf92
2 changed files with 116 additions and 11 deletions

View File

@@ -93,6 +93,16 @@ describe("matrix verification actions", () => {
};
}
function mockCrossSigningPublicationStatus(published = true) {
return {
masterKeyPublished: published,
published,
selfSigningKeyPublished: published,
userId: "@bot:example.org",
userSigningKeyPublished: published,
};
}
it("points encryption guidance at the selected Matrix account", async () => {
loadConfigMock.mockReturnValue({
channels: {
@@ -346,12 +356,21 @@ describe("matrix verification actions", () => {
};
const confirmSas = vi.fn(async () => true);
const getOwnDeviceVerificationStatus = vi.fn(async () => mockVerifiedOwnerStatus());
const getOwnCrossSigningPublicationStatus = vi.fn(async () =>
mockCrossSigningPublicationStatus(),
);
const bootstrapOwnDeviceVerification = vi.fn(async () => ({
crossSigning: mockCrossSigningPublicationStatus(),
success: true,
verification: mockVerifiedOwnerStatus(),
}));
withStartedActionClientMock.mockImplementation(async (_opts, run) => {
return await run({ bootstrapOwnDeviceVerification, crypto, getOwnDeviceVerificationStatus });
return await run({
bootstrapOwnDeviceVerification,
crypto,
getOwnCrossSigningPublicationStatus,
getOwnDeviceVerificationStatus,
});
});
await expect(runMatrixSelfVerification({ confirmSas, timeoutMs: 500 })).resolves.toMatchObject({
@@ -372,6 +391,7 @@ describe("matrix verification actions", () => {
allowAutomaticCrossSigningReset: false,
strict: false,
});
expect(getOwnCrossSigningPublicationStatus).not.toHaveBeenCalled();
expect(getOwnDeviceVerificationStatus).not.toHaveBeenCalled();
});
@@ -406,7 +426,11 @@ describe("matrix verification actions", () => {
.fn()
.mockResolvedValueOnce(mockUnverifiedOwnerStatus())
.mockResolvedValueOnce(mockVerifiedOwnerStatus());
const getOwnCrossSigningPublicationStatus = vi.fn(async () =>
mockCrossSigningPublicationStatus(),
);
const bootstrapOwnDeviceVerification = vi.fn(async () => ({
crossSigning: mockCrossSigningPublicationStatus(),
success: true,
verification: mockUnverifiedOwnerStatus(),
}));
@@ -415,6 +439,7 @@ describe("matrix verification actions", () => {
return await run({
bootstrapOwnDeviceVerification,
crypto,
getOwnCrossSigningPublicationStatus,
getOwnDeviceVerificationStatus,
trustOwnIdentityAfterSelfVerification,
});
@@ -431,9 +456,73 @@ describe("matrix verification actions", () => {
});
expect(getOwnDeviceVerificationStatus).toHaveBeenCalledTimes(2);
expect(getOwnCrossSigningPublicationStatus).toHaveBeenCalledTimes(2);
expect(trustOwnIdentityAfterSelfVerification).toHaveBeenCalledTimes(1);
});
it("does not complete self-verification until cross-signing keys are published", async () => {
const requested = {
completed: false,
hasSas: false,
id: "verification-1",
phaseName: "requested",
transactionId: "tx-self",
};
const sas = {
...requested,
hasSas: true,
phaseName: "started",
sas: {
decimal: [1, 2, 3],
},
};
const completed = {
...sas,
completed: true,
phaseName: "done",
};
const crypto = {
confirmVerificationSas: vi.fn(async () => completed),
listVerifications: vi.fn(async () => [sas]),
requestVerification: vi.fn(async () => requested),
startVerification: vi.fn(async () => sas),
};
const getOwnDeviceVerificationStatus = vi.fn(async () => mockVerifiedOwnerStatus());
const getOwnCrossSigningPublicationStatus = vi
.fn()
.mockResolvedValueOnce(mockCrossSigningPublicationStatus(false))
.mockResolvedValueOnce(mockCrossSigningPublicationStatus(true));
const bootstrapOwnDeviceVerification = vi.fn(async () => ({
crossSigning: mockCrossSigningPublicationStatus(false),
success: false,
verification: mockVerifiedOwnerStatus(),
}));
const trustOwnIdentityAfterSelfVerification = vi.fn(async () => {});
withStartedActionClientMock.mockImplementation(async (_opts, run) => {
return await run({
bootstrapOwnDeviceVerification,
crypto,
getOwnCrossSigningPublicationStatus,
getOwnDeviceVerificationStatus,
trustOwnIdentityAfterSelfVerification,
});
});
await expect(
runMatrixSelfVerification({ confirmSas: vi.fn(async () => true), timeoutMs: 500 }),
).resolves.toMatchObject({
completed: true,
deviceOwnerVerified: true,
ownerVerification: {
verified: true,
},
});
expect(getOwnDeviceVerificationStatus).toHaveBeenCalledTimes(2);
expect(getOwnCrossSigningPublicationStatus).toHaveBeenCalledTimes(2);
expect(trustOwnIdentityAfterSelfVerification).not.toHaveBeenCalled();
});
it("waits for SAS data without restarting an already-started self-verification", async () => {
const requested = {
completed: false,
@@ -465,6 +554,7 @@ describe("matrix verification actions", () => {
startVerification: vi.fn(),
};
const bootstrapOwnDeviceVerification = vi.fn(async () => ({
crossSigning: mockCrossSigningPublicationStatus(),
success: true,
verification: mockVerifiedOwnerStatus(),
}));
@@ -472,6 +562,7 @@ describe("matrix verification actions", () => {
return await run({
bootstrapOwnDeviceVerification,
crypto,
getOwnCrossSigningPublicationStatus: vi.fn(async () => mockCrossSigningPublicationStatus()),
getOwnDeviceVerificationStatus: vi.fn(async () => mockVerifiedOwnerStatus()),
});
});
@@ -543,6 +634,7 @@ describe("matrix verification actions", () => {
};
const confirmSas = vi.fn(async () => true);
const bootstrapOwnDeviceVerification = vi.fn(async () => ({
crossSigning: mockCrossSigningPublicationStatus(),
success: true,
verification: mockVerifiedOwnerStatus(),
}));
@@ -550,6 +642,7 @@ describe("matrix verification actions", () => {
return await run({
bootstrapOwnDeviceVerification,
crypto,
getOwnCrossSigningPublicationStatus: vi.fn(async () => mockCrossSigningPublicationStatus()),
getOwnDeviceVerificationStatus: vi.fn(async () => mockVerifiedOwnerStatus()),
});
});
@@ -594,6 +687,7 @@ describe("matrix verification actions", () => {
startVerification: vi.fn(async () => sas),
};
const bootstrapOwnDeviceVerification = vi.fn(async () => ({
crossSigning: mockCrossSigningPublicationStatus(),
success: false,
error: "Matrix room key backup is not trusted by this device",
verification: mockVerifiedOwnerStatus(),
@@ -643,6 +737,7 @@ describe("matrix verification actions", () => {
startVerification: vi.fn(async () => sas),
};
const bootstrapOwnDeviceVerification = vi.fn(async () => ({
crossSigning: mockCrossSigningPublicationStatus(false),
success: false,
error: "cross-signing identity is still not trusted",
verification: mockUnverifiedOwnerStatus(),
@@ -651,6 +746,9 @@ describe("matrix verification actions", () => {
return await run({
bootstrapOwnDeviceVerification,
crypto,
getOwnCrossSigningPublicationStatus: vi.fn(async () =>
mockCrossSigningPublicationStatus(false),
),
getOwnDeviceVerificationStatus: vi.fn(async () => mockUnverifiedOwnerStatus()),
});
});

View File

@@ -168,15 +168,21 @@ function formatMatrixOwnerVerificationDiagnostics(
}, locally trusted: ${status.localVerified ? "yes" : "no"}`;
}
async function waitForMatrixOwnerVerificationStatus(params: {
async function waitForMatrixSelfVerificationTrustStatus(params: {
client: MatrixActionClient;
timeoutMs: number;
}): Promise<MatrixOwnDeviceVerificationStatus> {
const startedAt = Date.now();
let last: MatrixOwnDeviceVerificationStatus | undefined;
let crossSigningPublished = false;
while (Date.now() - startedAt < params.timeoutMs) {
last = await params.client.getOwnDeviceVerificationStatus();
if (last.verified) {
const [status, crossSigning] = await Promise.all([
params.client.getOwnDeviceVerificationStatus(),
params.client.getOwnCrossSigningPublicationStatus(),
]);
last = status;
crossSigningPublished = crossSigning.published;
if (last.verified && crossSigningPublished) {
return last;
}
await sleep(Math.min(250, Math.max(25, params.timeoutMs - (Date.now() - startedAt))));
@@ -184,7 +190,7 @@ async function waitForMatrixOwnerVerificationStatus(params: {
throw new Error(
`Timed out waiting for Matrix self-verification to establish full Matrix identity trust for this device (${formatMatrixOwnerVerificationDiagnostics(
last,
)}). Complete self-verification from another Matrix client, then check Matrix verification status for details.`,
)}, cross-signing keys published: ${crossSigningPublished ? "yes" : "no"}). Complete self-verification from another Matrix client, then check Matrix verification status for details.`,
);
}
@@ -215,12 +221,13 @@ async function completeMatrixSelfVerification(params: {
if (!bootstrap.verification.verified) {
await params.client.trustOwnIdentityAfterSelfVerification?.();
}
const ownerVerification = bootstrap.verification.verified
? bootstrap.verification
: await waitForMatrixOwnerVerificationStatus({
client: params.client,
timeoutMs: params.timeoutMs,
});
const ownerVerification =
bootstrap.verification.verified && bootstrap.crossSigning.published
? bootstrap.verification
: await waitForMatrixSelfVerificationTrustStatus({
client: params.client,
timeoutMs: params.timeoutMs,
});
return {
...params.completed,
deviceOwnerVerified: ownerVerification.verified,