Skip to content

Commit

Permalink
feat: client certificate revocation list [WPB-3232] (#16649)
Browse files Browse the repository at this point in the history
* feat: handle conversation state every time crl changes

* feat: show modal if self client's cert is revoked

* test: fix tests by updating mocks

* runfix: cover validating slef crl with try catch

* chore: remove node script

* chore: bump core

* runfix: bump core with rolled back core-crypto to vrc33
  • Loading branch information
PatrykBuniX authored and atomrc committed Jan 29, 2024
1 parent c43835f commit e769795
Show file tree
Hide file tree
Showing 11 changed files with 124 additions and 25 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"@peculiar/x509": "1.9.6",
"@wireapp/avs": "9.6.9",
"@wireapp/commons": "5.2.4",
"@wireapp/core": "43.9.1",
"@wireapp/core": "43.11.2",
"@wireapp/react-ui-kit": "9.12.8",
"@wireapp/store-engine-dexie": "2.1.7",
"@wireapp/webapp-events": "0.20.1",
Expand Down
4 changes: 4 additions & 0 deletions src/__mocks__/@wireapp/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ export class Account extends EventEmitter {
getDeviceIdentities: jest.fn(),
getConversationState: jest.fn(),
registerServerCertificates: jest.fn(),
on: jest.fn(),
emit: jest.fn(),
off: jest.fn(),
initialize: jest.fn(),
},
mls: {
schedulePeriodicKeyMaterialRenewals: jest.fn(),
Expand Down
4 changes: 4 additions & 0 deletions src/i18n/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,10 @@
"acme.renewCertificate.gracePeriodOver.paragraph": "The end-to-end identity certificate for this device has expired. To keep your Wire communication at the highest security level, please update the certificate. <br/> <br/> Enter your identity provider’s credentials in the next step to update the certificate automatically. <br/> <br/> <a href=\"{{url}}\" target=\"_blank\"> Learn more about end-to-end identity </a>",
"acme.renewCertificate.headline.alt": "Update end-to-end identity certificate",
"acme.renewCertificate.paragraph": "The end-to-end identity certificate for this device expires soon. To keep your communication at the highest security level, update your certificate now. <br/> <br/> Enter your identity provider’s credentials in the next step to update the certificate automatically. <br/> <br/> <a href=\"{{url}}\" target=\"_blank\"> Learn more about end-to-end identity </a>",
"acme.selfCertificateRevoked.button.primary": "Log out",
"acme.selfCertificateRevoked.button.cancel": "Continue using this device",
"acme.selfCertificateRevoked.text": "Your team admin revoked the certificate for this device.<br/>Log out to reduce security risks. Then log in again, get a new certificate, and reset your password.<br/><br/>If you keep using this device, your conversations are no longer verified.",
"acme.selfCertificateRevoked.title": "End-to-end identity certificate revoked",
"acme.renewal.done.headline": "Certificate updated",
"acme.renewal.done.paragraph": "The certificate is updated and your device is verified. You can find more details about this certificate in your [bold]Wire Preferences[/bold] under [bold]Devices.[/bold] <br/> <br/> <a href=\"{{url}}\" target=\"_blank\"> Learn more about end-to-end identity </a>",
"acme.renewal.inProgress.headline": "Updating Certificate...",
Expand Down
11 changes: 10 additions & 1 deletion src/script/E2EIdentity/E2EIdentityEnrollment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,16 @@ export class E2EIHandler extends TypedEventEmitter<Events> {
}),
};

await this.coreE2EIService.registerServerCertificates(discoveryUrl);
await this.coreE2EIService.initialize(discoveryUrl);
await this.coreE2EIService.registerServerCertificates();

try {
//FIXME: this doesn't work on curernt core-crypto version
await this.coreE2EIService.validateSelfCrl();
} catch (error) {
console.error('Error validating self CRL', error);
}

this.currentStep = E2EIHandlerStep.INITIALIZED;
return this;
}
Expand Down
16 changes: 16 additions & 0 deletions src/script/E2EIdentity/Modals/Modals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export enum ModalType {
SUCCESS = 'success',
LOADING = 'loading',
CERTIFICATE_RENEWAL = 'certificate_renewal',
SELF_CERTIFICATE_REVOKED = 'self_certificate_revoked',
SNOOZE_REMINDER = 'snooze_reminder',
}

