CDSI Support

This commit is contained in:
Fedor Indutny 2022-06-14 18:15:33 -07:00 committed by GitHub
parent 038ec9e05d
commit 253e050262
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 1432 additions and 1000 deletions

View file

@ -49,7 +49,10 @@ import './startup_config';
import type { ConfigType } from './config';
import type { RendererConfigType } from '../ts/types/RendererConfig';
import { rendererConfigSchema } from '../ts/types/RendererConfig';
import {
directoryConfigSchema,
rendererConfigSchema,
} from '../ts/types/RendererConfig';
import config from './config';
import {
Environment,
@ -368,15 +371,7 @@ async function prepareUrl(
): Promise<string> {
const theme = await getResolvedThemeSetting();
const urlParams: RendererConfigType = {
name: packageJson.productName,
locale: getLocale().name,
version: app.getVersion(),
buildCreation: config.get<number>('buildCreation'),
buildExpiration: config.get<number>('buildExpiration'),
serverUrl: config.get<string>('serverUrl'),
storageUrl: config.get<string>('storageUrl'),
updatesUrl: config.get<string>('updatesUrl'),
const directoryConfig = directoryConfigSchema.safeParse({
directoryVersion: config.get<number | undefined>('directoryVersion') || 1,
directoryUrl: config.get<string | null>('directoryUrl') || undefined,
directoryEnclaveId:
@ -388,6 +383,28 @@ async function prepareUrl(
config.get<string | null>('directoryV2PublicKey') || undefined,
directoryV2CodeHashes:
config.get<Array<string> | null>('directoryV2CodeHashes') || undefined,
directoryV3Url: config.get<string | null>('directoryV3Url') || undefined,
directoryV3MRENCLAVE:
config.get<string | null>('directoryV3MRENCLAVE') || undefined,
directoryV3Root: config.get<string | null>('directoryV3Root') || undefined,
});
if (!directoryConfig.success) {
throw new Error(
`prepareUrl: Failed to parse renderer directory config ${JSON.stringify(
directoryConfig.error.flatten()
)}`
);
}
const urlParams: RendererConfigType = {
name: packageJson.productName,
locale: getLocale().name,
version: app.getVersion(),
buildCreation: config.get<number>('buildCreation'),
buildExpiration: config.get<number>('buildExpiration'),
serverUrl: config.get<string>('serverUrl'),
storageUrl: config.get<string>('storageUrl'),
updatesUrl: config.get<string>('updatesUrl'),
cdnUrl0: config.get<ConfigType>('cdn').get<string>('0'),
cdnUrl2: config.get<ConfigType>('cdn').get<string>('2'),
certificateAuthority: config.get<string>('certificateAuthority'),
@ -408,6 +425,8 @@ async function prepareUrl(
homePath: app.getPath('home'),
crashDumpsPath: app.getPath('crashDumps'),
directoryConfig: directoryConfig.data,
// Only used by the main window
isMainWindowFullScreen: Boolean(mainWindow?.isFullScreen()),

View file

@ -1,15 +1,16 @@
{
"serverUrl": "https://chat.staging.signal.org",
"storageUrl": "https://storage-staging.signal.org",
"directoryVersion": 2,
"directoryVersion": 3,
"directoryUrl": null,
"directoryEnclaveId": null,
"directoryTrustAnchor": null,
"directoryV2Url": "https://cdsh.staging.signal.org",
"directoryV2PublicKey": "2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74",
"directoryV2CodeHashes": [
"2f79dc6c1599b71c70fc2d14f3ea2e3bc65134436eb87011c88845b137af673a"
],
"directoryV2Url": null,
"directoryV2PublicKey": null,
"directoryV2CodeHashes": null,
"directoryV3Url": "https://cdsi.staging.signal.org",
"directoryV3MRENCLAVE": "51133fecb3fa18aaf0c8f64cb763656d3272d9faaacdb26ae7df082e414fb142",
"directoryV3Root": "-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n",
"cdn": {
"0": "https://cdn-staging.signal.org",
"2": "https://cdn2-staging.signal.org"

View file

@ -80,7 +80,7 @@
"@indutny/frameless-titlebar": "2.2.0",
"@popperjs/core": "2.9.2",
"@react-spring/web": "9.4.5",
"@signalapp/libsignal-client": "0.16.0",
"@signalapp/libsignal-client": "0.17.0",
"@sindresorhus/is": "0.8.0",
"@types/fabric": "4.5.3",
"abort-controller": "3.0.0",

View file

@ -21,6 +21,10 @@ message CDSClientRequest {
// the request's prev_e164s, only counting new_e164s. If not set, then
// rate limiting considers both prev_e164s' and new_e164s' size.
optional bytes token = 6;
// After receiving a new token from the server, send back a message just
// containing a token_ack.
optional bool token_ack = 7;
}
message CDSClientResponse {

View file

@ -30,13 +30,7 @@ const WebAPI = initializeWebAPI({
url: config.serverUrl,
storageUrl: config.storageUrl,
updatesUrl: config.updatesUrl,
directoryVersion: config.directoryVersion,
directoryUrl: config.directoryUrl,
directoryEnclaveId: config.directoryEnclaveId,
directoryTrustAnchor: config.directoryTrustAnchor,
directoryV2Url: config.directoryV2Url,
directoryV2PublicKey: config.directoryV2PublicKey,
directoryV2CodeHashes: config.directoryV2CodeHashes,
directoryConfig: config.directoryConfig,
cdnUrlObject: {
0: config.cdnUrl0,
2: config.cdnUrl2,

View file

@ -1,296 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { EventEmitter } from 'events';
import { noop } from 'lodash';
import { Readable } from 'stream';
import type { HsmEnclaveClient } from '@signalapp/libsignal-client';
import type { connection as WebSocket } from 'websocket';
import Long from 'long';
import { strictAssert } from '../util/assert';
import { dropNull } from '../util/dropNull';
import { explodePromise } from '../util/explodePromise';
import * as durations from '../util/durations';
import * as log from '../logging/log';
import type { UUIDStringType } from '../types/UUID';
import { UUID_BYTE_SIZE } from '../types/UUID';
import * as Bytes from '../Bytes';
import * as Timers from '../Timers';
import { uuidToBytes, bytesToUuid } from '../Crypto';
import { SignalService as Proto } from '../protobuf';
enum State {
Handshake,
Established,
Closed,
}
export type CDSRequestOptionsType = Readonly<
{
auth: CDSAuthType;
e164s: ReadonlyArray<string>;
timeout?: number;
} & (
| {
version: 1;
acis?: undefined;
accessKeys?: undefined;
}
| {
version: 2;
acis: ReadonlyArray<UUIDStringType>;
accessKeys: ReadonlyArray<string>;
}
)
>;
export type CDSAuthType = Readonly<{
username: string;
password: string;
}>;
export type CDSSocketDictionaryEntryType = Readonly<{
aci: UUIDStringType | undefined;
pni: UUIDStringType | undefined;
}>;
export type CDSSocketDictionaryType = Readonly<
Record<string, CDSSocketDictionaryEntryType>
>;
export type CDSSocketResponseType = Readonly<{
dictionary: CDSSocketDictionaryType;
retryAfterSecs?: number;
}>;
const MAX_E164_COUNT = 5000;
const HANDSHAKE_TIMEOUT = 10 * durations.SECOND;
const REQUEST_TIMEOUT = 10 * durations.SECOND;
const E164_BYTE_SIZE = 8;
const TRIPLE_BYTE_SIZE = UUID_BYTE_SIZE * 2 + E164_BYTE_SIZE;
export class CDSSocket extends EventEmitter {
private state = State.Handshake;
private readonly finishedHandshake: Promise<void>;
private readonly responseStream = new Readable({
read: noop,
// Don't coalesce separate websocket messages
objectMode: true,
});
constructor(
private readonly socket: WebSocket,
private readonly enclaveClient: HsmEnclaveClient
) {
super();
const {
promise: finishedHandshake,
resolve,
reject,
} = explodePromise<void>();
this.finishedHandshake = finishedHandshake;
const timer = Timers.setTimeout(() => {
reject(new Error('CDS handshake timed out'));
}, HANDSHAKE_TIMEOUT);
socket.on('message', ({ type, binaryData }) => {
strictAssert(type === 'binary', 'Invalid CDS socket packet');
strictAssert(binaryData, 'Invalid CDS socket packet');
if (this.state === State.Handshake) {
this.enclaveClient.completeHandshake(binaryData);
this.state = State.Established;
Timers.clearTimeout(timer);
resolve();
return;
}
try {
this.responseStream.push(
this.enclaveClient.establishedRecv(binaryData)
);
} catch (error) {
this.responseStream.destroy(error);
}
});
socket.on('close', (code, reason) => {
if (this.state === State.Established) {
if (code === 1000) {
this.responseStream.push(null);
} else {
this.responseStream.destroy(
new Error(`Socket closed with code ${code} and reason ${reason}`)
);
}
}
this.state = State.Closed;
this.emit('close', code, reason);
});
socket.on('error', (error: Error) => this.emit('error', error));
socket.sendBytes(this.enclaveClient.initialRequest());
}
public close(code: number, reason: string): void {
this.socket.close(code, reason);
}
public async request({
version,
timeout = REQUEST_TIMEOUT,
e164s,
acis = [],
accessKeys = [],
}: CDSRequestOptionsType): Promise<CDSSocketResponseType> {
strictAssert(
e164s.length < MAX_E164_COUNT,
'CDSSocket does not support paging. Use this for one-off requests'
);
log.info('CDSSocket.request(): awaiting handshake');
await this.finishedHandshake;
strictAssert(
this.state === State.Established,
'Connection not established'
);
strictAssert(
acis.length === accessKeys.length,
`Number of ACIs ${acis.length} is different ` +
`from number of access keys ${accessKeys.length}`
);
const aciUakPairs = new Array<Uint8Array>();
for (let i = 0; i < acis.length; i += 1) {
aciUakPairs.push(
Bytes.concatenate([
uuidToBytes(acis[i]),
Bytes.fromBase64(accessKeys[i]),
])
);
}
const request = Proto.CDSClientRequest.encode({
newE164s: Buffer.concat(
e164s.map(e164 => {
// Long.fromString handles numbers with or without a leading '+'
return new Uint8Array(Long.fromString(e164).toBytesBE());
})
),
aciUakPairs: Buffer.concat(aciUakPairs),
}).finish();
const timer = Timers.setTimeout(() => {
this.responseStream.destroy(new Error('CDS request timed out'));
}, timeout);
log.info(`CDSSocket.request(): sending version=${version} request`);
this.socket.sendBytes(
this.enclaveClient.establishedSend(
Buffer.concat([Buffer.from([version]), request])
)
);
const resultMap: Map<string, CDSSocketDictionaryEntryType> = new Map();
let retryAfterSecs: number | undefined;
for await (const message of this.responseStream) {
log.info('CDSSocket.request(): processing response message');
const response = Proto.CDSClientResponse.decode(message);
const newRetryAfterSecs = dropNull(response.retryAfterSecs);
decodeSingleResponse(resultMap, response);
if (newRetryAfterSecs) {
retryAfterSecs = Math.max(newRetryAfterSecs, retryAfterSecs ?? 0);
}
}
const result: Record<string, CDSSocketDictionaryEntryType> =
Object.create(null);
for (const [key, value] of resultMap) {
result[key] = value;
}
log.info('CDSSocket.request(): done');
Timers.clearTimeout(timer);
return {
dictionary: result,
retryAfterSecs,
};
}
// EventEmitter types
public override on(
type: 'close',
callback: (code: number, reason?: string) => void
): this;
public override on(type: 'error', callback: (error: Error) => void): this;
public override on(
type: string | symbol,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
listener: (...args: Array<any>) => void
): this {
return super.on(type, listener);
}
public override emit(type: 'close', code: number, reason?: string): boolean;
public override emit(type: 'error', error: Error): boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public override emit(type: string | symbol, ...args: Array<any>): boolean {
return super.emit(type, ...args);
}
}
function decodeSingleResponse(
resultMap: Map<string, CDSSocketDictionaryEntryType>,
response: Proto.CDSClientResponse
): void {
for (
let i = 0;
i < response.e164PniAciTriples.length;
i += TRIPLE_BYTE_SIZE
) {
const tripleBytes = response.e164PniAciTriples.slice(
i,
i + TRIPLE_BYTE_SIZE
);
strictAssert(
tripleBytes.length === TRIPLE_BYTE_SIZE,
'Invalid size of CDS response triple'
);
let offset = 0;
const e164Bytes = tripleBytes.slice(offset, offset + E164_BYTE_SIZE);
offset += E164_BYTE_SIZE;
const pniBytes = tripleBytes.slice(offset, offset + UUID_BYTE_SIZE);
offset += UUID_BYTE_SIZE;
const aciBytes = tripleBytes.slice(offset, offset + UUID_BYTE_SIZE);
offset += UUID_BYTE_SIZE;
const e164Long = Long.fromBytesBE(Array.from(e164Bytes));
if (e164Long.isZero()) {
continue;
}
const e164 = `+${e164Long.toString()}`;
const pni = bytesToUuid(pniBytes);
const aci = bytesToUuid(aciBytes);
resultMap.set(e164, { pni, aci });
}
}

View file

@ -1,109 +0,0 @@
// Copyright 2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import ProxyAgent from 'proxy-agent';
import { HsmEnclaveClient } from '@signalapp/libsignal-client';
import type { connection as WebSocket } from 'websocket';
import * as Bytes from '../Bytes';
import type { AbortableProcess } from '../util/AbortableProcess';
import * as durations from '../util/durations';
import { getBasicAuth } from '../util/getBasicAuth';
import { sleep } from '../util/sleep';
import * as log from '../logging/log';
import { CDSSocket } from './CDSSocket';
import type {
CDSAuthType,
CDSRequestOptionsType,
CDSSocketDictionaryType,
} from './CDSSocket';
import { connect as connectWebSocket } from './WebSocket';
export type CDSSocketManagerOptionsType = Readonly<{
url: string;
publicKey: string;
codeHashes: ReadonlyArray<string>;
certificateAuthority: string;
proxyUrl?: string;
version: string;
}>;
export type CDSResponseType = CDSSocketDictionaryType;
export class CDSSocketManager {
private readonly publicKey: Buffer;
private readonly codeHashes: Array<Buffer>;
private readonly proxyAgent?: ReturnType<typeof ProxyAgent>;
private retryAfter?: number;
constructor(private readonly options: CDSSocketManagerOptionsType) {
this.publicKey = Buffer.from(Bytes.fromHex(options.publicKey));
this.codeHashes = options.codeHashes.map(hash =>
Buffer.from(Bytes.fromHex(hash))
);
if (options.proxyUrl) {
this.proxyAgent = new ProxyAgent(options.proxyUrl);
}
}
public async request(
options: CDSRequestOptionsType
): Promise<CDSResponseType> {
if (this.retryAfter !== undefined) {
const delay = Math.max(0, this.retryAfter - Date.now());
log.info(`CDSSocketManager: waiting ${delay}ms before retrying`);
await sleep(delay);
}
const { auth } = options;
log.info('CDSSocketManager: connecting socket');
const socket = await this.connect(auth).getResult();
log.info('CDSSocketManager: connected socket');
try {
const { dictionary, retryAfterSecs = 0 } = await socket.request(options);
if (retryAfterSecs > 0) {
this.retryAfter = Math.max(
this.retryAfter ?? Date.now(),
Date.now() + retryAfterSecs * durations.SECOND
);
}
return dictionary;
} finally {
log.info('CDSSocketManager: closing socket');
socket.close(3000, 'Normal');
}
}
private connect(auth: CDSAuthType): AbortableProcess<CDSSocket> {
const enclaveClient = HsmEnclaveClient.new(this.publicKey, this.codeHashes);
const { publicKey: publicKeyHex, codeHashes, version } = this.options;
const url = `${
this.options.url
}/discovery/${publicKeyHex}/${codeHashes.join(',')}`;
return connectWebSocket<CDSSocket>({
name: 'CDSSocket',
url,
version,
proxyAgent: this.proxyAgent,
certificateAuthority: this.options.certificateAuthority,
extraHeaders: {
authorization: getBasicAuth(auth),
},
createResource: (socket: WebSocket): CDSSocket => {
return new CDSSocket(socket, enclaveClient);
},
});
}
}

View file

@ -48,7 +48,7 @@ import type {
SendLogCallbackType,
} from './OutgoingMessage';
import OutgoingMessage from './OutgoingMessage';
import type { CDSResponseType } from './CDSSocketManager';
import type { CDSResponseType } from './cds/Types.d';
import * as Bytes from '../Bytes';
import { getRandomBytes, getZeroes, encryptAttachment } from '../Crypto';
import {

View file

@ -2,7 +2,6 @@
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable no-param-reassign */
/* eslint-disable no-bitwise */
/* eslint-disable guard-for-in */
/* eslint-disable no-restricted-syntax */
/* eslint-disable @typescript-eslint/no-explicit-any */
@ -12,16 +11,12 @@ import type { Response } from 'node-fetch';
import fetch from 'node-fetch';
import ProxyAgent from 'proxy-agent';
import { Agent } from 'https';
import pProps from 'p-props';
import type { Dictionary } from 'lodash';
import { compact, escapeRegExp, isNumber, mapValues, zipObject } from 'lodash';
import { createVerify } from 'crypto';
import { pki } from 'node-forge';
import { escapeRegExp, isNumber } from 'lodash';
import is from '@sindresorhus/is';
import PQueue from 'p-queue';
import { v4 as getGuid } from 'uuid';
import { z } from 'zod';
import Long from 'long';
import type { Readable } from 'stream';
import { assert, strictAssert } from '../util/assert';
@ -40,21 +35,17 @@ import { isPackIdValid, redactPackId } from '../types/Stickers';
import type { UUID, UUIDStringType } from '../types/UUID';
import { isValidUuid, UUIDKind } from '../types/UUID';
import * as Bytes from '../Bytes';
import {
constantTimeEqual,
decryptAesGcm,
deriveSecrets,
encryptCdsDiscoveryRequest,
getRandomValue,
splitUuids,
} from '../Crypto';
import { calculateAgreement, generateKeyPair } from '../Curve';
import { getRandomValue } from '../Crypto';
import * as linkPreviewFetch from '../linkPreviews/linkPreviewFetch';
import { isBadgeImageFileUrlValid } from '../badges/isBadgeImageFileUrlValid';
import { SocketManager } from './SocketManager';
import type { CDSResponseType } from './CDSSocketManager';
import { CDSSocketManager } from './CDSSocketManager';
import type { CDSAuthType, CDSResponseType } from './cds/Types.d';
import type { CDSBase } from './cds/CDSBase';
import { LegacyCDS } from './cds/LegacyCDS';
import type { LegacyCDSPutAttestationResponseType } from './cds/LegacyCDS';
import { CDSH } from './cds/CDSH';
import { CDSI } from './cds/CDSI';
import type WebSocketResource from './WebsocketResources';
import { SignalService as Proto } from '../protobuf';
@ -75,43 +66,6 @@ import { maybeParseUrl } from '../util/url';
// debugging failed requests.
const DEBUG = false;
type SgxConstantsType = {
SGX_FLAGS_INITTED: Long;
SGX_FLAGS_DEBUG: Long;
SGX_FLAGS_MODE64BIT: Long;
SGX_FLAGS_PROVISION_KEY: Long;
SGX_FLAGS_EINITTOKEN_KEY: Long;
SGX_FLAGS_RESERVED: Long;
SGX_XFRM_LEGACY: Long;
SGX_XFRM_AVX: Long;
SGX_XFRM_RESERVED: Long;
};
let sgxConstantCache: SgxConstantsType | null = null;
function makeLong(value: string): Long {
return Long.fromString(value);
}
function getSgxConstants() {
if (sgxConstantCache) {
return sgxConstantCache;
}
sgxConstantCache = {
SGX_FLAGS_INITTED: makeLong('x0000000000000001L'),
SGX_FLAGS_DEBUG: makeLong('x0000000000000002L'),
SGX_FLAGS_MODE64BIT: makeLong('x0000000000000004L'),
SGX_FLAGS_PROVISION_KEY: makeLong('x0000000000000004L'),
SGX_FLAGS_EINITTOKEN_KEY: makeLong('x0000000000000004L'),
SGX_FLAGS_RESERVED: makeLong('xFFFFFFFFFFFFFFC8L'),
SGX_XFRM_LEGACY: makeLong('x0000000000000003L'),
SGX_XFRM_AVX: makeLong('x0000000000000006L'),
SGX_XFRM_RESERVED: makeLong('xFFFFFFFFFFFFFFF8L'),
};
return sgxConstantCache;
}
function _createRedactor(
...toReplace: ReadonlyArray<string | undefined>
): RedactUrl {
@ -211,8 +165,8 @@ type PromiseAjaxOptionsType = {
}
);
type JSONWithDetailsType = {
data: unknown;
type JSONWithDetailsType<Data = unknown> = {
data: Data;
contentType: string | null;
response: Response;
};
@ -594,17 +548,46 @@ const WEBSOCKET_CALLS = new Set<keyof typeof URL_CALLS>([
'storageToken',
]);
type DirectoryV1OptionsType = Readonly<{
directoryVersion: 1;
directoryUrl: string;
directoryEnclaveId: string;
directoryTrustAnchor: string;
}>;
type DirectoryV2OptionsType = Readonly<{
directoryVersion: 2;
directoryV2Url: string;
directoryV2PublicKey: string;
directoryV2CodeHashes: ReadonlyArray<string>;
}>;
type DirectoryV3OptionsType = Readonly<{
directoryVersion: 3;
directoryV3Url: string;
directoryV3MRENCLAVE: string;
directoryV3Root: string;
}>;
type OptionalDirectoryFieldsType = {
directoryUrl?: unknown;
directoryEnclaveId?: unknown;
directoryTrustAnchor?: unknown;
directoryV2Url?: unknown;
directoryV2PublicKey?: unknown;
directoryV2CodeHashes?: unknown;
directoryV3Url?: unknown;
directoryV3MRENCLAVE?: unknown;
directoryV3Root?: unknown;
};
type DirectoryOptionsType = OptionalDirectoryFieldsType &
(DirectoryV1OptionsType | DirectoryV2OptionsType | DirectoryV3OptionsType);
type InitializeOptionsType = {
url: string;
storageUrl: string;
updatesUrl: string;
directoryVersion: number;
directoryUrl?: string;
directoryEnclaveId?: string;
directoryTrustAnchor?: string;
directoryV2Url?: string;
directoryV2PublicKey?: string;
directoryV2CodeHashes?: ReadonlyArray<string>;
cdnUrlObject: {
readonly '0': string;
readonly [propName: string]: string;
@ -613,6 +596,7 @@ type InitializeOptionsType = {
contentProxyUrl: string;
proxyUrl: string | undefined;
version: string;
directoryConfig: DirectoryOptionsType;
};
export type MessageType = Readonly<{
@ -1031,13 +1015,7 @@ export function initialize({
url,
storageUrl,
updatesUrl,
directoryVersion,
directoryUrl,
directoryEnclaveId,
directoryTrustAnchor,
directoryV2Url,
directoryV2PublicKey,
directoryV2CodeHashes,
directoryConfig,
cdnUrlObject,
certificateAuthority,
contentProxyUrl,
@ -1053,36 +1031,6 @@ export function initialize({
if (!is.string(updatesUrl)) {
throw new Error('WebAPI.initialize: Invalid updatesUrl');
}
if (directoryVersion === 1) {
if (!is.string(directoryEnclaveId)) {
throw new Error('WebAPI.initialize: Invalid directory enclave id');
}
if (!is.string(directoryTrustAnchor)) {
throw new Error('WebAPI.initialize: Invalid directory trust anchor');
}
if (!is.string(directoryUrl)) {
throw new Error('WebAPI.initialize: Invalid directory url');
}
} else {
if (directoryEnclaveId) {
throw new Error('WebAPI.initialize: Invalid directory enclave id');
}
if (directoryTrustAnchor) {
throw new Error('WebAPI.initialize: Invalid directory trust anchor');
}
if (directoryUrl) {
throw new Error('WebAPI.initialize: Invalid directory url');
}
}
if (!is.string(directoryV2Url)) {
throw new Error('WebAPI.initialize: Invalid directory V2 url');
}
if (!is.string(directoryV2PublicKey)) {
throw new Error('WebAPI.initialize: Invalid directory V2 public key');
}
if (!is.array(directoryV2CodeHashes)) {
throw new Error('WebAPI.initialize: Invalid directory V2 code hash');
}
if (!is.object(cdnUrlObject)) {
throw new Error('WebAPI.initialize: Invalid cdnUrlObject');
}
@ -1146,21 +1094,128 @@ export function initialize({
socketManager.authenticate({ username, password });
}
const cdsUrl = directoryV2Url || directoryUrl;
if (!cdsUrl) {
throw new Error('No CDS url available!');
let cds: CDSBase;
if (directoryConfig.directoryVersion === 1) {
const { directoryUrl, directoryEnclaveId, directoryTrustAnchor } =
directoryConfig;
cds = new LegacyCDS({
logger: log,
directoryEnclaveId,
directoryTrustAnchor,
proxyUrl,
async putAttestation(auth, publicKey) {
const data = JSON.stringify({
clientPublic: Bytes.toBase64(publicKey),
});
const result = (await _outerAjax(null, {
certificateAuthority,
type: 'PUT',
contentType: 'application/json; charset=utf-8',
host: directoryUrl,
path: `${URL_CALLS.attestation}/${directoryEnclaveId}`,
user: auth.username,
password: auth.password,
responseType: 'jsonwithdetails',
data,
timeout: 30000,
version,
})) as JSONWithDetailsType<LegacyCDSPutAttestationResponseType>;
const { response, data: responseBody } = result;
const cookie = response.headers.get('set-cookie') ?? undefined;
return { cookie, responseBody };
},
async fetchDiscoveryData(auth, data, cookie) {
const response = (await _outerAjax(null, {
certificateAuthority,
type: 'PUT',
headers: cookie
? {
cookie,
}
: undefined,
contentType: 'application/json; charset=utf-8',
host: directoryUrl,
path: `${URL_CALLS.discovery}/${directoryEnclaveId}`,
user: auth.username,
password: auth.password,
responseType: 'json',
timeout: 30000,
data: JSON.stringify(data),
version,
})) as {
requestId: string;
iv: string;
data: string;
mac: string;
};
return {
requestId: Bytes.fromBase64(response.requestId),
iv: Bytes.fromBase64(response.iv),
data: Bytes.fromBase64(response.data),
mac: Bytes.fromBase64(response.mac),
};
},
async getAuth() {
return (await _ajax({
call: 'directoryAuth',
httpType: 'GET',
responseType: 'json',
})) as CDSAuthType;
},
});
} else if (directoryConfig.directoryVersion === 2) {
const { directoryV2Url, directoryV2PublicKey, directoryV2CodeHashes } =
directoryConfig;
cds = new CDSH({
logger: log,
proxyUrl,
url: directoryV2Url,
publicKey: directoryV2PublicKey,
codeHashes: directoryV2CodeHashes,
certificateAuthority,
version,
async getAuth() {
return (await _ajax({
call: 'directoryAuthV2',
httpType: 'GET',
responseType: 'json',
})) as CDSAuthType;
},
});
} else if (directoryConfig.directoryVersion === 3) {
const { directoryV3Url, directoryV3MRENCLAVE, directoryV3Root } =
directoryConfig;
cds = new CDSI({
logger: log,
proxyUrl,
url: directoryV3Url,
mrenclave: directoryV3MRENCLAVE,
root: directoryV3Root,
certificateAuthority,
version,
async getAuth() {
return (await _ajax({
call: 'directoryAuthV2',
httpType: 'GET',
responseType: 'json',
})) as CDSAuthType;
},
});
}
if (!directoryV2PublicKey || !directoryV2CodeHashes?.length) {
throw new Error('No CDS public key or code hashes available');
}
const cdsSocketManager = new CDSSocketManager({
url: cdsUrl,
publicKey: directoryV2PublicKey,
codeHashes: directoryV2CodeHashes,
certificateAuthority,
version,
proxyUrl,
});
let fetchForLinkPreviews: linkPreviewFetch.FetchFn;
if (proxyUrl) {
@ -2778,444 +2833,18 @@ export function initialize({
return socketManager.getProvisioningResource(handler);
}
async function getDirectoryAuth(): Promise<{
username: string;
password: string;
}> {
strictAssert(directoryVersion === 1, 'Legacy CDS should not be used');
return (await _ajax({
call: 'directoryAuth',
httpType: 'GET',
responseType: 'json',
})) as { username: string; password: string };
}
async function getDirectoryAuthV2(): Promise<{
username: string;
password: string;
}> {
return (await _ajax({
call: 'directoryAuthV2',
httpType: 'GET',
responseType: 'json',
})) as { username: string; password: string };
}
function validateAttestationQuote({
serverStaticPublic,
quote: quoteBytes,
}: {
serverStaticPublic: Uint8Array;
quote: Uint8Array;
}) {
strictAssert(directoryVersion === 1, 'Legacy CDS should not be used');
strictAssert(directoryEnclaveId, 'Legacy CDS needs directoryEnclaveId');
const SGX_CONSTANTS = getSgxConstants();
const quote = Buffer.from(quoteBytes);
const quoteVersion = quote.readInt16LE(0) & 0xffff;
if (quoteVersion < 0 || quoteVersion > 2) {
throw new Error(`Unknown version ${quoteVersion}`);
}
const miscSelect = quote.slice(64, 64 + 4);
if (!miscSelect.every(byte => byte === 0)) {
throw new Error('Quote miscSelect invalid!');
}
const reserved1 = quote.slice(68, 68 + 28);
if (!reserved1.every(byte => byte === 0)) {
throw new Error('Quote reserved1 invalid!');
}
const flags = Long.fromBytesLE(
Array.from(quote.slice(96, 96 + 8).values())
);
if (
flags.and(SGX_CONSTANTS.SGX_FLAGS_RESERVED).notEquals(0) ||
flags.and(SGX_CONSTANTS.SGX_FLAGS_INITTED).equals(0) ||
flags.and(SGX_CONSTANTS.SGX_FLAGS_MODE64BIT).equals(0)
) {
throw new Error(`Quote flags invalid ${flags.toString()}`);
}
const xfrm = Long.fromBytesLE(
Array.from(quote.slice(104, 104 + 8).values())
);
if (xfrm.and(SGX_CONSTANTS.SGX_XFRM_RESERVED).notEquals(0)) {
throw new Error(`Quote xfrm invalid ${xfrm}`);
}
const mrenclave = quote.slice(112, 112 + 32);
const enclaveIdBytes = Bytes.fromHex(directoryEnclaveId);
if (mrenclave.compare(enclaveIdBytes) !== 0) {
throw new Error('Quote mrenclave invalid!');
}
const reserved2 = quote.slice(144, 144 + 32);
if (!reserved2.every(byte => byte === 0)) {
throw new Error('Quote reserved2 invalid!');
}
const reportData = quote.slice(368, 368 + 64);
const serverStaticPublicBytes = serverStaticPublic;
if (
!reportData.every((byte, index) => {
if (index >= 32) {
return byte === 0;
}
return byte === serverStaticPublicBytes[index];
})
) {
throw new Error('Quote report_data invalid!');
}
const reserved3 = quote.slice(208, 208 + 96);
if (!reserved3.every(byte => byte === 0)) {
throw new Error('Quote reserved3 invalid!');
}
const reserved4 = quote.slice(308, 308 + 60);
if (!reserved4.every(byte => byte === 0)) {
throw new Error('Quote reserved4 invalid!');
}
const signatureLength = quote.readInt32LE(432) >>> 0;
if (signatureLength !== quote.byteLength - 436) {
throw new Error(`Bad signatureLength ${signatureLength}`);
}
// const signature = quote.slice(436, 436 + signatureLength);
}
function validateAttestationSignatureBody(
signatureBody: {
timestamp: string;
version: number;
isvEnclaveQuoteBody: string;
isvEnclaveQuoteStatus: string;
},
encodedQuote: string
) {
strictAssert(directoryVersion === 1, 'Legacy CDS should not be used');
// Parse timestamp as UTC
const { timestamp } = signatureBody;
const utcTimestamp = timestamp.endsWith('Z')
? timestamp
: `${timestamp}Z`;
const signatureTime = new Date(utcTimestamp).getTime();
const now = Date.now();
if (signatureBody.version !== 3) {
throw new Error('Attestation signature invalid version!');
}
if (!encodedQuote.startsWith(signatureBody.isvEnclaveQuoteBody)) {
throw new Error('Attestion signature mismatches quote!');
}
if (signatureBody.isvEnclaveQuoteStatus !== 'OK') {
throw new Error('Attestation signature status not "OK"!');
}
if (signatureTime < now - 24 * 60 * 60 * 1000) {
throw new Error('Attestation signature timestamp older than 24 hours!');
}
}
async function validateAttestationSignature(
signature: Uint8Array,
signatureBody: string,
certificates: string
) {
strictAssert(directoryVersion === 1, 'Legacy CDS should not be used');
strictAssert(
directoryTrustAnchor,
'Legacy CDS needs directoryTrustAnchor'
);
const CERT_PREFIX = '-----BEGIN CERTIFICATE-----';
const pem = compact(
certificates.split(CERT_PREFIX).map(match => {
if (!match) {
return null;
}
return `${CERT_PREFIX}${match}`;
})
);
if (pem.length < 2) {
throw new Error(
`validateAttestationSignature: Expect two or more entries; got ${pem.length}`
);
}
const verify = createVerify('RSA-SHA256');
verify.update(Buffer.from(Bytes.fromString(signatureBody)));
const isValid = verify.verify(pem[0], Buffer.from(signature));
if (!isValid) {
throw new Error('Validation of signature across signatureBody failed!');
}
const caStore = pki.createCaStore([directoryTrustAnchor]);
const chain = compact(pem.map(cert => pki.certificateFromPem(cert)));
const isChainValid = pki.verifyCertificateChain(caStore, chain);
if (!isChainValid) {
throw new Error('Validation of certificate chain failed!');
}
const leafCert = chain[0];
const fieldCN = leafCert.subject.getField('CN');
if (
!fieldCN ||
fieldCN.value !== 'Intel SGX Attestation Report Signing'
) {
throw new Error('Leaf cert CN field had unexpected value');
}
const fieldO = leafCert.subject.getField('O');
if (!fieldO || fieldO.value !== 'Intel Corporation') {
throw new Error('Leaf cert O field had unexpected value');
}
const fieldL = leafCert.subject.getField('L');
if (!fieldL || fieldL.value !== 'Santa Clara') {
throw new Error('Leaf cert L field had unexpected value');
}
const fieldST = leafCert.subject.getField('ST');
if (!fieldST || fieldST.value !== 'CA') {
throw new Error('Leaf cert ST field had unexpected value');
}
const fieldC = leafCert.subject.getField('C');
if (!fieldC || fieldC.value !== 'US') {
throw new Error('Leaf cert C field had unexpected value');
}
}
async function putRemoteAttestation(auth: {
username: string;
password: string;
}) {
strictAssert(directoryVersion === 1, 'Legacy CDS should not be used');
const keyPair = generateKeyPair();
const { privKey, pubKey } = keyPair;
// Remove first "key type" byte from public key
const slicedPubKey = pubKey.slice(1);
const pubKeyBase64 = Bytes.toBase64(slicedPubKey);
// Do request
const data = JSON.stringify({ clientPublic: pubKeyBase64 });
const result: JSONWithDetailsType = (await _outerAjax(null, {
certificateAuthority,
type: 'PUT',
contentType: 'application/json; charset=utf-8',
host: directoryUrl,
path: `${URL_CALLS.attestation}/${directoryEnclaveId}`,
user: auth.username,
password: auth.password,
responseType: 'jsonwithdetails',
data,
timeout: 30000,
version,
})) as JSONWithDetailsType;
const { data: responseBody, response } = result as {
data: {
attestations: Record<
string,
{
ciphertext: string;
iv: string;
quote: string;
serverEphemeralPublic: string;
serverStaticPublic: string;
signature: string;
signatureBody: string;
tag: string;
certificates: string;
}
>;
};
response: Response;
};
const attestationsLength = Object.keys(responseBody.attestations).length;
if (attestationsLength > 3) {
throw new Error(
'Got more than three attestations from the Contact Discovery Service'
);
}
if (attestationsLength < 1) {
throw new Error(
'Got no attestations from the Contact Discovery Service'
);
}
const cookie = response.headers.get('set-cookie');
// Decode response
return {
cookie,
attestations: await pProps(
responseBody.attestations,
async attestation => {
const decoded = {
...attestation,
ciphertext: Bytes.fromBase64(attestation.ciphertext),
iv: Bytes.fromBase64(attestation.iv),
quote: Bytes.fromBase64(attestation.quote),
serverEphemeralPublic: Bytes.fromBase64(
attestation.serverEphemeralPublic
),
serverStaticPublic: Bytes.fromBase64(
attestation.serverStaticPublic
),
signature: Bytes.fromBase64(attestation.signature),
tag: Bytes.fromBase64(attestation.tag),
};
// Validate response
validateAttestationQuote(decoded);
validateAttestationSignatureBody(
JSON.parse(decoded.signatureBody),
attestation.quote
);
await validateAttestationSignature(
decoded.signature,
decoded.signatureBody,
decoded.certificates
);
// Derive key
const ephemeralToEphemeral = calculateAgreement(
decoded.serverEphemeralPublic,
privKey
);
const ephemeralToStatic = calculateAgreement(
decoded.serverStaticPublic,
privKey
);
const masterSecret = Bytes.concatenate([
ephemeralToEphemeral,
ephemeralToStatic,
]);
const publicKeys = Bytes.concatenate([
slicedPubKey,
decoded.serverEphemeralPublic,
decoded.serverStaticPublic,
]);
const [clientKey, serverKey] = deriveSecrets(
masterSecret,
publicKeys,
new Uint8Array(0)
);
// Decrypt ciphertext into requestId
const requestId = decryptAesGcm(
serverKey,
decoded.iv,
Bytes.concatenate([decoded.ciphertext, decoded.tag])
);
return {
clientKey,
serverKey,
requestId,
};
}
),
};
}
async function getLegacyUuidsForE164s(
e164s: ReadonlyArray<string>
): Promise<Dictionary<UUIDStringType | null>> {
const directoryAuth = await getDirectoryAuth();
const attestationResult = await putRemoteAttestation(directoryAuth);
// Encrypt data for discovery
const data = await encryptCdsDiscoveryRequest(
attestationResult.attestations,
e164s
);
const { cookie } = attestationResult;
// Send discovery request
const discoveryResponse = (await _outerAjax(null, {
certificateAuthority,
type: 'PUT',
headers: cookie
? {
cookie,
}
: undefined,
contentType: 'application/json; charset=utf-8',
host: directoryUrl,
path: `${URL_CALLS.discovery}/${directoryEnclaveId}`,
user: directoryAuth.username,
password: directoryAuth.password,
responseType: 'json',
timeout: 30000,
data: JSON.stringify(data),
version,
})) as {
requestId: string;
iv: string;
data: string;
mac: string;
};
// Decode discovery request response
const decodedDiscoveryResponse = mapValues(discoveryResponse, value => {
return Bytes.fromBase64(value);
}) as unknown as {
[K in keyof typeof discoveryResponse]: Uint8Array;
};
const returnedAttestation = Object.values(
attestationResult.attestations
).find(at =>
constantTimeEqual(at.requestId, decodedDiscoveryResponse.requestId)
);
if (!returnedAttestation) {
throw new Error('No known attestations returned from CDS');
}
// Decrypt discovery response
const decryptedDiscoveryData = decryptAesGcm(
returnedAttestation.serverKey,
decodedDiscoveryResponse.iv,
Bytes.concatenate([
decodedDiscoveryResponse.data,
decodedDiscoveryResponse.mac,
])
);
// Process and return result
const uuids = splitUuids(decryptedDiscoveryData);
if (uuids.length !== e164s.length) {
throw new Error(
'Returned set of UUIDs did not match returned set of e164s!'
);
}
return zipObject(e164s, uuids);
}
async function getUuidsForE164s(
e164s: ReadonlyArray<string>
): Promise<Dictionary<UUIDStringType | null>> {
if (directoryVersion === 1) {
return getLegacyUuidsForE164s(e164s);
}
const auth = await getDirectoryAuthV2();
const dictionary = await cdsSocketManager.request({
version: 1,
auth,
const map = await cds.request({
e164s,
});
return mapValues(dictionary, value => value.aci ?? null);
const result: Dictionary<UUIDStringType | null> = {};
for (const [key, value] of map) {
result[key] = value.pni ?? value.aci ?? null;
}
return result;
}
async function getUuidsForE164sV2({
@ -3223,11 +2852,7 @@ export function initialize({
acis,
accessKeys,
}: GetUuidsForE164sV2OptionsType): Promise<CDSResponseType> {
const auth = await getDirectoryAuthV2();
return cdsSocketManager.request({
version: 2,
auth,
return cds.request({
e164s,
acis,
accessKeys,

View file

@ -0,0 +1,67 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import ProxyAgent from 'proxy-agent';
import type {
CDSAuthType,
CDSRequestOptionsType,
CDSResponseType,
} from './Types.d';
import type { LoggerType } from '../../types/Logging';
import { isOlderThan } from '../../util/timestamp';
import { HOUR } from '../../util/durations';
// It is 24 hours, but we don't want latency between server and client to be
// count.
const CACHED_AUTH_TTL = 23 * HOUR;
export type CDSBaseOptionsType = Readonly<{
logger: LoggerType;
proxyUrl?: string;
getAuth(): Promise<CDSAuthType>;
}>;
export type CachedAuthType = Readonly<{
timestamp: number;
auth: CDSAuthType;
}>;
export abstract class CDSBase<
Options extends CDSBaseOptionsType = CDSBaseOptionsType
> {
protected readonly logger: LoggerType;
protected readonly proxyAgent?: ReturnType<typeof ProxyAgent>;
protected cachedAuth?: CachedAuthType;
constructor(protected readonly options: Options) {
this.logger = options.logger;
if (options.proxyUrl) {
this.proxyAgent = new ProxyAgent(options.proxyUrl);
}
}
public abstract request(
options: CDSRequestOptionsType
): Promise<CDSResponseType>;
protected async getAuth(): Promise<CDSAuthType> {
if (this.cachedAuth) {
if (isOlderThan(this.cachedAuth.timestamp, CACHED_AUTH_TTL)) {
this.cachedAuth = undefined;
} else {
return this.cachedAuth.auth;
}
}
const auth = await this.options.getAuth();
this.cachedAuth = {
auth,
timestamp: Date.now(),
};
return auth;
}
}

50
ts/textsecure/cds/CDSH.ts Normal file
View file

@ -0,0 +1,50 @@
// Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { HsmEnclaveClient } from '@signalapp/libsignal-client';
import type { connection as WebSocket } from 'websocket';
import * as Bytes from '../../Bytes';
import { CDSHSocket } from './CDSHSocket';
import type { CDSSocketManagerBaseOptionsType } from './CDSSocketManagerBase';
import { CDSSocketManagerBase } from './CDSSocketManagerBase';
export type CDSHOptionsType = Readonly<{
publicKey: string;
codeHashes: ReadonlyArray<string>;
}> &
CDSSocketManagerBaseOptionsType;
export class CDSH extends CDSSocketManagerBase<CDSHSocket, CDSHOptionsType> {
private readonly publicKey: Buffer;
private readonly codeHashes: Array<Buffer>;
constructor(options: CDSHOptionsType) {
super(options);
this.publicKey = Buffer.from(Bytes.fromHex(options.publicKey));
this.codeHashes = options.codeHashes.map(hash =>
Buffer.from(Bytes.fromHex(hash))
);
}
protected override getSocketUrl(): string {
const { publicKey: publicKeyHex, codeHashes } = this.options;
return (
`${this.options.url}/discovery/${publicKeyHex}/` +
`${codeHashes.join(',')}`
);
}
protected override createSocket(socket: WebSocket): CDSHSocket {
const enclaveClient = HsmEnclaveClient.new(this.publicKey, this.codeHashes);
return new CDSHSocket({
logger: this.logger,
socket,
enclaveClient,
});
}
}

View file

@ -0,0 +1,49 @@
// Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { HsmEnclaveClient } from '@signalapp/libsignal-client';
import { strictAssert } from '../../util/assert';
import { CDSSocketBase, CDSSocketState } from './CDSSocketBase';
import type { CDSSocketBaseOptionsType } from './CDSSocketBase';
export type CDSHSocketOptionsType = Readonly<{
enclaveClient: HsmEnclaveClient;
}> &
CDSSocketBaseOptionsType;
export class CDSHSocket extends CDSSocketBase<CDSHSocketOptionsType> {
public override async handshake(): Promise<void> {
strictAssert(
this.state === CDSSocketState.Open,
'CDSH handshake called twice'
);
this.state = CDSSocketState.Handshake;
// Handshake
this.socket.sendBytes(this.options.enclaveClient.initialRequest());
const { done, value: message } = await this.socketIterator.next();
strictAssert(!done, 'Expected CDSH handshake response');
this.options.enclaveClient.completeHandshake(message);
this.state = CDSSocketState.Established;
}
protected override async sendRequest(
version: number,
request: Buffer
): Promise<void> {
this.socket.sendBytes(
this.options.enclaveClient.establishedSend(
Buffer.concat([Buffer.from([version]), request])
)
);
}
protected override async decryptResponse(
ciphertext: Buffer
): Promise<Buffer> {
return this.options.enclaveClient.establishedRecv(ciphertext);
}
}

43
ts/textsecure/cds/CDSI.ts Normal file
View file

@ -0,0 +1,43 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { connection as WebSocket } from 'websocket';
import * as Bytes from '../../Bytes';
import { CDSISocket } from './CDSISocket';
import type { CDSSocketManagerBaseOptionsType } from './CDSSocketManagerBase';
import { CDSSocketManagerBase } from './CDSSocketManagerBase';
export type CDSIOptionsType = Readonly<{
mrenclave: string;
root: string;
}> &
CDSSocketManagerBaseOptionsType;
export class CDSI extends CDSSocketManagerBase<CDSISocket, CDSIOptionsType> {
private readonly mrenclave: Buffer;
private readonly trustedCaCert: Buffer;
constructor(options: CDSIOptionsType) {
super(options);
this.mrenclave = Buffer.from(Bytes.fromHex(options.mrenclave));
this.trustedCaCert = Buffer.from(options.root);
}
protected override getSocketUrl(): string {
const { mrenclave } = this.options;
return `${this.options.url}/v1/${mrenclave}/discovery`;
}
protected override createSocket(socket: WebSocket): CDSISocket {
return new CDSISocket({
logger: this.logger,
socket,
mrenclave: this.mrenclave,
trustedCaCert: this.trustedCaCert,
});
}
}

View file

@ -0,0 +1,100 @@
// Copyright 2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { Cds2Client } from '@signalapp/libsignal-client';
import { strictAssert } from '../../util/assert';
import { DAY } from '../../util/durations';
import { SignalService as Proto } from '../../protobuf';
import { CDSSocketBase, CDSSocketState } from './CDSSocketBase';
import type { CDSSocketBaseOptionsType } from './CDSSocketBase';
export type CDSISocketOptionsType = Readonly<{
mrenclave: Buffer;
trustedCaCert: Buffer;
}> &
CDSSocketBaseOptionsType;
export class CDSISocket extends CDSSocketBase<CDSISocketOptionsType> {
private privCdsClient: Cds2Client | undefined;
public override async handshake(): Promise<void> {
strictAssert(
this.state === CDSSocketState.Open,
'CDSI handshake called twice'
);
this.state = CDSSocketState.Handshake;
{
const { done, value: attestationMessage } =
await this.socketIterator.next();
strictAssert(!done, 'CDSI socket closed before handshake');
const earliestValidTimestamp = new Date(Date.now() - DAY);
strictAssert(
this.privCdsClient === undefined,
'CDSI handshake called twice'
);
this.privCdsClient = Cds2Client.new_NOT_FOR_PRODUCTION(
this.options.mrenclave,
this.options.trustedCaCert,
attestationMessage,
earliestValidTimestamp
);
}
this.socket.sendBytes(this.cdsClient.initialRequest());
{
const { done, value: message } = await this.socketIterator.next();
strictAssert(!done, 'CDSI socket expected handshake data');
this.cdsClient.completeHandshake(message);
}
this.state = CDSSocketState.Established;
}
protected override async sendRequest(
_version: number,
request: Buffer
): Promise<void> {
this.socket.sendBytes(this.cdsClient.establishedSend(request));
const { done, value: ciphertext } = await this.socketIterator.next();
strictAssert(!done, 'CDSISocket.sendRequest(): expected token message');
const message = await this.decryptResponse(ciphertext);
this.logger.info('CDSISocket.sendRequest(): processing token message');
const { token } = Proto.CDSClientResponse.decode(message);
strictAssert(token, 'CDSISocket.sendRequest(): expected token');
this.socket.sendBytes(
this.cdsClient.establishedSend(
Buffer.from(
Proto.CDSClientRequest.encode({
tokenAck: true,
}).finish()
)
)
);
}
protected override async decryptResponse(
ciphertext: Buffer
): Promise<Buffer> {
return this.cdsClient.establishedRecv(ciphertext);
}
//
// Private
//
private get cdsClient(): Cds2Client {
strictAssert(this.privCdsClient, 'CDSISocket did not start handshake');
return this.privCdsClient;
}
}

View file

@ -0,0 +1,260 @@
// Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import { EventEmitter } from 'events';
import { Readable } from 'stream';
import { noop } from 'lodash';
import type { connection as WebSocket } from 'websocket';
import Long from 'long';
import type { LoggerType } from '../../types/Logging';
import { strictAssert } from '../../util/assert';
import { dropNull } from '../../util/dropNull';
import { UUID_BYTE_SIZE } from '../../types/UUID';
import * as Bytes from '../../Bytes';
import { uuidToBytes, bytesToUuid } from '../../Crypto';
import { SignalService as Proto } from '../../protobuf';
import type {
CDSRequestOptionsType,
CDSResponseEntryType,
CDSResponseType,
} from './Types.d';
export type CDSSocketBaseOptionsType = Readonly<{
logger: LoggerType;
socket: WebSocket;
}>;
export type CDSSocketResponseType = Readonly<{
response: CDSResponseType;
retryAfterSecs?: number;
}>;
export enum CDSSocketState {
Open = 'Open',
Handshake = 'Handshake',
Established = 'Established',
Closed = 'Closed',
}
const MAX_E164_COUNT = 5000;
const E164_BYTE_SIZE = 8;
const TRIPLE_BYTE_SIZE = UUID_BYTE_SIZE * 2 + E164_BYTE_SIZE;
export abstract class CDSSocketBase<
Options extends CDSSocketBaseOptionsType = CDSSocketBaseOptionsType
> extends EventEmitter {
protected state = CDSSocketState.Open;
protected readonly socket: WebSocket;
protected readonly logger: LoggerType;
protected readonly socketIterator: AsyncIterator<Buffer>;
constructor(protected readonly options: Options) {
super();
// For easier access
this.logger = options.logger;
this.socket = options.socket;
this.socketIterator = this.iterateSocket();
}
public async close(code: number, reason: string): Promise<void> {
return this.socket.close(code, reason);
}
public async request({
e164s,
acis,
accessKeys,
}: CDSRequestOptionsType): Promise<CDSSocketResponseType> {
const log = this.logger;
strictAssert(
e164s.length < MAX_E164_COUNT,
'CDSSocket does not support paging. Use this for one-off requests'
);
strictAssert(
this.state === CDSSocketState.Established,
'CDS Connection not established'
);
const aciUakPairs = new Array<Uint8Array>();
let version: 1 | 2;
if (acis) {
strictAssert(accessKeys, 'accessKeys are required when acis are present');
strictAssert(
acis.length === accessKeys.length,
`Number of ACIs ${acis.length} is different ` +
`from number of access keys ${accessKeys.length}`
);
version = 2;
for (let i = 0; i < acis.length; i += 1) {
aciUakPairs.push(
Bytes.concatenate([
uuidToBytes(acis[i]),
Bytes.fromBase64(accessKeys[i]),
])
);
}
} else {
version = 1;
}
const request = Proto.CDSClientRequest.encode({
newE164s: Buffer.concat(
e164s.map(e164 => {
// Long.fromString handles numbers with or without a leading '+'
return new Uint8Array(Long.fromString(e164).toBytesBE());
})
),
aciUakPairs: Buffer.concat(aciUakPairs),
}).finish();
log.info(`CDSSocket.request(): sending version=${version} request`);
await this.sendRequest(version, Buffer.from(request));
const resultMap: Map<string, CDSResponseEntryType> = new Map();
let retryAfterSecs: number | undefined;
// eslint-disable-next-line no-constant-condition
while (true) {
// eslint-disable-next-line no-await-in-loop
const { done, value: ciphertext } = await this.socketIterator.next();
if (done) {
this.state = CDSSocketState.Closed;
break;
}
// eslint-disable-next-line no-await-in-loop
const message = await this.decryptResponse(ciphertext);
log.info('CDSSocket.request(): processing response message');
const response = Proto.CDSClientResponse.decode(message);
const newRetryAfterSecs = dropNull(response.retryAfterSecs);
decodeSingleResponse(resultMap, response);
if (newRetryAfterSecs) {
retryAfterSecs = Math.max(newRetryAfterSecs, retryAfterSecs ?? 0);
}
}
log.info('CDSSocket.request(): done');
return {
response: resultMap,
retryAfterSecs,
};
}
// Abstract methods
public abstract handshake(): Promise<void>;
protected abstract sendRequest(version: number, data: Buffer): Promise<void>;
protected abstract decryptResponse(ciphertext: Buffer): Promise<Buffer>;
// EventEmitter types
public override on(
type: 'close',
callback: (code: number, reason?: string) => void
): this;
public override on(type: 'error', callback: (error: Error) => void): this;
public override on(
type: string | symbol,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
listener: (...args: Array<any>) => void
): this {
return super.on(type, listener);
}
public override emit(type: 'close', code: number, reason?: string): boolean;
public override emit(type: 'error', error: Error): boolean;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public override emit(type: string | symbol, ...args: Array<any>): boolean {
return super.emit(type, ...args);
}
//
// Private
//
private iterateSocket(): AsyncIterator<Buffer> {
const stream = new Readable({ read: noop, objectMode: true });
this.socket.on('message', ({ type, binaryData }) => {
strictAssert(type === 'binary', 'Invalid CDS socket packet');
strictAssert(binaryData, 'Invalid CDS socket packet');
stream.push(binaryData);
});
this.socket.on('close', (code, reason) => {
if (code === 1000) {
stream.push(null);
} else {
stream.destroy(
new Error(`Socket closed with code ${code} and reason ${reason}`)
);
}
});
this.socket.on('error', (error: Error) => stream.destroy(error));
return stream[Symbol.asyncIterator]();
}
}
function decodeSingleResponse(
resultMap: Map<string, CDSResponseEntryType>,
response: Proto.CDSClientResponse
): void {
for (
let i = 0;
i < response.e164PniAciTriples.length;
i += TRIPLE_BYTE_SIZE
) {
const tripleBytes = response.e164PniAciTriples.slice(
i,
i + TRIPLE_BYTE_SIZE
);
strictAssert(
tripleBytes.length === TRIPLE_BYTE_SIZE,
'Invalid size of CDS response triple'
);
let offset = 0;
const e164Bytes = tripleBytes.slice(offset, offset + E164_BYTE_SIZE);
offset += E164_BYTE_SIZE;
const pniBytes = tripleBytes.slice(offset, offset + UUID_BYTE_SIZE);
offset += UUID_BYTE_SIZE;
const aciBytes = tripleBytes.slice(offset, offset + UUID_BYTE_SIZE);
offset += UUID_BYTE_SIZE;
const e164Long = Long.fromBytesBE(Array.from(e164Bytes));
if (e164Long.isZero()) {
continue;
}
const e164 = `+${e164Long.toString()}`;
const pni = bytesToUuid(pniBytes);
const aci = bytesToUuid(aciBytes);
resultMap.set(e164, { pni, aci });
}
}

View file

@ -0,0 +1,107 @@
// Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { connection as WebSocket } from 'websocket';
import pTimeout from 'p-timeout';
import type { AbortableProcess } from '../../util/AbortableProcess';
import * as durations from '../../util/durations';
import { getBasicAuth } from '../../util/getBasicAuth';
import { sleep } from '../../util/sleep';
import { SECOND } from '../../util/durations';
import type { CDSBaseOptionsType } from './CDSBase';
import { CDSBase } from './CDSBase';
import type { CDSSocketBase } from './CDSSocketBase';
import type {
CDSRequestOptionsType,
CDSResponseType,
CDSAuthType,
} from './Types.d';
import { connect as connectWebSocket } from '../WebSocket';
const REQUEST_TIMEOUT = 10 * SECOND;
export type CDSSocketManagerBaseOptionsType = Readonly<{
url: string;
certificateAuthority: string;
version: string;
}> &
CDSBaseOptionsType;
export abstract class CDSSocketManagerBase<
Socket extends CDSSocketBase,
Options extends CDSSocketManagerBaseOptionsType
> extends CDSBase<Options> {
private retryAfter?: number;
public async request(
options: CDSRequestOptionsType
): Promise<CDSResponseType> {
const log = this.logger;
if (this.retryAfter !== undefined) {
const delay = Math.max(0, this.retryAfter - Date.now());
log.info(`CDSSocketManager: waiting ${delay}ms before retrying`);
await sleep(delay);
}
const auth = await this.getAuth();
log.info('CDSSocketManager: connecting socket');
const socket = await this.connect(auth).getResult();
log.info('CDSSocketManager: connected socket');
try {
let { timeout = REQUEST_TIMEOUT } = options;
// Handshake
{
const start = Date.now();
await pTimeout(socket.handshake(), timeout);
const duration = Date.now() - start;
timeout = Math.max(timeout - duration, 0);
}
// Send request
const { response, retryAfterSecs = 0 } = await pTimeout(
socket.request(options),
timeout
);
if (retryAfterSecs > 0) {
this.retryAfter = Math.max(
this.retryAfter ?? Date.now(),
Date.now() + retryAfterSecs * durations.SECOND
);
}
return response;
} finally {
log.info('CDSSocketManager: closing socket');
socket.close(3000, 'Normal');
}
}
private connect(auth: CDSAuthType): AbortableProcess<Socket> {
return connectWebSocket<Socket>({
name: 'CDSSocket',
url: this.getSocketUrl(),
version: this.options.version,
proxyAgent: this.proxyAgent,
certificateAuthority: this.options.certificateAuthority,
extraHeaders: {
authorization: getBasicAuth(auth),
},
createResource: (socket: WebSocket): Socket => {
return this.createSocket(socket);
},
});
}
protected abstract getSocketUrl(): string;
protected abstract createSocket(socket: WebSocket): Socket;
}

View file

@ -0,0 +1,455 @@
// Copyright 2020-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable no-bitwise */
import pProps from 'p-props';
import { compact } from 'lodash';
import Long from 'long';
import { createVerify } from 'crypto';
import { pki } from 'node-forge';
import {
constantTimeEqual,
decryptAesGcm,
deriveSecrets,
encryptCdsDiscoveryRequest,
splitUuids,
} from '../../Crypto';
import { calculateAgreement, generateKeyPair } from '../../Curve';
import * as Bytes from '../../Bytes';
import { strictAssert } from '../../util/assert';
import { UUID } from '../../types/UUID';
import type { CDSBaseOptionsType } from './CDSBase';
import { CDSBase } from './CDSBase';
import type {
CDSRequestOptionsType,
CDSResponseType,
CDSAuthType,
CDSResponseEntryType,
} from './Types.d';
export type LegacyCDSPutAttestationResponseType = Readonly<{
attestations: Record<
string,
{
ciphertext: string;
iv: string;
quote: string;
serverEphemeralPublic: string;
serverStaticPublic: string;
signature: string;
signatureBody: string;
tag: string;
certificates: string;
}
>;
}>;
export type LegacyCDSPutAttestationResultType = Readonly<{
cookie?: string;
responseBody: LegacyCDSPutAttestationResponseType;
}>;
export type LegacyCDSDiscoveryResponseType = Readonly<{
requestId: Uint8Array;
iv: Uint8Array;
data: Uint8Array;
mac: Uint8Array;
}>;
export type LegacyCDSOptionsType = Readonly<{
directoryEnclaveId: string;
directoryTrustAnchor: string;
putAttestation: (
auth: CDSAuthType,
publicKey: Uint8Array
) => Promise<LegacyCDSPutAttestationResultType>;
fetchDiscoveryData: (
auth: CDSAuthType,
data: Record<string, unknown>,
cookie?: string
) => Promise<LegacyCDSDiscoveryResponseType>;
}> &
CDSBaseOptionsType;
type AttestationMapType = Readonly<{
cookie?: string;
attestations: Record<
string,
Readonly<{
clientKey: Uint8Array;
serverKey: Uint8Array;
requestId: Uint8Array;
}>
>;
}>;
type SgxConstantsType = {
SGX_FLAGS_INITTED: Long;
SGX_FLAGS_DEBUG: Long;
SGX_FLAGS_MODE64BIT: Long;
SGX_FLAGS_PROVISION_KEY: Long;
SGX_FLAGS_EINITTOKEN_KEY: Long;
SGX_FLAGS_RESERVED: Long;
SGX_XFRM_LEGACY: Long;
SGX_XFRM_AVX: Long;
SGX_XFRM_RESERVED: Long;
};
let sgxConstantCache: SgxConstantsType | null = null;
function makeLong(value: string): Long {
return Long.fromString(value);
}
function getSgxConstants() {
if (sgxConstantCache) {
return sgxConstantCache;
}
sgxConstantCache = {
SGX_FLAGS_INITTED: makeLong('x0000000000000001L'),
SGX_FLAGS_DEBUG: makeLong('x0000000000000002L'),
SGX_FLAGS_MODE64BIT: makeLong('x0000000000000004L'),
SGX_FLAGS_PROVISION_KEY: makeLong('x0000000000000004L'),
SGX_FLAGS_EINITTOKEN_KEY: makeLong('x0000000000000004L'),
SGX_FLAGS_RESERVED: makeLong('xFFFFFFFFFFFFFFC8L'),
SGX_XFRM_LEGACY: makeLong('x0000000000000003L'),
SGX_XFRM_AVX: makeLong('x0000000000000006L'),
SGX_XFRM_RESERVED: makeLong('xFFFFFFFFFFFFFFF8L'),
};
return sgxConstantCache;
}
export class LegacyCDS extends CDSBase<LegacyCDSOptionsType> {
public override async request({
e164s,
acis,
accessKeys,
}: CDSRequestOptionsType): Promise<CDSResponseType> {
strictAssert(!acis && !accessKeys, 'LegacyCDS does not support PNP');
const directoryAuth = await this.getAuth();
const attestationResult = await this.putAttestation(directoryAuth);
// Encrypt data for discovery
const data = await encryptCdsDiscoveryRequest(
attestationResult.attestations,
e164s
);
const { cookie } = attestationResult;
// Send discovery request
const discoveryResponse = await this.options.fetchDiscoveryData(
directoryAuth,
data,
cookie
);
const returnedAttestation = Object.values(
attestationResult.attestations
).find(at => constantTimeEqual(at.requestId, discoveryResponse.requestId));
if (!returnedAttestation) {
throw new Error('No known attestations returned from CDS');
}
// Decrypt discovery response
const decryptedDiscoveryData = decryptAesGcm(
returnedAttestation.serverKey,
discoveryResponse.iv,
Bytes.concatenate([discoveryResponse.data, discoveryResponse.mac])
);
// Process and return result
const uuids = splitUuids(decryptedDiscoveryData);
if (uuids.length !== e164s.length) {
throw new Error(
'Returned set of UUIDs did not match returned set of e164s!'
);
}
const result = new Map<string, CDSResponseEntryType>();
for (const [i, e164] of e164s.entries()) {
const uuid = uuids[i];
result.set(e164, {
aci: undefined,
pni: uuid ? UUID.cast(uuid) : undefined,
});
}
return result;
}
//
// Private
//
private async putAttestation(auth: CDSAuthType): Promise<AttestationMapType> {
const { privKey, pubKey } = generateKeyPair();
// Remove first "key type" byte from public key
const slicedPubKey = pubKey.slice(1);
// Do request
const { cookie, responseBody } = await this.options.putAttestation(
auth,
slicedPubKey
);
const attestationsLength = Object.keys(responseBody.attestations).length;
if (attestationsLength > 3) {
throw new Error(
'Got more than three attestations from the Contact Discovery Service'
);
}
if (attestationsLength < 1) {
throw new Error('Got no attestations from the Contact Discovery Service');
}
// Decode response
return {
cookie,
attestations: await pProps(
responseBody.attestations,
async attestation => {
const decoded = {
...attestation,
ciphertext: Bytes.fromBase64(attestation.ciphertext),
iv: Bytes.fromBase64(attestation.iv),
quote: Bytes.fromBase64(attestation.quote),
serverEphemeralPublic: Bytes.fromBase64(
attestation.serverEphemeralPublic
),
serverStaticPublic: Bytes.fromBase64(
attestation.serverStaticPublic
),
signature: Bytes.fromBase64(attestation.signature),
tag: Bytes.fromBase64(attestation.tag),
};
// Validate response
this.validateAttestationQuote(decoded);
validateAttestationSignatureBody(
JSON.parse(decoded.signatureBody),
attestation.quote
);
await this.validateAttestationSignature(
decoded.signature,
decoded.signatureBody,
decoded.certificates
);
// Derive key
const ephemeralToEphemeral = calculateAgreement(
decoded.serverEphemeralPublic,
privKey
);
const ephemeralToStatic = calculateAgreement(
decoded.serverStaticPublic,
privKey
);
const masterSecret = Bytes.concatenate([
ephemeralToEphemeral,
ephemeralToStatic,
]);
const publicKeys = Bytes.concatenate([
slicedPubKey,
decoded.serverEphemeralPublic,
decoded.serverStaticPublic,
]);
const [clientKey, serverKey] = deriveSecrets(
masterSecret,
publicKeys,
new Uint8Array(0)
);
// Decrypt ciphertext into requestId
const requestId = decryptAesGcm(
serverKey,
decoded.iv,
Bytes.concatenate([decoded.ciphertext, decoded.tag])
);
return {
clientKey,
serverKey,
requestId,
};
}
),
};
}
private async validateAttestationSignature(
signature: Uint8Array,
signatureBody: string,
certificates: string
) {
const CERT_PREFIX = '-----BEGIN CERTIFICATE-----';
const pem = compact(
certificates.split(CERT_PREFIX).map(match => {
if (!match) {
return null;
}
return `${CERT_PREFIX}${match}`;
})
);
if (pem.length < 2) {
throw new Error(
`validateAttestationSignature: Expect two or more entries; got ${pem.length}`
);
}
const verify = createVerify('RSA-SHA256');
verify.update(Buffer.from(Bytes.fromString(signatureBody)));
const isValid = verify.verify(pem[0], Buffer.from(signature));
if (!isValid) {
throw new Error('Validation of signature across signatureBody failed!');
}
const caStore = pki.createCaStore([this.options.directoryTrustAnchor]);
const chain = compact(pem.map(cert => pki.certificateFromPem(cert)));
const isChainValid = pki.verifyCertificateChain(caStore, chain);
if (!isChainValid) {
throw new Error('Validation of certificate chain failed!');
}
const leafCert = chain[0];
const fieldCN = leafCert.subject.getField('CN');
if (!fieldCN || fieldCN.value !== 'Intel SGX Attestation Report Signing') {
throw new Error('Leaf cert CN field had unexpected value');
}
const fieldO = leafCert.subject.getField('O');
if (!fieldO || fieldO.value !== 'Intel Corporation') {
throw new Error('Leaf cert O field had unexpected value');
}
const fieldL = leafCert.subject.getField('L');
if (!fieldL || fieldL.value !== 'Santa Clara') {
throw new Error('Leaf cert L field had unexpected value');
}
const fieldST = leafCert.subject.getField('ST');
if (!fieldST || fieldST.value !== 'CA') {
throw new Error('Leaf cert ST field had unexpected value');
}
const fieldC = leafCert.subject.getField('C');
if (!fieldC || fieldC.value !== 'US') {
throw new Error('Leaf cert C field had unexpected value');
}
}
private validateAttestationQuote({
serverStaticPublic,
quote: quoteBytes,
}: {
serverStaticPublic: Uint8Array;
quote: Uint8Array;
}): void {
const SGX_CONSTANTS = getSgxConstants();
const quote = Buffer.from(quoteBytes);
const quoteVersion = quote.readInt16LE(0) & 0xffff;
if (quoteVersion < 0 || quoteVersion > 2) {
throw new Error(`Unknown version ${quoteVersion}`);
}
const miscSelect = quote.slice(64, 64 + 4);
if (!miscSelect.every(byte => byte === 0)) {
throw new Error('Quote miscSelect invalid!');
}
const reserved1 = quote.slice(68, 68 + 28);
if (!reserved1.every(byte => byte === 0)) {
throw new Error('Quote reserved1 invalid!');
}
const flags = Long.fromBytesLE(
Array.from(quote.slice(96, 96 + 8).values())
);
if (
flags.and(SGX_CONSTANTS.SGX_FLAGS_RESERVED).notEquals(0) ||
flags.and(SGX_CONSTANTS.SGX_FLAGS_INITTED).equals(0) ||
flags.and(SGX_CONSTANTS.SGX_FLAGS_MODE64BIT).equals(0)
) {
throw new Error(`Quote flags invalid ${flags.toString()}`);
}
const xfrm = Long.fromBytesLE(
Array.from(quote.slice(104, 104 + 8).values())
);
if (xfrm.and(SGX_CONSTANTS.SGX_XFRM_RESERVED).notEquals(0)) {
throw new Error(`Quote xfrm invalid ${xfrm}`);
}
const mrenclave = quote.slice(112, 112 + 32);
const enclaveIdBytes = Bytes.fromHex(this.options.directoryEnclaveId);
if (mrenclave.compare(enclaveIdBytes) !== 0) {
throw new Error('Quote mrenclave invalid!');
}
const reserved2 = quote.slice(144, 144 + 32);
if (!reserved2.every(byte => byte === 0)) {
throw new Error('Quote reserved2 invalid!');
}
const reportData = quote.slice(368, 368 + 64);
const serverStaticPublicBytes = serverStaticPublic;
if (
!reportData.every((byte, index) => {
if (index >= 32) {
return byte === 0;
}
return byte === serverStaticPublicBytes[index];
})
) {
throw new Error('Quote report_data invalid!');
}
const reserved3 = quote.slice(208, 208 + 96);
if (!reserved3.every(byte => byte === 0)) {
throw new Error('Quote reserved3 invalid!');
}
const reserved4 = quote.slice(308, 308 + 60);
if (!reserved4.every(byte => byte === 0)) {
throw new Error('Quote reserved4 invalid!');
}
const signatureLength = quote.readInt32LE(432) >>> 0;
if (signatureLength !== quote.byteLength - 436) {
throw new Error(`Bad signatureLength ${signatureLength}`);
}
// const signature = quote.slice(436, 436 + signatureLength);
}
}
function validateAttestationSignatureBody(
signatureBody: {
timestamp: string;
version: number;
isvEnclaveQuoteBody: string;
isvEnclaveQuoteStatus: string;
},
encodedQuote: string
) {
// Parse timestamp as UTC
const { timestamp } = signatureBody;
const utcTimestamp = timestamp.endsWith('Z') ? timestamp : `${timestamp}Z`;
const signatureTime = new Date(utcTimestamp).getTime();
const now = Date.now();
if (signatureBody.version !== 3) {
throw new Error('Attestation signature invalid version!');
}
if (!encodedQuote.startsWith(signatureBody.isvEnclaveQuoteBody)) {
throw new Error('Attestion signature mismatches quote!');
}
if (signatureBody.isvEnclaveQuoteStatus !== 'OK') {
throw new Error('Attestation signature status not "OK"!');
}
if (signatureTime < now - 24 * 60 * 60 * 1000) {
throw new Error('Attestation signature timestamp older than 24 hours!');
}
}

23
ts/textsecure/cds/Types.d.ts vendored Normal file
View file

@ -0,0 +1,23 @@
// Copyright 2021-2022 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
import type { UUIDStringType } from '../../types/UUID';
export type CDSAuthType = Readonly<{
username: string;
password: string;
}>;
export type CDSResponseEntryType = Readonly<{
aci: UUIDStringType | undefined;
pni: UUIDStringType | undefined;
}>;
export type CDSResponseType = ReadonlyMap<string, CDSResponseEntryType>;
export type CDSRequestOptionsType = Readonly<{
e164s: ReadonlyArray<string>;
acis?: ReadonlyArray<UUIDStringType>;
accessKeys?: ReadonlyArray<string>;
timeout?: number;
}>;

View file

@ -11,11 +11,55 @@ export type ConfigRequiredStringType = z.infer<
typeof configRequiredStringSchema
>;
const configOptionalUnknownSchema = configRequiredStringSchema.or(z.unknown());
const configOptionalStringSchema = configRequiredStringSchema.or(z.undefined());
export type configOptionalStringType = z.infer<
typeof configOptionalStringSchema
>;
const directoryV1ConfigSchema = z.object({
directoryVersion: z.literal(1),
directoryEnclaveId: configRequiredStringSchema,
directoryTrustAnchor: configRequiredStringSchema,
directoryUrl: configRequiredStringSchema,
});
const directoryV2ConfigSchema = z.object({
directoryVersion: z.literal(2),
directoryV2CodeHashes: z.array(z.string().nonempty()),
directoryV2PublicKey: configRequiredStringSchema,
directoryV2Url: configRequiredStringSchema,
});
const directoryV3ConfigSchema = z.object({
directoryVersion: z.literal(3),
directoryV3Url: configRequiredStringSchema,
directoryV3MRENCLAVE: configRequiredStringSchema,
directoryV3Root: configRequiredStringSchema,
});
export const directoryConfigSchema = z
.object({
// Unknown defaults
directoryEnclaveId: configOptionalUnknownSchema,
directoryTrustAnchor: configOptionalUnknownSchema,
directoryUrl: configOptionalUnknownSchema,
directoryV2CodeHashes: configOptionalUnknownSchema,
directoryV2PublicKey: configOptionalUnknownSchema,
directoryV2Url: configOptionalUnknownSchema,
directoryV3Url: configOptionalUnknownSchema,
directoryV3MRENCLAVE: configOptionalUnknownSchema,
directoryV3Root: configOptionalUnknownSchema,
})
.and(
directoryV1ConfigSchema
.or(directoryV2ConfigSchema)
.or(directoryV3ConfigSchema)
);
export type DirectoryConfigType = z.infer<typeof directoryConfigSchema>;
export const rendererConfigSchema = z.object({
appInstance: configOptionalStringSchema,
appStartInitialSpellcheckSetting: z.boolean(),
@ -26,13 +70,6 @@ export const rendererConfigSchema = z.object({
certificateAuthority: configRequiredStringSchema,
contentProxyUrl: configRequiredStringSchema,
crashDumpsPath: configRequiredStringSchema,
directoryEnclaveId: configOptionalStringSchema,
directoryTrustAnchor: configOptionalStringSchema,
directoryUrl: configOptionalStringSchema,
directoryV2CodeHashes: z.array(z.string().nonempty()).or(z.undefined()),
directoryV2PublicKey: configOptionalStringSchema,
directoryV2Url: configOptionalStringSchema,
directoryVersion: z.number(),
enableCI: z.boolean(),
environment: environmentSchema,
homePath: configRequiredStringSchema,
@ -51,6 +88,7 @@ export const rendererConfigSchema = z.object({
updatesUrl: configRequiredStringSchema,
userDataPath: configRequiredStringSchema,
version: configRequiredStringSchema,
directoryConfig: directoryConfigSchema,
// Only used by main window
isMainWindowFullScreen: z.boolean(),

1
ts/window.d.ts vendored
View file

@ -233,7 +233,6 @@ declare global {
preloadStartTime: number;
preloadEndTime: number;
preloadConnectTime: number;
removeSetupMenuItems: () => unknown;
showPermissionsPopup: (

View file

@ -109,16 +109,17 @@ window.isAfterVersion = (toCheck, baseVersion) => {
window.setBadgeCount = count => ipc.send('set-badge-count', count);
let preloadConnectTime = 0;
window.logAuthenticatedConnect = () => {
if (window.preloadConnectTime === 0) {
window.preloadConnectTime = Date.now();
if (preloadConnectTime === 0) {
preloadConnectTime = Date.now();
}
};
window.logAppLoadedEvent = ({ processedCount }) =>
ipc.send('signal-app-loaded', {
preloadTime: window.preloadEndTime - window.preloadStartTime,
connectTime: window.preloadConnectTime - window.preloadEndTime,
connectTime: preloadConnectTime - window.preloadEndTime,
processedCount,
});

View file

@ -29,13 +29,7 @@ window.WebAPI = window.textsecure.WebAPI.initialize({
url: config.serverUrl,
storageUrl: config.storageUrl,
updatesUrl: config.updatesUrl,
directoryVersion: config.directoryVersion,
directoryUrl: config.directoryUrl,
directoryEnclaveId: config.directoryEnclaveId,
directoryTrustAnchor: config.directoryTrustAnchor,
directoryV2Url: config.directoryV2Url,
directoryV2PublicKey: config.directoryV2PublicKey,
directoryV2CodeHashes: config.directoryV2CodeHashes,
directoryConfig: config.directoryConfig,
cdnUrlObject: {
0: config.cdnUrl0,
2: config.cdnUrl2,

View file

@ -1674,7 +1674,15 @@
"@react-spring/shared" "~9.4.5"
"@react-spring/types" "~9.4.5"
"@signalapp/libsignal-client@0.16.0", "@signalapp/libsignal-client@^0.16.0":
"@signalapp/libsignal-client@0.17.0":
version "0.17.0"
resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.17.0.tgz#ffe6763d80f56148b45192bca29deb16f9a0aea8"
integrity sha512-O5bd/BURWnybh6KhRYSO3NmNb1/oySu5yJx5ELy3QsfeFvpMnTkr0/PcXd0MCvRiaoN+/a0TsnywMO43t6Nxsw==
dependencies:
node-gyp-build "^4.2.3"
uuid "^8.3.0"
"@signalapp/libsignal-client@^0.16.0":
version "0.16.0"
resolved "https://registry.yarnpkg.com/@signalapp/libsignal-client/-/libsignal-client-0.16.0.tgz#7acba54b7ba05f513cdcf7f555efa1ccc6ce0145"
integrity sha512-/5EzlAcQoQReDomqV6VTtin5tvqvdUxoe8knSiz+L1kcLSlHA0So0zTR9WAdfQQ69t4q69vhaS4pu5yVI28YHA==