-
Notifications
You must be signed in to change notification settings - Fork 694
feat(pqc): add post quantum cryptography safe showcase integration tests #8875
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -31,24 +31,43 @@ 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'; | ||||||||||||||||||
| const tarballFilename = `gapic-showcase-${showcaseVersion}-${platform}-${arch}.tar.gz`; | ||||||||||||||||||
| 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); | ||||||||||||||||||
| } | ||||||||||||||||||
|
Comment on lines
+46
to
+49
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use the local
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
| await fsp.rm(testDir, {recursive: true, force: true}); | ||||||||||||||||||
| await mkdir(testDir); | ||||||||||||||||||
| process.chdir(testDir); | ||||||||||||||||||
| console.log(`Server will be run from ${testDir}.`); | ||||||||||||||||||
|
|
||||||||||||||||||
| 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); | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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:')) { | ||
|
Comment on lines
+2979
to
+2980
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To prevent potential runtime (auth as any).fetch = async (url: string, opts: any) => {
opts = opts || {};
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(); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There are two issues here:
this.originalCwdis typed asstring | undefined, passing it directly topath.joinandpath.resolvewill cause compilation errors understrictNullChecks. Using a localcwdvariable avoids this.start()fails (e.g., due to download or port issues) beforethis.serveris assigned,this.serverremainsundefined. Whenstop()is called in thefinallyblock, it will throw an error becausethis.serveris missing, which masks the original failure and preventsprocess.chdirfrom restoring the directory. Initializingthis.serverwith a dummy object ensuresstop()can safely restore the directory and won't mask the real error.