Remove LegacyCDS

This commit is contained in:
Fedor Indutny 2022-10-26 16:17:14 -07:00 committed by GitHub
parent 13785a0936
commit 7f0a66847b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 28 additions and 1124 deletions

View file

@ -40,14 +40,7 @@ import * as linkPreviewFetch from '../linkPreviews/linkPreviewFetch';
import { isBadgeImageFileUrlValid } from '../badges/isBadgeImageFileUrlValid';
import { SocketManager } from './SocketManager';
import type {
CDSAuthType,
CDSRequestOptionsType,
CDSResponseType,
} from './cds/Types.d';
import type { CDSBase } from './cds/CDSBase';
import { LegacyCDS } from './cds/LegacyCDS';
import type { LegacyCDSPutAttestationResponseType } from './cds/LegacyCDS';
import type { CDSAuthType, CDSResponseType } from './cds/Types.d';
import { CDSI } from './cds/CDSI';
import type WebSocketResource from './WebsocketResources';
import { SignalService as Proto } from '../protobuf';
@ -63,12 +56,6 @@ import type {
import { handleStatusCode, translateError } from './Utils';
import * as log from '../logging/log';
import { maybeParseUrl } from '../util/url';
import {
ToastInternalError,
ToastInternalErrorKind,
} from '../components/ToastInternalError';
import { showToast } from '../util/showToast';
import { isProduction } from '../util/version';
// Note: this will break some code that expects to be able to use err.response when a
// web request fails, because it will force it to text. But it is very useful for
@ -738,8 +725,6 @@ export type CdsLookupOptionsType = Readonly<{
acis?: ReadonlyArray<UUIDStringType>;
accessKeys?: ReadonlyArray<string>;
returnAcisWithoutUaks?: boolean;
isLegacy: boolean;
isMirroring: boolean;
}>;
type GetProfileCommonOptionsType = Readonly<
@ -1126,117 +1111,25 @@ export function initialize({
socketManager.authenticate({ username, password });
}
const {
directoryType,
directoryUrl,
directoryEnclaveId,
directoryTrustAnchor,
} = directoryConfig;
const { directoryUrl, directoryMRENCLAVE } = directoryConfig;
let legacyCDS: LegacyCDS | undefined;
let cds: CDSBase;
if (directoryType === 'legacy' || directoryType === 'mirrored-cdsi') {
legacyCDS = new LegacyCDS({
logger: log,
directoryEnclaveId,
directoryTrustAnchor,
proxyUrl,
const cds = new CDSI({
logger: log,
proxyUrl,
async putAttestation(auth, publicKey) {
const data = JSON.stringify({
clientPublic: Bytes.toBase64(publicKey),
iasVersion: 4,
});
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,
proxyUrl,
responseType: 'jsonwithdetails',
data,
timeout: 30000,
version,
})) as JSONWithDetailsType<LegacyCDSPutAttestationResponseType>;
url: directoryUrl,
mrenclave: directoryMRENCLAVE,
certificateAuthority,
version,
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,
proxyUrl,
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;
},
});
if (directoryType === 'legacy') {
cds = legacyCDS;
}
}
if (directoryType === 'cdsi' || directoryType === 'mirrored-cdsi') {
const { directoryCDSIUrl, directoryCDSIMRENCLAVE } = directoryConfig;
cds = new CDSI({
logger: log,
proxyUrl,
url: directoryCDSIUrl,
mrenclave: directoryCDSIMRENCLAVE,
certificateAuthority,
version,
async getAuth() {
return (await _ajax({
call: 'directoryAuthV2',
httpType: 'GET',
responseType: 'json',
})) as CDSAuthType;
},
});
}
async getAuth() {
return (await _ajax({
call: 'directoryAuthV2',
httpType: 'GET',
responseType: 'json',
})) as CDSAuthType;
},
});
let fetchForLinkPreviews: linkPreviewFetch.FetchFn;
if (proxyUrl) {
@ -2936,74 +2829,18 @@ export function initialize({
return socketManager.getProvisioningResource(handler);
}
async function mirroredCdsLookup(
requestOptions: CDSRequestOptionsType,
expectedMapPromise: Promise<CDSResponseType>
): Promise<void> {
try {
log.info('cdsLookup: sending mirrored request');
const actualMap = await cds.request(requestOptions);
const expectedMap = await expectedMapPromise;
let matched = 0;
let warnings = 0;
for (const [e164, { aci }] of actualMap) {
if (!aci) {
continue;
}
const expectedACI = expectedMap.get(e164)?.aci;
if (expectedACI === aci) {
matched += 1;
} else {
warnings += 1;
log.warn(
`cdsLookup: mirrored request has aci=${aci} for ${e164}, while ` +
`expected aci=${expectedACI}`
);
}
}
if (warnings !== 0 && !isProduction(window.getVersion())) {
log.info('cdsLookup: showing error toast');
showToast(ToastInternalError, {
kind: ToastInternalErrorKind.CDSMirroringError,
onShowDebugLog: () => window.showDebugLog(),
});
}
log.info(`cdsLookup: mirrored request success, matched=${matched}`);
} catch (error) {
log.error('cdsLookup: mirrored request error', toLogFormat(error));
}
}
async function cdsLookup({
e164s,
acis = [],
accessKeys = [],
returnAcisWithoutUaks,
isLegacy,
isMirroring,
}: CdsLookupOptionsType): Promise<CDSResponseType> {
const requestOptions = {
return cds.request({
e164s,
acis,
accessKeys,
returnAcisWithoutUaks,
};
if (!isLegacy || !legacyCDS) {
return cds.request(requestOptions);
}
const legacyRequest = legacyCDS.request(requestOptions);
if (legacyCDS !== cds && isMirroring) {
// Intentionally not awaiting
mirroredCdsLookup(requestOptions, legacyRequest);
}
return legacyRequest;
});
}
}
}

View file

@ -1,459 +0,0 @@
// 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 { 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,
}: CDSRequestOptionsType): Promise<CDSResponseType> {
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: uuid ? UUID.cast(uuid) : undefined,
pni: 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);
}
}
const ALLOWED_ADVISORIES = new Set(['INTEL-SA-00334', 'INTEL-SA-00615']);
function validateAttestationSignatureBody(
signatureBody: {
timestamp: string;
version: number;
isvEnclaveQuoteBody: string;
isvEnclaveQuoteStatus: string;
advisoryIDs: ReadonlyArray<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 !== 4) {
throw new Error('Attestation signature invalid version!');
}
if (!encodedQuote.startsWith(signatureBody.isvEnclaveQuoteBody)) {
throw new Error('Attestion signature mismatches quote!');
}
if (signatureBody.isvEnclaveQuoteStatus !== 'SW_HARDENING_NEEDED') {
throw new Error('Attestation signature status not "SW_HARDENING_NEEDED"!');
}
if (!signatureBody.advisoryIDs.every(id => ALLOWED_ADVISORIES.has(id))) {
throw new Error('Attestation advisory ids are incorrect');
}
if (signatureBody.advisoryIDs.length > ALLOWED_ADVISORIES.size) {
throw new Error('Attestation advisory count is incorrect');
}
if (signatureTime < now - 24 * 60 * 60 * 1000) {
throw new Error('Attestation signature timestamp older than 24 hours!');
}
}