diff --git a/core/packages/gax/test/showcase-server/src/index.ts b/core/packages/gax/test/showcase-server/src/index.ts index a7f904cc6bff..c9dfda05d1d4 100644 --- a/core/packages/gax/test/showcase-server/src/index.ts +++ b/core/packages/gax/test/showcase-server/src/index.ts @@ -31,9 +31,11 @@ function sleep(timeoutMs: number) { export class ShowcaseServer { server: execa.ExecaChildProcess | undefined; + originalCwd: string | undefined; - async start() { - const testDir = path.join(process.cwd(), '.showcase-server-dir'); + async start(opts?: { tls?: boolean; port?: string; caCertOutputFile?: string }) { + this.originalCwd = process.cwd(); + const testDir = path.join(this.originalCwd, '.showcase-server-dir'); const platform = process.platform; const arch = process.arch === 'x64' ? 'amd64' : process.arch; const showcaseVersion = process.env['SHOWCASE_VERSION'] || '0.36.2'; @@ -41,6 +43,11 @@ export class ShowcaseServer { const fallbackServerUrl = `https://github.com/googleapis/gapic-showcase/releases/download/v${showcaseVersion}/${tarballFilename}`; const binaryName = './gapic-showcase'; + let resolvedCaCertPath = ''; + if (opts?.caCertOutputFile) { + resolvedCaCertPath = path.resolve(this.originalCwd, opts.caCertOutputFile); + } + await fsp.rm(testDir, {recursive: true, force: true}); await mkdir(testDir); process.chdir(testDir); @@ -48,7 +55,19 @@ export class ShowcaseServer { await download(fallbackServerUrl, testDir); await execa('tar', ['xzf', tarballFilename]); - const childProcess = execa(binaryName, ['run'], { + + const args = ['run']; + if (opts?.tls) { + args.push('--tls'); + } + if (opts?.port) { + args.push('--port', opts.port); + } + if (resolvedCaCertPath) { + args.push('--ca-cert-output-file', resolvedCaCertPath); + } + + const childProcess = execa(binaryName, args, { cwd: testDir, stdio: 'inherit', }); @@ -75,6 +94,9 @@ export class ShowcaseServer { throw new Error("Cannot kill the server, it's not started."); } this.server.kill(); + if (this.originalCwd) { + process.chdir(this.originalCwd); + } } } diff --git a/core/packages/gax/test/test-application/src/index.ts b/core/packages/gax/test/test-application/src/index.ts index d6ab4154e17b..417d8b1cd3aa 100644 --- a/core/packages/gax/test/test-application/src/index.ts +++ b/core/packages/gax/test/test-application/src/index.ts @@ -19,6 +19,8 @@ import {EchoClient, SequenceServiceClient, protos} from 'showcase-echo-client'; import {ShowcaseServer} from 'showcase-server'; import * as assert from 'assert'; import {promises as fsp} from 'fs'; +import * as fs from 'fs'; +import * as https from 'https'; import * as path from 'path'; import { protobuf, @@ -2902,6 +2904,112 @@ async function testStreamingErrorAfterDataNoBufferNoRetry( }); } +async function testPqc(pemPath: string, port: number) { + console.log('Running Post Quantum Cryptography (PQC) Integration Tests...'); + + // Verify the CA certificate file exists + let pemExists = false; + for (let i = 0; i < 15; i++) { + if (fs.existsSync(pemPath)) { + pemExists = true; + break; + } + await new Promise(resolve => setTimeout(resolve, 1000)); + } + if (!pemExists) { + throw new Error(`CA Certificate file not found at ${pemPath}`); + } + + const pemBuffer = fs.readFileSync(pemPath); + + // --- 1. gRPC PQC Test --- + console.log('Testing PQC via gRPC...'); + let negotiatedGroupGrpc: string | undefined; + + const interceptor = (options: any, nextCall: any) => { + return new grpc.InterceptingCall(nextCall(options), { + start: (metadata: any, listener: any, next: any) => { + next(metadata, { + onReceiveMetadata: (receivedMetadata: any, nextListener: any) => { + const group = receivedMetadata.get('x-showcase-tls-group'); + if (group && group.length > 0) { + negotiatedGroupGrpc = group[0].toString(); + } + nextListener(receivedMetadata); + }, + }); + }, + }); + }; + + const grpcClientOpts = { + grpc, + sslCreds: grpc.credentials.createSsl(pemBuffer), + servicePath: 'localhost', + port: port, + }; + + const grpcClient = new EchoClient(grpcClientOpts); + + const [responseGrpc] = await grpcClient.echo( + { content: 'grpc-pqc-test' }, + { + otherArgs: { + options: { + interceptors: [interceptor], + }, + }, + } + ); + + assert.strictEqual(responseGrpc.content, 'grpc-pqc-test'); + console.log(`gRPC TLS negotiated group: ${negotiatedGroupGrpc}`); + assert.ok(negotiatedGroupGrpc, 'Expected negotiated TLS group in gRPC response metadata'); + assert.strictEqual(negotiatedGroupGrpc, 'X25519MLKEM768'); + + // --- 2. HTTP/REST Fallback PQC Test --- + console.log('Testing PQC via HTTP/REST Fallback...'); + let negotiatedGroupRest: string | undefined; + + const auth = new GoogleAuth({ + authClient: new googleAuthLibrary.PassThroughClient(), + }); + + const originalFetch = auth.fetch.bind(auth); + (auth as any).fetch = async (url: string, opts: any) => { + if (url.startsWith('https:')) { + opts.agent = new https.Agent({ + ca: pemBuffer, + keepAlive: true, + }); + } + const res = await originalFetch(url, opts); + const group = typeof res.headers.get === 'function' ? res.headers.get('x-showcase-tls-group') : (res.headers as any)['x-showcase-tls-group']; + if (group) { + negotiatedGroupRest = group; + } + return res; + }; + + const restClientOpts = { + fallback: true, + protocol: 'https', + servicePath: 'localhost', + port: port, + auth: auth, + }; + + const restClient = new EchoClient(restClientOpts); + const [responseRest] = await restClient.echo({ content: 'rest-pqc-test' }); + + assert.strictEqual(responseRest.content, 'rest-pqc-test'); + console.log(`REST TLS negotiated group: ${negotiatedGroupRest}`); + assert.ok(negotiatedGroupRest, 'Expected negotiated TLS group in REST response headers'); + assert.strictEqual(negotiatedGroupRest, 'X25519MLKEM768'); + + console.log('All PQC Integration Tests Passed Successfully!'); +} + async function main() { const showcaseServer = new ShowcaseServer(); try { @@ -2910,6 +3018,32 @@ async function main() { } finally { showcaseServer.stop(); } + + console.log('Starting PQC test with TLS-enabled showcase server...'); + process.env['SHOWCASE_VERSION'] = '0.41.1'; + const showcaseServerTls = new ShowcaseServer(); + const tlsPort = 7443; + const caCertOutputFile = 'showcase.pem'; + const pemPath = path.join(process.cwd(), caCertOutputFile); + + try { + if (fs.existsSync(pemPath)) { + fs.unlinkSync(pemPath); + } + + await (showcaseServerTls as any).start({ + tls: true, + port: `:${tlsPort}`, + caCertOutputFile: caCertOutputFile, + }); + + await testPqc(pemPath, tlsPort); + } finally { + showcaseServerTls.stop(); + if (fs.existsSync(pemPath)) { + fs.unlinkSync(pemPath); + } + } } main();