Expand Down Expand Up @@ -120,6 +121,21 @@ export const getModalOptions = ({
hideSecondary || secondaryActionFn === undefined ? PrimaryModal.type.ACKNOWLEDGE : PrimaryModal.type.CONFIRM;
break;

case ModalType.SELF_CERTIFICATE_REVOKED:
options = {
text: {
closeBtnLabel: t('acme.selfCertificateRevoked.button.cancel'),
htmlMessage: t('acme.selfCertificateRevoked.text'),
title: t('acme.selfCertificateRevoked.title'),
},
primaryAction: {
action: primaryActionFn,
text: t('acme.selfCertificateRevoked.button.primary'),
},
};
modalType = PrimaryModal.type.CONFIRM;
break;

case ModalType.SNOOZE_REMINDER:
options = {
text: {
Expand Down
2 changes: 1 addition & 1 deletion src/script/E2EIdentity/certificateDetails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const mapMLSStatus = (status?: CoreStatus) => {
const statusMap: Record<any, MLSStatuses> = {
Valid: MLSStatuses.VALID,
Expired: MLSStatuses.EXPIRED,
Revoked: MLSStatuses.EXPIRED,
Revoked: MLSStatuses.REVOKED,
};

if (!status) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,12 @@ const MLSVerificationBadge = ({context, MLSStatus}: {MLSStatus?: MLSStatuses; co
<CertificateExpiredIcon />
</span>
);
case MLSStatuses.REVOKED:
return (
<span {...mlsVerificationProps} data-tooltip={t('E2EI.certificateRevoked')}>
<CertificateExpiredIcon />
</span>
);
case MLSStatuses.EXPIRES_SOON:
return (
<span {...mlsVerificationProps} data-tooltip={t('E2EI.certificateExpiresSoon')}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,15 +47,15 @@ describe('MLSConversationVerificationStateHandler', () => {
it('should do nothing if MLS service is not available', () => {
core.service!.mls = undefined;

const t = () => registerMLSConversationVerificationStateHandler(undefined, conversationState, core);
const t = () => registerMLSConversationVerificationStateHandler(undefined, undefined, conversationState, core);

expect(t).not.toThrow();
});

it('should do nothing if e2eIdentity service is not available', () => {
core.service!.e2eIdentity = undefined;

registerMLSConversationVerificationStateHandler(undefined, conversationState, core);
registerMLSConversationVerificationStateHandler(undefined, undefined, conversationState, core);

expect(core.service?.mls?.on).not.toHaveBeenCalled();
});
Expand All @@ -69,7 +69,7 @@ describe('MLSConversationVerificationStateHandler', () => {
.spyOn(core.service!.mls!, 'on')
.mockImplementation((_event, listener) => (triggerEpochChange = listener) as any);

registerMLSConversationVerificationStateHandler(undefined, conversationState, core);
registerMLSConversationVerificationStateHandler(undefined, undefined, conversationState, core);

triggerEpochChange({groupId});
await new Promise(resolve => setTimeout(resolve, 0));
Expand All @@ -84,7 +84,7 @@ describe('MLSConversationVerificationStateHandler', () => {
.spyOn(core.service!.mls!, 'on')
.mockImplementation((_event, listener) => (triggerEpochChange = listener) as any);

registerMLSConversationVerificationStateHandler(undefined, conversationState, core);
registerMLSConversationVerificationStateHandler(undefined, undefined, conversationState, core);

triggerEpochChange({groupId});
await new Promise(resolve => setTimeout(resolve, 0));
Expand All @@ -99,7 +99,7 @@ describe('MLSConversationVerificationStateHandler', () => {
.spyOn(core.service!.mls!, 'on')
.mockImplementation((_event, listener) => (triggerEpochChange = listener) as any);

registerMLSConversationVerificationStateHandler(undefined, conversationState, core);
registerMLSConversationVerificationStateHandler(undefined, undefined, conversationState, core);

triggerEpochChange({groupId});
await new Promise(resolve => setTimeout(resolve, 0));
Expand All @@ -116,7 +116,7 @@ describe('MLSConversationVerificationStateHandler', () => {
.spyOn(core.service!.mls!, 'on')
.mockImplementation((_event, listener) => (triggerEpochChange = listener) as any);

registerMLSConversationVerificationStateHandler(undefined, conversationState, core);
registerMLSConversationVerificationStateHandler(undefined, undefined, conversationState, core);

triggerEpochChange({groupId: newConversation.groupId});
setTimeout(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,13 @@ import {QualifiedId} from '@wireapp/api-client/lib/user';
import {E2eiConversationState} from '@wireapp/core/lib/messagingProtocols/mls';
import {container} from 'tsyringe';

import {getConversationVerificationState, getUsersIdentities, MLSStatuses} from 'src/script/E2EIdentity';
import {
getActiveWireIdentity,
getConversationVerificationState,
getUsersIdentities,
MLSStatuses,
} from 'src/script/E2EIdentity';
import {Conversation} from 'src/script/entity/Conversation';
import {E2EIVerificationMessageType} from 'src/script/message/E2EIVerificationMessageType';
import {Core} from 'src/script/service/CoreSingleton';
import {Logger, getLogger} from 'Util/Logger';
Expand All @@ -38,6 +44,7 @@ class MLSConversationVerificationStateHandler {

public constructor(
private readonly onConversationVerificationStateChange: OnConversationE2EIVerificationStateChange,
private readonly onSelfClientCertificateRevoked: () => Promise<void>,
private readonly conversationState: ConversationState,
private readonly core: Core,
) {
Expand All @@ -48,7 +55,9 @@ class MLSConversationVerificationStateHandler {
}

// We hook into the newEpoch event of the MLS service to check if the conversation needs to be verified or degraded
this.core.service.mls.on('newEpoch', this.checkConversationVerificationState);
this.core.service.mls.on('newEpoch', this.onEpochChanged);
this.core.service.e2eIdentity.on('remoteCrlChanged', this.checkAllConversationsVerificationState);
this.core.service.e2eIdentity.on('selfCrlChanged', this.checkSelfCertificateRevocation);
}

/**
Expand Down Expand Up @@ -89,7 +98,34 @@ class MLSConversationVerificationStateHandler {
});
}

private checkConversationVerificationState = async ({groupId}: {groupId: string}): Promise<void> => {
/**
* This function checks if self client certificate is revoked
*/
private checkSelfCertificateRevocation = async (): Promise<void> => {
const activeIdentity = await getActiveWireIdentity();

if (!activeIdentity) {
return;
}

const isRevoked = activeIdentity.status === MLSStatuses.REVOKED;

if (isRevoked) {
await this.onSelfClientCertificateRevoked();
}

await this.checkAllConversationsVerificationState();
};

/**
* This function checks all conversations if they are verified or degraded and updates them accordingly
*/
private checkAllConversationsVerificationState = async (): Promise<void> => {
const conversations = this.conversationState.conversations();
await Promise.all(conversations.map(conversation => this.checkConversationVerificationState(conversation)));
};

private onEpochChanged = async ({groupId}: {groupId: string}): Promise<void> => {
// There could be a race condition where we would receive an epoch update for a conversation that is not yet known by the webapp.
// We just wait for it to be available and then check the verification state
const conversation = await waitFor(() =>
Expand All @@ -100,12 +136,16 @@ class MLSConversationVerificationStateHandler {
return this.logger.warn(`Epoch changed but conversation could not be found after waiting for 5 seconds`);
}

return this.checkConversationVerificationState(conversation);
};

private checkConversationVerificationState = async (conversation: Conversation): Promise<void> => {
const isSelfConversation = conversation.type() === CONVERSATION_TYPE.SELF;
if (!isMLSConversation(conversation) || isSelfConversation) {
return;
}

const verificationState = await getConversationVerificationState(groupId);
const verificationState = await getConversationVerificationState(conversation.groupId);

if (
verificationState === E2eiConversationState.NotVerified &&
Expand All @@ -123,8 +163,14 @@ class MLSConversationVerificationStateHandler {

export const registerMLSConversationVerificationStateHandler = (
onConversationVerificationStateChange: OnConversationE2EIVerificationStateChange = () => {},
onSelfClientCertificateRevoked: () => Promise<void> = async () => {},
conversationState: ConversationState = container.resolve(ConversationState),
core: Core = container.resolve(Core),
): void => {
new MLSConversationVerificationStateHandler(onConversationVerificationStateChange, conversationState, core);
new MLSConversationVerificationStateHandler(
onConversationVerificationStateChange,
onSelfClientCertificateRevoked,
conversationState,
core,
);
};
16 changes: 15 additions & 1 deletion src/script/main/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import {container} from 'tsyringe';
import {Runtime} from '@wireapp/commons';
import {WebAppEvents} from '@wireapp/webapp-events';

import {PrimaryModal} from 'Components/Modals/PrimaryModal';
import {E2EIHandler} from 'src/script/E2EIdentity';
import {initializeDataDog} from 'Util/DataDog';
import {DebugUtil} from 'Util/DebugUtil';
Expand Down Expand Up @@ -64,6 +65,7 @@ import {OnConversationE2EIVerificationStateChange} from '../conversation/Convers
import {EventBuilder} from '../conversation/EventBuilder';
import {MessageRepository} from '../conversation/MessageRepository';
import {CryptographyRepository} from '../cryptography/CryptographyRepository';
import {getModalOptions, ModalType} from '../E2EIdentity/Modals';
import {User} from '../entity/User';
import {AccessTokenError} from '../error/AccessTokenError';
import {AuthError} from '../error/AuthError';
Expand Down Expand Up @@ -450,7 +452,10 @@ export class App {
if (supportsMLS()) {
//if mls is supported, we need to initialize the callbacks (they are used when decrypting messages)
conversationRepository.initMLSConversationRecoveredListener();
registerMLSConversationVerificationStateHandler(this.updateConversationE2EIVerificationState);
registerMLSConversationVerificationStateHandler(
this.updateConversationE2EIVerificationState,
this.showClientCertificateRevokedWarning,
);
}

onProgress(25, t('initReceivedUserData'));
Expand Down Expand Up @@ -859,4 +864,13 @@ export class App {
break;
}
};

private showClientCertificateRevokedWarning = async () => {
const {modalOptions, modalType} = getModalOptions({
type: ModalType.SELF_CERTIFICATE_REVOKED,
primaryActionFn: () => this.logout(SIGN_OUT_REASON.APP_INIT, false),
});

PrimaryModal.show(modalType, modalOptions);
};
}
20 changes: 10 additions & 10 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5118,20 +5118,20 @@ __metadata:
languageName: node
linkType: hard

"@wireapp/core-crypto@npm:1.0.0-rc.32":
version: 1.0.0-rc.32
resolution: "@wireapp/core-crypto@npm:1.0.0-rc.32"
checksum: b8d5c8b28308e276419fd8528cee1ca2eb55f74e47d1ea7ca24054e6a13dd76a4f8fb761bf0697848942a06f412a6ee22491bb14eae30954bf09e2433f9181b6
"@wireapp/core-crypto@npm:1.0.0-rc.33":
version: 1.0.0-rc.33
resolution: "@wireapp/core-crypto@npm:1.0.0-rc.33"
checksum: 44b88f4b7a1ec2ce9c13ce48329c53193c91833f13345abf28e3bfcf51f41ab510187d1192dcf76293c23b9fa4167e67ee1bd084a9739f2dc598ca7987258d27
languageName: node
linkType: hard

"@wireapp/core@npm:43.9.1":
version: 43.9.1
resolution: "@wireapp/core@npm:43.9.1"
"@wireapp/core@npm:43.11.2":
version: 43.11.2
resolution: "@wireapp/core@npm:43.11.2"
dependencies:
"@wireapp/api-client": ^26.10.1
"@wireapp/commons": ^5.2.4
"@wireapp/core-crypto": 1.0.0-rc.32
"@wireapp/core-crypto": 1.0.0-rc.33
"@wireapp/cryptobox": 12.8.0
"@wireapp/promise-queue": ^2.2.9
"@wireapp/protocol-messaging": 1.44.0
Expand All @@ -5147,7 +5147,7 @@ __metadata:
long: ^5.2.0
uuidjs: 4.2.13
zod: 3.22.4
checksum: ad69c04e17c82c01f96fda511c47a335c7972c475ac755042097f2c4e95f97846f8416795666be3eff4f4b4e5caa33022890472a1e99cdfbf89b5f7df928acde
checksum: 685d047c9dd296e528ae8500e6b82c1ad6aa07762fd6ae549672028e8f9741cc88c3dbab15959b68d3b0fc47870daab7ba30d3b2127c15d256409551165df002
languageName: node
linkType: hard

Expand Down Expand Up @@ -17846,7 +17846,7 @@ __metadata:
"@wireapp/avs": 9.6.9
"@wireapp/commons": 5.2.4
"@wireapp/copy-config": 2.1.14
"@wireapp/core": 43.9.1
"@wireapp/core": 43.11.2
"@wireapp/eslint-config": 3.0.5
"@wireapp/prettier-config": 0.6.3
"@wireapp/react-ui-kit": 9.12.8
Expand Down

0 comments on commit e769795

Please sign in to comment.