Skip to content
This repository was archived by the owner on Feb 8, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion packages/teleterm/src/mainProcess/resolveNetworkAddress.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,15 @@ const TCP_PORT_MATCH = /\{CONNECT_GRPC_PORT:\s(\d+)}/;
// transport method.
const UDS_MATCH = /\{CONNECT_GRPC_PORT:/;

/**
* Waits for the process to start a gRPC server and log the address used by the gRPC server.
*
* @return {Promise<string>} The address used by the gRPC server started from the process.
*/
export async function resolveNetworkAddress(
requestedAddress: string,
process: ChildProcess,
timeoutMs = 10_000 // 10s
timeoutMs = 15_000 // 15s; needs to be larger than other timeouts in the processes.
): Promise<string> {
const protocol = new URL(requestedAddress).protocol;

Expand Down
58 changes: 49 additions & 9 deletions packages/teleterm/src/preload.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
import { contextBridge } from 'electron';
import { ChannelCredentials } from '@grpc/grpc-js';

import createTshClient from 'teleterm/services/tshd/createClient';
import createMainProcessClient from 'teleterm/mainProcess/mainProcessClient';
import createLoggerService from 'teleterm/services/logger';
import PreloadLogger from 'teleterm/logger';

import Logger from 'teleterm/logger';
import { createPtyService } from 'teleterm/services/pty/ptyService';
import { getClientCredentials } from 'teleterm/services/grpcCredentials';

import { ElectronGlobals } from './types';
import {
GrpcCertName,
createClientCredentials,
createInsecureClientCredentials,
generateAndSaveGrpcCert,
readGrpcCert,
shouldEncryptConnection,
} from 'teleterm/services/grpcCredentials';
import { ElectronGlobals, RuntimeSettings } from 'teleterm/types';

const mainProcessClient = createMainProcessClient();
const runtimeSettings = mainProcessClient.getRuntimeSettings();
Expand All @@ -18,16 +24,18 @@ const loggerService = createLoggerService({
name: 'renderer',
});

PreloadLogger.init(loggerService);
Logger.init(loggerService);

contextBridge.exposeInMainWorld('loggerService', loggerService);

contextBridge.exposeInMainWorld('electron', getElectronGlobals());

async function getElectronGlobals(): Promise<ElectronGlobals> {
const [addresses, credentials] = await Promise.all([
mainProcessClient.getResolvedChildProcessAddresses(),
getClientCredentials(runtimeSettings),
createGrpcCredentials(runtimeSettings),
]);
const tshClient = createTshClient(addresses.tsh, credentials.tsh);
const tshClient = createTshClient(addresses.tsh, credentials.tshd);
const ptyServiceClient = createPtyService(
addresses.shared,
credentials.shared,
Expand All @@ -38,6 +46,38 @@ async function getElectronGlobals(): Promise<ElectronGlobals> {
mainProcessClient,
tshClient,
ptyServiceClient,
loggerService,
};
}

/**
* For TCP transport, createGrpcCredentials generates the renderer key pair and reads the public key
* for tshd and the shared process from disk. This lets us set up gRPC clients in the renderer
* process that connect to the gRPC servers of tshd and the shared process.
*/
async function createGrpcCredentials(
runtimeSettings: RuntimeSettings
): Promise<{
// Credentials for talking to the tshd process.
tshd: ChannelCredentials;
// Credentials for talking to the shared process.
shared: ChannelCredentials;
}> {
if (!shouldEncryptConnection(runtimeSettings)) {
return {
tshd: createInsecureClientCredentials(),
shared: createInsecureClientCredentials(),
};
}

const { certsDir } = runtimeSettings;
const [rendererKeyPair, tshdCert, sharedCert] = await Promise.all([
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens if any of these promises fail?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createGrpcCredentials will throw, then after it getElectronGlobals will throw. getElectronGlobals gets exposed through contextBridge as window.electron:

contextBridge.exposeInMainWorld('electron', getElectronGlobals());

It's used in boot.tsx:

try {
const globals = await getElectronGlobals();

getElectronGlobals in boot.tsx calls window.electron underneath, so it will fail as well, triggering the catch branch:

} catch (e) {
logger.error('Failed to boot the React app', e);
renderApp(<FailedApp message={e.toString()} />);

This will show the error to the user in the actual browser window.

generateAndSaveGrpcCert(certsDir, GrpcCertName.Renderer),
readGrpcCert(certsDir, GrpcCertName.Tshd),
readGrpcCert(certsDir, GrpcCertName.Shared),
]);

return {
tshd: createClientCredentials(rendererKeyPair, tshdCert),
shared: createClientCredentials(rendererKeyPair, sharedCert),
};
}

This file was deleted.

56 changes: 56 additions & 0 deletions packages/teleterm/src/services/grpcCredentials/credentials.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
ChannelCredentials,
credentials,
ServerCredentials,
} from '@grpc/grpc-js';

import { RuntimeSettings } from 'teleterm/mainProcess/types';

export function createClientCredentials(
clientKeyPair: { cert: Buffer; key: Buffer },
serverCert: Buffer
): ChannelCredentials {
return credentials.createSsl(
serverCert,
clientKeyPair.key,
clientKeyPair.cert
);
}

export function createServerCredentials(
serverKeyPair: { cert: Buffer; key: Buffer },
clientCert: Buffer
): ServerCredentials {
return ServerCredentials.createSsl(
clientCert,
[
{
cert_chain: serverKeyPair.cert,
private_key: serverKeyPair.key,
},
],
true
);
}

export function createInsecureClientCredentials(): ChannelCredentials {
return credentials.createInsecure();
}

export function createInsecureServerCredentials(): ServerCredentials {
return ServerCredentials.createInsecure();
}

/**
* Checks if the gRPC connection should be encrypted.
* The only source of truth is the type of tshd protocol.
* Any protocol other than `unix` should be encrypted.
* The same check is performed on the tshd side.
*/
export function shouldEncryptConnection(
runtimeSettings: RuntimeSettings
): boolean {
return (
new URL(runtimeSettings.tshd.requestedNetworkAddress).protocol !== 'unix:'
);
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import path from 'path';

import { watch } from 'fs';
import { readFile, writeFile, stat, rename } from 'fs/promises';

import { RuntimeSettings } from 'teleterm/mainProcess/types';

import { makeCert } from './makeCert';

/**
Expand Down Expand Up @@ -83,17 +80,3 @@ export async function readGrpcCert(
abortController.abort();
}
}

/**
* Checks if the gRPC connection should be encrypted.
* The only source of truth is the type of tshd protocol.
* Any other protocol than `unix` should be encrypted.
* The same check is performed on the tshd side.
*/
export function shouldEncryptConnection(
runtimeSettings: RuntimeSettings
): boolean {
return (
new URL(runtimeSettings.tshd.requestedNetworkAddress).protocol !== 'unix:'
);
}
5 changes: 3 additions & 2 deletions packages/teleterm/src/services/grpcCredentials/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from './clientCredentials';
export * from './serverCredentials';
export * from './types';
export * from './credentials';
export * from './files';
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ import { makeCert } from './makeCert';

test('create CA cert', async () => {
const ca = await makeCert({
commonName: 'Test CA',
commonName: 'test-ca',
validityDays: 365,
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ interface GeneratedCert {
cert: string;
}

/**
* Creates a self-signed cert. commonName should be a valid domain name.
*/
export async function makeCert({
commonName,
validityDays,
Expand All @@ -69,6 +72,15 @@ export async function makeCert({
digitalSignature: true,
keyEncipherment: true,
},
{
name: 'subjectAltName',
altNames: [
{
type: 2, // DNS type
value: commonName,
},
],
},
Comment on lines +75 to +83
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To my surprise, Go didn't complain when a cert without this was used as a client cert, but it was a problem when I tried to use it as a server cert for the client credentials.

];

return await generateRawCert({
Expand Down

This file was deleted.

11 changes: 7 additions & 4 deletions packages/teleterm/src/services/grpcCredentials/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// `Client` and `TshServer` file names are also used on the tshd side
// Each process creates its own key pair. The public key is saved to disk under the specified
// filename, the private key stays in the memory.
//
// `Renderer` and `Tshd` file names are also used on the tshd side.
export enum GrpcCertName {
Client = 'client.crt',
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we're going to have both a gRPC client and a gRPC server in the renderer process, the name "client" stopped being sufficient. I refactored this so that it closer reflects what we want to achieve with gRPC certs – each process owns its own key pair, keeps the private key in memory and the public key is saved to the file.

TshServer = 'tsh_server.crt',
SharedServer = 'shared_server.crt',
Renderer = 'renderer.crt',
Tshd = 'tshd.crt',
Shared = 'shared.crt',
}
Loading