feat!: gate node API behind a derived SNI

This commit is contained in:
kastov 2026-08-17 07:36:40 +03:00
parent 5213041e12
commit 2b7c70780a
No known key found for this signature in database
GPG key ID: 1B27BE29057F4C90
2 changed files with 49 additions and 3 deletions

View file

@ -0,0 +1,32 @@
import { hkdfSync, timingSafeEqual } from 'node:crypto';
const HKDF_INFO = 'rw-v1';
const TLDS = ['com', 'net', 'org', 'io', 'dev', 'app'];
export function deriveSni(caCertPem: string, jwtPublicKey: string): string {
const canon = (pem: string) =>
pem.replace(/-----[^-]+-----/g, '').replace(/[^A-Za-z0-9+/=]/g, '');
const ikm = Buffer.concat([
Buffer.from(canon(jwtPublicKey), 'utf8'),
Buffer.from(canon(caCertPem), 'utf8'),
]);
const okm = Buffer.from(hkdfSync('sha256', ikm, Buffer.alloc(0), HKDF_INFO, 22));
const host = okm.subarray(0, 16).toString('hex');
return `${host}.${okm.subarray(16, 21).toString('hex')}.${TLDS[okm[21] % TLDS.length]}`;
}
export function makeSniVerifier(
caCertPem: string,
jwtPublicKey: string,
): (servername: string) => boolean {
const expected = Buffer.from(deriveSni(caCertPem, jwtPublicKey));
return (servername: string): boolean => {
if (!servername) return false;
const got = Buffer.from(servername);
if (got.length !== expected.length) return false;
return timingSafeEqual(got, expected);
};
}

View file

@ -8,7 +8,7 @@ import { Server } from 'https';
import morgan from 'morgan';
import { utilities as nestWinstonModuleUtilities, WinstonModule } from 'nest-winston';
import { ZodValidationPipe } from 'nestjs-zod';
import { SecureVersion } from 'node:tls';
import { createSecureContext, SecureContext, SecureVersion } from 'node:tls';
import { createLogger } from 'winston';
import * as winston from 'winston';
@ -19,6 +19,10 @@ import { NestFactory } from '@nestjs/core';
import { NotFoundExceptionFilter } from '@common/exception';
import { acquireInstanceLock } from '@common/utils/acquire-instance-lock';
import { parseNodePayload } from '@common/utils/decode-node-payload';
import {
deriveSni,
makeSniVerifier,
} from '@common/utils/decode-node-payload/decode-servername.util';
import { customLogFilter } from '@common/utils/filter-logs';
import { getDuplicateInstanceMessage } from '@common/utils/get-duplicate-instance-message';
import { getStartMessage } from '@common/utils/get-start-message';
@ -55,14 +59,24 @@ async function bootstrap(): Promise<void> {
const nodePayload = parseNodePayload();
const httpsOptions: { minVersion?: SecureVersion } & HttpsOptions = {
const realCtx: SecureContext = createSecureContext({
key: nodePayload.nodeKeyPem,
cert: nodePayload.nodeCertPem,
ca: [nodePayload.caCertPem],
minVersion: 'TLSv1.3',
});
logger.info(`Expected SNI: ${deriveSni(nodePayload.caCertPem, nodePayload.jwtPublicKey)}`);
const verifySni = makeSniVerifier(nodePayload.caCertPem, nodePayload.jwtPublicKey);
const httpsOptions: { minVersion?: SecureVersion } & HttpsOptions = {
SNICallback: (servername: string, cb: (err: Error | null, ctx?: SecureContext) => void) =>
verifySni(servername) ? cb(null, realCtx) : cb(new Error('unknown sni')),
requestCert: true,
rejectUnauthorized: true,
minVersion: 'TLSv1.3',
};
} as { minVersion?: SecureVersion } & HttpsOptions;
const app = await NestFactory.create(AppModule, {
httpsOptions,