diff --git a/config/scripts/ssh-relay-runtime-archive-extraction.test.mjs b/config/scripts/ssh-relay-runtime-archive-extraction.test.mjs index c5843413d2c..7540b9e2eae 100644 --- a/config/scripts/ssh-relay-runtime-archive-extraction.test.mjs +++ b/config/scripts/ssh-relay-runtime-archive-extraction.test.mjs @@ -88,7 +88,10 @@ afterEach(async () => { describe('SSH relay runtime archive extraction', () => { it('reconstructs exact POSIX and Windows runtime trees into exclusive directories', async () => { - for (const tupleId of ['darwin-arm64', 'win32-x64']) { + // Why: NTFS cannot materialize the executable modes needed to construct a POSIX tar fixture. + const tuples = + process.platform === 'win32' ? [`win32-${process.arch}`] : ['darwin-arm64', 'win32-x64'] + for (const tupleId of tuples) { const value = await fixture(tupleId) const outputDirectory = join(value.root, 'reconstructed') const result = await extractSshRelayRuntimeArchive({ @@ -106,7 +109,8 @@ describe('SSH relay runtime archive extraction', () => { }) it('rejects modified archives and existing outputs without retaining partial trees', async () => { - const modified = await fixture('darwin-x64') + const modifiedTuple = process.platform === 'win32' ? `win32-${process.arch}` : 'darwin-x64' + const modified = await fixture(modifiedTuple) await writeFile(modified.archive.path, 'not the authenticated archive') const rejectedOutput = join(modified.root, 'rejected') await expect( diff --git a/config/scripts/ssh-relay-runtime-native-signing-finalization.test.mjs b/config/scripts/ssh-relay-runtime-native-signing-finalization.test.mjs index a06f5f95e8b..8dc8dada2ca 100644 --- a/config/scripts/ssh-relay-runtime-native-signing-finalization.test.mjs +++ b/config/scripts/ssh-relay-runtime-native-signing-finalization.test.mjs @@ -9,7 +9,9 @@ import { expectedSshRelayRuntimeClosureEntries } from './ssh-relay-runtime-closu import { sshRelayRuntimeCompatibility } from './ssh-relay-runtime-compatibility.mjs' import { computeSshRelayRuntimeContentId } from './ssh-relay-runtime-identity.mjs' import { finalizeSshRelayRuntimeNativeSigning } from './ssh-relay-runtime-native-signing-finalization.mjs' +import { buildSshRelayRuntimeNativeSigningPlan } from './ssh-relay-runtime-native-signing-plan.mjs' import { prepareSshRelayRuntimeNativeSigningStage } from './ssh-relay-runtime-native-signing-stage.mjs' +import { parseSshRelayRuntimeNativeSigningStageReport } from './ssh-relay-runtime-native-signing-stage-report.mjs' const temporaryDirectories = [] const SOURCE_DATE_EPOCH = 1_788_739_200 @@ -19,7 +21,8 @@ function digest(bytes) { return `sha256:${createHash('sha256').update(bytes).digest('hex')}` } -function toolchain() { +function toolchain(tupleId) { + const platformTool = tupleId.startsWith('win32-') ? 'linker' : 'strip' return Object.fromEntries( [ 'buildNode', @@ -30,7 +33,7 @@ function toolchain() { 'archive', 'nodeAddonApi', 'nodeGyp', - 'strip' + platformTool ].map((name, index) => [ name, { version: `${name} fixture`, sha256: `sha256:${(index + 1).toString(16).repeat(64)}` } @@ -38,13 +41,17 @@ function toolchain() { ) } -async function fixture() { +async function fixture( + tupleId = process.platform === 'win32' ? `win32-${process.arch}` : 'darwin-arm64' +) { const root = await mkdtemp(join(tmpdir(), 'ssh-relay-signing-finalization-')) temporaryDirectories.push(root) const sourceRuntimeRoot = join(root, 'source-runtime') await mkdir(sourceRuntimeRoot) const entries = [] - for (const entry of expectedSshRelayRuntimeClosureEntries('darwin-arm64')) { + // Why: Windows finalization must create a ZIP because NTFS cannot supply POSIX tar modes. + const os = tupleId.startsWith('win32-') ? 'win32' : 'darwin' + for (const entry of expectedSshRelayRuntimeClosureEntries(tupleId)) { const path = join(sourceRuntimeRoot, ...entry.path.split('/')) if (entry.type === 'directory') { await mkdir(path, { recursive: true, mode: entry.mode }) @@ -58,10 +65,10 @@ async function fixture() { } const base = { identitySchemaVersion: 1, - tupleId: 'darwin-arm64', - os: 'darwin', - architecture: 'arm64', - compatibility: sshRelayRuntimeCompatibility['darwin-arm64'], + tupleId, + os, + architecture: tupleId.includes('arm64') ? 'arm64' : 'x64', + compatibility: sshRelayRuntimeCompatibility[tupleId], nodeVersion: '24.18.0', dependencies: { nodePtyVersion: '1.1.0', parcelWatcherVersion: '2.5.6' }, entries @@ -74,11 +81,30 @@ async function fixture() { expandedSize: files.reduce((total, entry) => total + entry.size, 0) } const returnedRoot = join(root, 'returned') + const preserved = new Set([ + 'node_modules/node-pty/build/Release/conpty/OpenConsole.exe', + 'node_modules/node-pty/build/Release/conpty/conpty.dll' + ]) + const assessments = + os === 'win32' + ? buildSshRelayRuntimeNativeSigningPlan(sourceIdentity).signingCandidates.map((entry) => + preserved.has(entry.path) + ? { + path: entry.path, + sourceSha256: entry.sourceSha256, + status: 'valid-upstream', + signerSubject: 'CN=Microsoft Corporation', + signerThumbprint: 'D'.repeat(40) + } + : { path: entry.path, sourceSha256: entry.sourceSha256, status: 'unsigned' } + ) + : [] const report = await prepareSshRelayRuntimeNativeSigningStage({ identity: sourceIdentity, runtimeRoot: sourceRuntimeRoot, stagingRoot: returnedRoot, - platform: 'darwin' + platform: os, + assessWindowsImpl: async () => assessments }) for (const entry of report.signingFiles) { await appendFile(join(returnedRoot, ...entry.path.split('/')), ':signed') @@ -86,18 +112,8 @@ async function fixture() { const nodeRelease = JSON.parse( await readFile(new URL('../ssh-relay-node-release-v24.18.0.json', import.meta.url), 'utf8') ) - const selection = { - tupleId: report.tupleId, - platform: report.platform, - policy: report.policy, - immutableVendorFiles: report.immutableVendorFiles, - signingFiles: report.signingFiles, - preservedUpstreamFiles: report.preservedUpstreamFiles, - verificationFiles: [...report.immutableVendorFiles, ...report.signingFiles].map( - ({ path, role, sourceSha256, sourceSize }) => ({ path, role, sourceSha256, sourceSize }) - ) - } - return { root, sourceRuntimeRoot, returnedRoot, sourceIdentity, selection, nodeRelease } + const { selection } = parseSshRelayRuntimeNativeSigningStageReport(sourceIdentity, report) + return { root, tupleId, sourceRuntimeRoot, returnedRoot, sourceIdentity, selection, nodeRelease } } function nativeReport(sourceIdentity, finalIdentity, selection) { @@ -106,18 +122,41 @@ function nativeReport(sourceIdentity, finalIdentity, selection) { .filter((entry) => entry.type === 'file') .map((entry) => [entry.path, entry]) ) + const immutable = new Set(selection.immutableVendorFiles.map((entry) => entry.path)) + const signing = new Set(selection.signingFiles.map((entry) => entry.path)) return { tupleId: finalIdentity.tupleId, sourceContentId: sourceIdentity.contentId, finalContentId: finalIdentity.contentId, - verifiedFiles: selection.verificationFiles.map((entry) => ({ - path: entry.path, - role: entry.role, - sha256: finalFiles.get(entry.path).sha256, - signerKind: entry.role === 'node' ? 'official-node' : 'orca-built', - authority: `Developer ID Application: Fixture (${entry.role === 'node' ? 'HX7739G8FX' : 'ABCDEFGHIJ'})`, - teamIdentifier: entry.role === 'node' ? 'HX7739G8FX' : 'ABCDEFGHIJ' - })) + verifiedFiles: selection.verificationFiles.map((entry) => { + const signerKind = immutable.has(entry.path) + ? 'official-node' + : signing.has(entry.path) + ? 'orca-built' + : 'preserved-upstream' + const base = { + path: entry.path, + role: entry.role, + sha256: finalFiles.get(entry.path).sha256, + signerKind + } + return sourceIdentity.os === 'win32' + ? { + ...base, + signerSubject: + signerKind === 'official-node' + ? 'CN=OpenJS Foundation' + : signerKind === 'orca-built' + ? 'CN=SignPath Foundation' + : 'CN=Microsoft Corporation', + signerThumbprint: (signerKind === 'preserved-upstream' ? 'D' : 'A').repeat(40) + } + : { + ...base, + authority: `Developer ID Application: Fixture (${signerKind === 'official-node' ? 'HX7739G8FX' : 'ABCDEFGHIJ'})`, + teamIdentifier: signerKind === 'official-node' ? 'HX7739G8FX' : 'ABCDEFGHIJ' + } + }) } } @@ -130,14 +169,17 @@ function finalizationInput(value, outputDirectory) { gitCommit: GIT_COMMIT, builder: 'https://github.com/stablyai/orca/ssh-relay-runtime-native-signing-fixture', runner: { - os: 'macOS', - architecture: 'ARM64', + os: value.sourceIdentity.os === 'win32' ? 'Windows' : 'macOS', + architecture: value.sourceIdentity.architecture.toUpperCase(), environment: 'fixture', - requestedLabel: 'macos-15', - image: { os: 'macos', version: 'fixture' } + requestedLabel: value.sourceIdentity.os === 'win32' ? 'windows-fixture' : 'macos-15', + image: { os: value.sourceIdentity.os, version: 'fixture' } + }, + toolchain: toolchain(value.tupleId), + nativeVerificationTool: { + name: value.sourceIdentity.os === 'win32' ? 'Get-AuthenticodeSignature' : 'codesign', + version: 'fixture-v1' }, - toolchain: toolchain(), - nativeVerificationTool: { name: 'codesign', version: 'fixture-v1' }, verifiedAt: '2026-07-15T12:00:00.000Z', verifyNativeImpl: ({ sourceIdentity, finalIdentity, selection }) => nativeReport(sourceIdentity, finalIdentity, selection), @@ -158,25 +200,29 @@ afterEach(async () => { describe('SSH relay runtime native signing finalization', () => { it('emits only final signed-byte assets after native verification and smoke', async () => { - const value = await fixture() - const outputDirectory = join(value.root, 'final') - const result = await finalizeSshRelayRuntimeNativeSigning( - finalizationInput(value, outputDirectory) - ) + const tuples = + process.platform === 'win32' ? [`win32-${process.arch}`] : ['darwin-arm64', 'win32-x64'] + for (const tupleId of tuples) { + const value = await fixture(tupleId) + const outputDirectory = join(value.root, 'final') + const result = await finalizeSshRelayRuntimeNativeSigning( + finalizationInput(value, outputDirectory) + ) - expect(result.finalContentId).not.toBe(value.sourceIdentity.contentId) - expect(result.returnedFiles).toHaveLength(3) - expect(await readdir(result.assetsRoot)).toHaveLength(4) - expect( - (await readdir(result.assetsRoot)).some((name) => name.endsWith('.manifest-tuple.json')) - ).toBe(true) - expect(await readdir(result.evidenceRoot)).toEqual( - expect.arrayContaining([ - 'darwin-arm64.final-identity.json', - 'darwin-arm64.finalization.json', - 'darwin-arm64.native-verification.json' - ]) - ) + expect(result.finalContentId).not.toBe(value.sourceIdentity.contentId) + expect(result.returnedFiles).toHaveLength(3) + expect(await readdir(result.assetsRoot)).toHaveLength(4) + expect( + (await readdir(result.assetsRoot)).some((name) => name.endsWith('.manifest-tuple.json')) + ).toBe(true) + expect(await readdir(result.evidenceRoot)).toEqual( + expect.arrayContaining([ + `${value.tupleId}.final-identity.json`, + `${value.tupleId}.finalization.json`, + `${value.tupleId}.native-verification.json` + ]) + ) + } }) it('removes the complete output when native trust fails', async () => {