Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: client certificate revocation list [WPB-3232] #16649

Merged
merged 7 commits into from
Jan 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
"@peculiar/x509": "1.9.6",
"@wireapp/avs": "9.6.9",
"@wireapp/commons": "5.2.4",
"@wireapp/core": "43.9.1",
"@wireapp/react-ui-kit": "9.12.7",
"@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",
"amplify": "https://github.com/wireapp/amplify#head=master",
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);
};
}
30 changes: 15 additions & 15 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4876,20 +4876,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 @@ -4905,7 +4905,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 @@ -5019,9 +5019,9 @@ __metadata:
languageName: node
linkType: hard

"@wireapp/react-ui-kit@npm:9.12.7":
version: 9.12.7
resolution: "@wireapp/react-ui-kit@npm:9.12.7"
"@wireapp/react-ui-kit@npm:9.12.8":
version: 9.12.8
resolution: "@wireapp/react-ui-kit@npm:9.12.8"
dependencies:
"@types/color": 3.0.6
color: 4.2.3
Expand All @@ -5036,7 +5036,7 @@ __metadata:
peerDependenciesMeta:
"@types/react":
optional: true
checksum: e31a97b6380decd1705dd2b3603a9e59b6e9a7f0e193c718ace0f58dde634aedc6ede11d7574ca37e4e9c6440c80c81fc7a6321bd7d03e751fa788a89b7b4eae
checksum: eaab4ee5abbc36b1b5a50511b7b7824c9f7f717b2b852a35fac148ecb505f1f2a3ad910ecfd3b54c16de04b0fd7284ef60442a957e78da7a25f20a466c6b7fa5
languageName: node
linkType: hard

Expand Down Expand Up @@ -17602,10 +17602,10 @@ __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.7
"@wireapp/react-ui-kit": 9.12.8
"@wireapp/store-engine": ^5.1.4
"@wireapp/store-engine-dexie": 2.1.7
"@wireapp/webapp-events": 0.20.1
Expand Down
Loading