Additional protocol changes for CDS v2
This commit is contained in:
parent
b35d330c0a
commit
bb15cfc622
5 changed files with 66 additions and 20 deletions
|
@ -5,8 +5,8 @@
|
||||||
"directoryEnclaveId": "c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15",
|
"directoryEnclaveId": "c98e00a4e3ff977a56afefe7362a27e4961e4f19e211febfbb19b897e6b80b15",
|
||||||
"directoryTrustAnchor": "-----BEGIN CERTIFICATE-----\nMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV\nBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV\nBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0\nYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy\nMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL\nU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD\nDCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G\nCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR+tXc8u1EtJzLA10Feu1Wg+p7e\nLmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh\nrgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT\nL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe\nNpEJUmg4ktal4qgIAxk+QHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ\nbyinkNndn+Bgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H\nafuVeLHcDsRp6hol4P+ZFIhu8mmbI1u0hH3W/0C2BuYXB5PC+5izFFh/nP0lc2Lf\n6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM\nRoOaX4AS+909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX\nMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50\nL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW\nBBR4Q3t2pn680K9+QjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9+Qjfr\nNXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq\nhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir\nIEqucRiJSSx+HjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi+ripMtPZ\nsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi\nzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra\nUd4APK0wZTGtfPXU7w+IBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA\n152Sq049ESDz+1rRGc2NVEqh1KaGXmtXvqxXcTB+Ljy5Bw2ke0v8iGngFBPqCTVB\n3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5+xmBc388v9Dm21HGfcC8O\nDD+gT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R+mJTLwPXVMrv\nDaVzWh5aiEx+idkSGMnX\n-----END CERTIFICATE-----\n",
|
"directoryTrustAnchor": "-----BEGIN CERTIFICATE-----\nMIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV\nBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV\nBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0\nYXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy\nMzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL\nU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD\nDCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G\nCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR+tXc8u1EtJzLA10Feu1Wg+p7e\nLmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh\nrgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT\nL/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe\nNpEJUmg4ktal4qgIAxk+QHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ\nbyinkNndn+Bgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H\nafuVeLHcDsRp6hol4P+ZFIhu8mmbI1u0hH3W/0C2BuYXB5PC+5izFFh/nP0lc2Lf\n6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM\nRoOaX4AS+909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX\nMFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50\nL0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW\nBBR4Q3t2pn680K9+QjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9+Qjfr\nNXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq\nhkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir\nIEqucRiJSSx+HjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi+ripMtPZ\nsFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi\nzLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra\nUd4APK0wZTGtfPXU7w+IBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA\n152Sq049ESDz+1rRGc2NVEqh1KaGXmtXvqxXcTB+Ljy5Bw2ke0v8iGngFBPqCTVB\n3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5+xmBc388v9Dm21HGfcC8O\nDD+gT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R+mJTLwPXVMrv\nDaVzWh5aiEx+idkSGMnX\n-----END CERTIFICATE-----\n",
|
||||||
"directoryV2Url": "https://cdsh.staging.signal.org",
|
"directoryV2Url": "https://cdsh.staging.signal.org",
|
||||||
"directoryV2PublicKey": "052fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74",
|
"directoryV2PublicKey": "2fe57da347cd62431528daac5fbb290730fff684afc4cfc2ed90995f58cb3b74",
|
||||||
"directoryV2CodeHash": "ec31a51880d19a5e9e0fed404740c1a3ff53a553125564b745acce475f0fded8",
|
"directoryV2CodeHash": "ec58c0d7561de8d5657f3a4b22a635eaa305204e9359dcc80a99dfd0c5f1cbf2",
|
||||||
"cdn": {
|
"cdn": {
|
||||||
"0": "https://cdn-staging.signal.org",
|
"0": "https://cdn-staging.signal.org",
|
||||||
"2": "https://cdn2-staging.signal.org"
|
"2": "https://cdn2-staging.signal.org"
|
||||||
|
|
11
ts/Curve.ts
11
ts/Curve.ts
|
@ -91,6 +91,13 @@ export function createKeyPair(incomingKey: Uint8Array): KeyPairType {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function prefixPublicKey(pubKey: Uint8Array): Uint8Array {
|
||||||
|
return Bytes.concatenate([
|
||||||
|
new Uint8Array([0x05]),
|
||||||
|
validatePubKeyFormat(pubKey),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
export function calculateAgreement(
|
export function calculateAgreement(
|
||||||
pubKey: Uint8Array,
|
pubKey: Uint8Array,
|
||||||
privKey: Uint8Array
|
privKey: Uint8Array
|
||||||
|
@ -98,9 +105,7 @@ export function calculateAgreement(
|
||||||
const privKeyBuffer = Buffer.from(privKey);
|
const privKeyBuffer = Buffer.from(privKey);
|
||||||
|
|
||||||
const pubKeyObj = client.PublicKey.deserialize(
|
const pubKeyObj = client.PublicKey.deserialize(
|
||||||
Buffer.from(
|
Buffer.from(prefixPublicKey(pubKey))
|
||||||
Bytes.concatenate([new Uint8Array([0x05]), validatePubKeyFormat(pubKey)])
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
const privKeyObj = client.PrivateKey.deserialize(privKeyBuffer);
|
const privKeyObj = client.PrivateKey.deserialize(privKeyBuffer);
|
||||||
const sharedSecret = privKeyObj.agree(pubKeyObj);
|
const sharedSecret = privKeyObj.agree(pubKeyObj);
|
||||||
|
|
|
@ -20,8 +20,22 @@ enum State {
|
||||||
Closed,
|
Closed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CDSRequestOptionsType = Readonly<{
|
||||||
|
e164s: ReadonlyArray<string>;
|
||||||
|
auth: CDSAuthType;
|
||||||
|
timeout?: number;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
export type CDSAuthType = Readonly<{
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
const HANDSHAKE_TIMEOUT = 10 * durations.SECOND;
|
const HANDSHAKE_TIMEOUT = 10 * durations.SECOND;
|
||||||
const REQUEST_TIMEOUT = 10 * durations.SECOND;
|
const REQUEST_TIMEOUT = 10 * durations.SECOND;
|
||||||
|
const VERSION = new Uint8Array([0x01]);
|
||||||
|
const USERNAME_LENGTH = 32;
|
||||||
|
const PASSWORD_LENGTH = 31;
|
||||||
|
|
||||||
export class CDSSocket extends EventEmitter {
|
export class CDSSocket extends EventEmitter {
|
||||||
private state = State.Handshake;
|
private state = State.Handshake;
|
||||||
|
@ -82,19 +96,30 @@ export class CDSSocket extends EventEmitter {
|
||||||
|
|
||||||
public async request({
|
public async request({
|
||||||
e164s,
|
e164s,
|
||||||
|
auth,
|
||||||
timeout = REQUEST_TIMEOUT,
|
timeout = REQUEST_TIMEOUT,
|
||||||
}: {
|
}: CDSRequestOptionsType): Promise<ReadonlyArray<UUIDStringType | null>> {
|
||||||
e164s: ReadonlyArray<string>;
|
|
||||||
timeout?: number;
|
|
||||||
}): Promise<ReadonlyArray<UUIDStringType | null>> {
|
|
||||||
await this.finishedHandshake;
|
await this.finishedHandshake;
|
||||||
strictAssert(
|
strictAssert(
|
||||||
this.state === State.Established,
|
this.state === State.Established,
|
||||||
'Connection not established'
|
'Connection not established'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const username = Bytes.fromString(auth.username);
|
||||||
|
const password = Bytes.fromString(auth.password);
|
||||||
|
strictAssert(
|
||||||
|
username.length === USERNAME_LENGTH,
|
||||||
|
'Invalid username length'
|
||||||
|
);
|
||||||
|
strictAssert(
|
||||||
|
password.length === PASSWORD_LENGTH,
|
||||||
|
'Invalid password length'
|
||||||
|
);
|
||||||
|
|
||||||
const request = Bytes.concatenate([
|
const request = Bytes.concatenate([
|
||||||
new Uint8Array([0x01]),
|
VERSION,
|
||||||
|
username,
|
||||||
|
password,
|
||||||
...e164s.map(e164 => {
|
...e164s.map(e164 => {
|
||||||
// Long.fromString handles numbers with or without a leading '+'
|
// Long.fromString handles numbers with or without a leading '+'
|
||||||
return new Uint8Array(Long.fromString(e164).toBytesBE());
|
return new Uint8Array(Long.fromString(e164).toBytesBE());
|
||||||
|
|
|
@ -6,10 +6,12 @@ import { HsmEnclaveClient, PublicKey } from '@signalapp/signal-client';
|
||||||
import type { connection as WebSocket } from 'websocket';
|
import type { connection as WebSocket } from 'websocket';
|
||||||
|
|
||||||
import * as Bytes from '../Bytes';
|
import * as Bytes from '../Bytes';
|
||||||
|
import { prefixPublicKey } from '../Curve';
|
||||||
import type { AbortableProcess } from '../util/AbortableProcess';
|
import type { AbortableProcess } from '../util/AbortableProcess';
|
||||||
import * as log from '../logging/log';
|
import * as log from '../logging/log';
|
||||||
import type { UUIDStringType } from '../types/UUID';
|
import type { UUIDStringType } from '../types/UUID';
|
||||||
import { CDSSocket } from './CDSSocket';
|
import { CDSSocket } from './CDSSocket';
|
||||||
|
import type { CDSRequestOptionsType } from './CDSSocket';
|
||||||
import { connect as connectWebSocket } from './WebSocket';
|
import { connect as connectWebSocket } from './WebSocket';
|
||||||
|
|
||||||
export type CDSSocketManagerOptionsType = Readonly<{
|
export type CDSSocketManagerOptionsType = Readonly<{
|
||||||
|
@ -30,7 +32,7 @@ export class CDSSocketManager {
|
||||||
|
|
||||||
constructor(private readonly options: CDSSocketManagerOptionsType) {
|
constructor(private readonly options: CDSSocketManagerOptionsType) {
|
||||||
this.publicKey = PublicKey.deserialize(
|
this.publicKey = PublicKey.deserialize(
|
||||||
Buffer.from(Bytes.fromHex(options.publicKey))
|
Buffer.from(prefixPublicKey(Bytes.fromHex(options.publicKey)))
|
||||||
);
|
);
|
||||||
this.codeHash = Buffer.from(Bytes.fromHex(options.codeHash));
|
this.codeHash = Buffer.from(Bytes.fromHex(options.codeHash));
|
||||||
if (options.proxyUrl) {
|
if (options.proxyUrl) {
|
||||||
|
@ -38,19 +40,15 @@ export class CDSSocketManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async request({
|
public async request(
|
||||||
e164s,
|
options: CDSRequestOptionsType
|
||||||
timeout,
|
): Promise<ReadonlyArray<UUIDStringType | null>> {
|
||||||
}: {
|
|
||||||
e164s: ReadonlyArray<string>;
|
|
||||||
timeout?: number;
|
|
||||||
}): Promise<ReadonlyArray<UUIDStringType | null>> {
|
|
||||||
log.info('CDSSocketManager: connecting socket');
|
log.info('CDSSocketManager: connecting socket');
|
||||||
const socket = await this.connect().getResult();
|
const socket = await this.connect().getResult();
|
||||||
log.info('CDSSocketManager: connected socket');
|
log.info('CDSSocketManager: connected socket');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await socket.request({ e164s, timeout });
|
return await socket.request(options);
|
||||||
} finally {
|
} finally {
|
||||||
log.info('CDSSocketManager: closing socket');
|
log.info('CDSSocketManager: closing socket');
|
||||||
socket.close(3000, 'Normal');
|
socket.close(3000, 'Normal');
|
||||||
|
|
|
@ -504,6 +504,7 @@ const URL_CALLS = {
|
||||||
deliveryCert: 'v1/certificate/delivery',
|
deliveryCert: 'v1/certificate/delivery',
|
||||||
devices: 'v1/devices',
|
devices: 'v1/devices',
|
||||||
directoryAuth: 'v1/directory/auth',
|
directoryAuth: 'v1/directory/auth',
|
||||||
|
directoryAuthV2: 'v2/directory/auth',
|
||||||
discovery: 'v1/discovery',
|
discovery: 'v1/discovery',
|
||||||
getGroupAvatarUpload: 'v1/groups/avatar/form',
|
getGroupAvatarUpload: 'v1/groups/avatar/form',
|
||||||
getGroupCredentials: 'v1/certificate/group',
|
getGroupCredentials: 'v1/certificate/group',
|
||||||
|
@ -556,6 +557,7 @@ const WEBSOCKET_CALLS = new Set<keyof typeof URL_CALLS>([
|
||||||
|
|
||||||
// Directory
|
// Directory
|
||||||
'directoryAuth',
|
'directoryAuth',
|
||||||
|
'directoryAuthV2',
|
||||||
|
|
||||||
// Storage
|
// Storage
|
||||||
'storageToken',
|
'storageToken',
|
||||||
|
@ -2475,6 +2477,17 @@ export function initialize({
|
||||||
})) as { username: string; password: string };
|
})) 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({
|
function validateAttestationQuote({
|
||||||
serverStaticPublic,
|
serverStaticPublic,
|
||||||
quote: quoteBytes,
|
quote: quoteBytes,
|
||||||
|
@ -2864,7 +2877,12 @@ export function initialize({
|
||||||
async function getUuidsForE164sV2(
|
async function getUuidsForE164sV2(
|
||||||
e164s: ReadonlyArray<string>
|
e164s: ReadonlyArray<string>
|
||||||
): Promise<Dictionary<UUIDStringType | null>> {
|
): Promise<Dictionary<UUIDStringType | null>> {
|
||||||
const uuids = await cdsSocketManager.request({ e164s });
|
const auth = await getDirectoryAuthV2();
|
||||||
|
|
||||||
|
const uuids = await cdsSocketManager.request({
|
||||||
|
auth,
|
||||||
|
e164s,
|
||||||
|
});
|
||||||
|
|
||||||
return zipObject(e164s, uuids);
|
return zipObject(e164s, uuids);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue