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: federation cross signed certs [WPB-5391] #5882

Merged
merged 9 commits into from
Jan 25, 2024
2 changes: 1 addition & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"dependencies": {
"@wireapp/api-client": "workspace:^",
"@wireapp/commons": "workspace:^",
"@wireapp/core-crypto": "1.0.0-rc.32",
"@wireapp/core-crypto": "1.0.0-rc.33",
"@wireapp/cryptobox": "12.8.0",
"@wireapp/promise-queue": "workspace:^",
"@wireapp/protocol-messaging": "1.44.0",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/Account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,7 @@ export class Account extends TypedEventEmitter<Events> {
cryptoClient.getNativeClient(),
clientService,
mlsService.config.cipherSuite,
this.recurringTaskScheduler,
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import {
OidcChallengeResponseSchema,
GetCertificateResponseSchema,
LocalCertificateRootResponseSchema,
FederationCrossSignedCertificatesResponseSchema,
} from './schema';

import {AcmeChallenge, AcmeDirectory} from '../../E2EIService.types';
Expand All @@ -50,6 +51,7 @@ export class AcmeService {
private readonly axiosInstance: AxiosInstance = axios.create();
private readonly url = {
ROOTS: '/roots.pem',
FEDERATION: '/federation',
};

constructor(private discoveryUrl: string) {}
Expand Down Expand Up @@ -108,6 +110,12 @@ export class AcmeService {
return localCertificateRoot;
}

public async getFederationCrossSignedCertificates(): Promise<string[]> {
const {data} = await this.axiosInstance.get(`${this.acmeBaseUrl}${this.url.FEDERATION}`);
const federationCrossSignedCertificates = FederationCrossSignedCertificatesResponseSchema.parse(data);
return federationCrossSignedCertificates.crts;
}

public async getInitialNonce(url: AcmeDirectory['newNonce']): GetInitialNonceReturnValue {
try {
const {headers} = await this.axiosInstance.head(url);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ export type DirectoryResponseData = z.infer<typeof DirectoryResponseSchema>;
export const LocalCertificateRootResponseSchema = nonOptionalString;
export type LocalCertificateRootResonseData = z.infer<typeof LocalCertificateRootResponseSchema>;

export const FederationCrossSignedCertificatesResponseSchema = z.object({crts: z.array(nonOptionalString)});
export type FederationCrossSignedCertificatesResponseData = z.infer<
typeof FederationCrossSignedCertificatesResponseSchema
>;

export const NewAccountResponseSchema = z.object({
status: nonOptionalString,
orders: nonOptionalUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,35 @@ import {Ciphersuite, CoreCrypto, WireIdentity} from '@wireapp/core-crypto';
import {E2EIServiceExternal} from './E2EIServiceExternal';

import {ClientService} from '../../../client';
import {openDB} from '../../../storage/CoreDB';
import {getUUID} from '../../../test/PayloadHelper';
import {RecurringTaskScheduler} from '../../../util/RecurringTaskScheduler';

function buildE2EIService() {
async function buildE2EIService() {
const coreCrypto = {
getUserIdentities: jest.fn(),
getClientIds: jest.fn().mockResolvedValue([]),
} as unknown as jest.Mocked<CoreCrypto>;

const clientService = {} as jest.Mocked<ClientService>;

const mockedDb = await openDB('core-test-db');

const recurringTaskScheduler = new RecurringTaskScheduler({
delete: key => mockedDb.delete('recurringTasks', key),
get: async key => (await mockedDb.get('recurringTasks', key))?.firingDate,
set: async (key, timestamp) => {
await mockedDb.put('recurringTasks', {key, firingDate: timestamp});
},
});

return [
new E2EIServiceExternal(coreCrypto, clientService, Ciphersuite.MLS_128_DHKEMP256_AES128GCM_SHA256_P256),
new E2EIServiceExternal(
coreCrypto,
clientService,
Ciphersuite.MLS_128_DHKEMP256_AES128GCM_SHA256_P256,
recurringTaskScheduler,
),
{coreCrypto},
] as const;
}
Expand Down Expand Up @@ -64,7 +81,7 @@ const groupId = 'AAEAAhJrE+8TbFFUqiagedTYDUMAZWxuYS53aXJlLmxpbms=';
describe('E2EIServiceExternal', () => {
describe('getUsersIdentities', () => {
it('returns the user identities', async () => {
const [service, {coreCrypto}] = buildE2EIService();
const [service, {coreCrypto}] = await buildE2EIService();
const user1 = {domain: 'elna.wire.link', id: '48a1c3b0-4b0e-4bcd-93ad-64c7344b1534'};
const user2 = {domain: 'elna.wire.link', id: 'b7d287e4-7bbd-40e0-a550-6b18dcaf5f31'};
const userIds = [user1, user2];
Expand All @@ -83,7 +100,7 @@ describe('E2EIServiceExternal', () => {
});

it('returns MLS basic devices with empty identity', async () => {
const [service, {coreCrypto}] = buildE2EIService();
const [service, {coreCrypto}] = await buildE2EIService();
const user1 = {domain: 'elna.wire.link', id: '48a1c3b0-4b0e-4bcd-93ad-64c7344b1534'};
const user2 = {domain: 'elna.wire.link', id: 'b7d287e4-7bbd-40e0-a550-6b18dcaf5f31'};
const userIds = [user1, user2];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
*/

import {QualifiedId} from '@wireapp/api-client/lib/user';
import {TimeInMillis} from '@wireapp/commons/lib/util/TimeUtil';
import {Decoder} from 'bazinga64';

import {Ciphersuite, CoreCrypto, E2eiConversationState, WireIdentity, DeviceStatus} from '@wireapp/core-crypto';
Expand All @@ -29,6 +30,7 @@ import {E2EIStorage} from './Storage/E2EIStorage';
import {ClientService} from '../../../client';
import {parseFullQualifiedClientId} from '../../../util/fullyQualifiedClientIdUtils';
import {LocalStorageStore} from '../../../util/LocalStorageStore';
import {RecurringTaskScheduler} from '../../../util/RecurringTaskScheduler';

export type DeviceIdentity = Omit<WireIdentity, 'free' | 'status'> & {status?: DeviceStatus; deviceId: string};

Expand All @@ -38,6 +40,7 @@ export class E2EIServiceExternal {
private readonly coreCryptoClient: CoreCrypto,
private readonly clientService: ClientService,
private readonly cipherSuite: Ciphersuite,
private readonly recurringTaskScheduler: RecurringTaskScheduler,
) {}

// If we have a handle in the local storage, we are in the enrollment process (this handle is saved before oauth redirect)
Expand Down Expand Up @@ -128,13 +131,18 @@ export class E2EIServiceExternal {
return typeof client.mls_public_keys.ed25519 !== 'string' || client.mls_public_keys.ed25519.length === 0;
}

private async registerLocalCertificateRoot(connection: AcmeService): Promise<string> {
const localCertificateRoot = await connection.getLocalCertificateRoot();
private async registerLocalCertificateRoot(acmeService: AcmeService): Promise<string> {
const localCertificateRoot = await acmeService.getLocalCertificateRoot();
await this.coreCryptoClient.e2eiRegisterAcmeCA(localCertificateRoot);

return localCertificateRoot;
}

private async registerCrossSignedCertificates(acmeService: AcmeService): Promise<void> {
const certificates = await acmeService.getFederationCrossSignedCertificates();
await Promise.all(certificates.map(cert => this.coreCryptoClient.e2eiRegisterIntermediateCA(cert)));
}

/**
* This function is used to register different server certificates in CoreCrypto.
*
Expand All @@ -157,14 +165,26 @@ export class E2EIServiceExternal {

// Register root certificate if not already registered
if (!store.has(ROOT_CA_KEY)) {
try {
await this.registerLocalCertificateRoot(acmeService);
store.add(ROOT_CA_KEY, 'true');
} catch (error) {
console.error('Failed to register root certificate', error);
}
await this.registerLocalCertificateRoot(acmeService);
store.add(ROOT_CA_KEY, 'true');
}

// Register intermediate certificate and update it every 24 hours

const INTERMEDIATE_CA_KEY = 'update-intermediate-certificates';
const hasPendingTask = await this.recurringTaskScheduler.hasTask(INTERMEDIATE_CA_KEY);

const task = () => this.registerCrossSignedCertificates(acmeService);

// If the task was never registered, we run it once, and then register it to run every 24 hours
if (!hasPendingTask) {
await task();
}

await this.recurringTaskScheduler.registerTask({
every: TimeInMillis.DAY,
key: INTERMEDIATE_CA_KEY,
task,
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,6 @@ export class E2EIServiceInternal {

// Step 7: Do OIDC client challenge
const oidcData = await doWireOidcChallenge({
coreCryptoClient: this.coreCryptoClient,
oAuthIdToken,
authData,
connection: this.acmeService,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,10 @@
import {Converter} from 'bazinga64';

import {AcmeService} from '../Connection/AcmeServer';
import {CoreCrypto, E2eiEnrollment, Nonce} from '../E2EIService.types';
import {E2eiEnrollment, Nonce} from '../E2EIService.types';
import {AuthData} from '../Storage/E2EIStorage.schema';

interface DoWireOidcChallengeParams {
coreCryptoClient: CoreCrypto;
authData: AuthData;
identity: E2eiEnrollment;
connection: AcmeService;
Expand All @@ -33,7 +32,6 @@ interface DoWireOidcChallengeParams {
}

export const doWireOidcChallenge = async ({
coreCryptoClient,
connection,
authData,
identity,
Expand All @@ -45,15 +43,13 @@ export const doWireOidcChallenge = async ({
throw new Error('No wireOIDCChallenge defined');
}

const refreshToken = 'empty'; // CC just stores the refresh token (which we don't need for web, as our oidc library does that for us)
const reqBody = await identity.newOidcChallengeRequest(oAuthIdToken, refreshToken, nonce);
const reqBody = await identity.newOidcChallengeRequest(oAuthIdToken, nonce);

const oidcChallengeResponse = await connection.validateOidcChallenge(oidcChallenge.url, reqBody);
if (!oidcChallengeResponse) {
throw new Error('No response received while validating OIDC challenge');
}
await identity.newOidcChallengeResponse(
coreCryptoClient,
Converter.stringToArrayBufferViewUTF8(JSON.stringify(oidcChallengeResponse.data)),
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,8 @@ export class RecurringTaskScheduler {
TaskScheduler.cancelTask(taskKey);
LowPrecisionTaskScheduler.cancelTask({intervalDelay: TimeUtil.TimeInMillis.MINUTE, key: taskKey});
};

public readonly hasTask = async (taskKey: string): Promise<boolean> => {
return !!(await this.storage.get(taskKey));
};
}
10 changes: 5 additions & 5 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -5208,10 +5208,10 @@ __metadata:
languageName: unknown
linkType: soft

"@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

Expand All @@ -5228,7 +5228,7 @@ __metadata:
"@types/tough-cookie": 4.0.5
"@wireapp/api-client": "workspace:^"
"@wireapp/commons": "workspace:^"
"@wireapp/core-crypto": 1.0.0-rc.32
"@wireapp/core-crypto": 1.0.0-rc.33
"@wireapp/cryptobox": 12.8.0
"@wireapp/promise-queue": "workspace:^"
"@wireapp/protocol-messaging": 1.44.0
Expand Down
Loading