347 lines
12 KiB
TypeScript
347 lines
12 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { dirname, resolve } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
parseThumbprintString,
|
|
addWindowsArtifactSigningToOverride,
|
|
addWindowsSigningToOverride,
|
|
addMacosAdhocToOverride,
|
|
type TauriOverrideConfig,
|
|
} from './signing-config.js';
|
|
|
|
// ─── parseThumbprintString ──────────────────────────────────────────────────
|
|
|
|
describe('parseThumbprintString', () => {
|
|
it('returns the uppercased thumbprint when given valid 40-hex input', () => {
|
|
const raw = 'abcdef0123456789abcdef0123456789abcdef01';
|
|
expect(parseThumbprintString(raw)).toBe(
|
|
'ABCDEF0123456789ABCDEF0123456789ABCDEF01',
|
|
);
|
|
});
|
|
|
|
it('strips surrounding whitespace and trailing newline (PowerShell output shape)', () => {
|
|
const raw = ' ABCDEF0123456789ABCDEF0123456789ABCDEF01\r\n';
|
|
expect(parseThumbprintString(raw)).toBe(
|
|
'ABCDEF0123456789ABCDEF0123456789ABCDEF01',
|
|
);
|
|
});
|
|
|
|
it('removes embedded whitespace inside the thumbprint (some clipboards introduce spaces)', () => {
|
|
const raw = 'AB CD EF 01 23 45 67 89 AB CD EF 01 23 45 67 89 AB CD EF 01';
|
|
expect(parseThumbprintString(raw)).toBe(
|
|
'ABCDEF0123456789ABCDEF0123456789ABCDEF01',
|
|
);
|
|
});
|
|
|
|
it('throws when the thumbprint is shorter than 40 chars', () => {
|
|
expect(() => parseThumbprintString('ABCDEF')).toThrow(
|
|
/must be 40 hex characters/i,
|
|
);
|
|
});
|
|
|
|
it('throws when the thumbprint contains non-hex characters', () => {
|
|
const raw = 'ZZCDEF0123456789ABCDEF0123456789ABCDEF01';
|
|
expect(() => parseThumbprintString(raw)).toThrow(/must be 40 hex characters/i);
|
|
});
|
|
|
|
it('throws on empty input', () => {
|
|
expect(() => parseThumbprintString('')).toThrow(/empty/i);
|
|
expect(() => parseThumbprintString(' ')).toThrow(/empty/i);
|
|
});
|
|
});
|
|
|
|
// ─── addWindowsSigningToOverride ────────────────────────────────────────────
|
|
|
|
describe('addWindowsSigningToOverride', () => {
|
|
const VALID_THUMBPRINT = 'ABCDEF0123456789ABCDEF0123456789ABCDEF01';
|
|
|
|
it('writes certificateThumbprint + sensible defaults when no options given', () => {
|
|
const input: TauriOverrideConfig = { build: { beforeBuildCommand: '' } };
|
|
const out = addWindowsSigningToOverride(input, VALID_THUMBPRINT);
|
|
|
|
expect(out.bundle?.windows?.certificateThumbprint).toBe(VALID_THUMBPRINT);
|
|
expect(out.bundle?.windows?.digestAlgorithm).toBe('sha256');
|
|
expect(out.bundle?.windows?.timestampUrl).toBe(
|
|
'http://timestamp.digicert.com',
|
|
);
|
|
});
|
|
|
|
it('preserves existing top-level fields (build, app, etc.)', () => {
|
|
const input: TauriOverrideConfig = {
|
|
build: { beforeBuildCommand: 'echo hello' },
|
|
app: { security: { csp: 'default-src self' } },
|
|
};
|
|
const out = addWindowsSigningToOverride(input, VALID_THUMBPRINT);
|
|
|
|
expect(out.build).toEqual({ beforeBuildCommand: 'echo hello' });
|
|
expect(out.app).toEqual({ security: { csp: 'default-src self' } });
|
|
});
|
|
|
|
it('preserves existing bundle.windows fields not related to signing', () => {
|
|
const input: TauriOverrideConfig = {
|
|
bundle: {
|
|
windows: { nsis: { installMode: 'currentUser' } },
|
|
},
|
|
};
|
|
const out = addWindowsSigningToOverride(input, VALID_THUMBPRINT);
|
|
|
|
expect(out.bundle?.windows?.nsis).toEqual({ installMode: 'currentUser' });
|
|
expect(out.bundle?.windows?.certificateThumbprint).toBe(VALID_THUMBPRINT);
|
|
});
|
|
|
|
it('removes an Azure signCommand when returning to certificate-store signing', () => {
|
|
const azure: TauriOverrideConfig = addWindowsArtifactSigningToOverride(
|
|
{
|
|
bundle: {
|
|
windows: { nsis: { installMode: 'currentUser' } },
|
|
},
|
|
},
|
|
String.raw`D:\a\waggle-os\app\scripts\sign-windows-artifact.ps1`,
|
|
);
|
|
|
|
const out = addWindowsSigningToOverride(azure, VALID_THUMBPRINT);
|
|
|
|
expect(out.bundle?.windows?.signCommand).toBeUndefined();
|
|
expect(out.bundle?.windows?.certificateThumbprint).toBe(VALID_THUMBPRINT);
|
|
expect(out.bundle?.windows?.nsis).toEqual({ installMode: 'currentUser' });
|
|
});
|
|
|
|
it('overrides custom digestAlgorithm and timestampUrl when options provided', () => {
|
|
const out = addWindowsSigningToOverride({}, VALID_THUMBPRINT, {
|
|
digestAlgorithm: 'sha384',
|
|
timestampUrl: 'http://timestamp.sectigo.com',
|
|
});
|
|
|
|
expect(out.bundle?.windows?.digestAlgorithm).toBe('sha384');
|
|
expect(out.bundle?.windows?.timestampUrl).toBe(
|
|
'http://timestamp.sectigo.com',
|
|
);
|
|
});
|
|
|
|
it('does not mutate the input config (immutability invariant)', () => {
|
|
const input: TauriOverrideConfig = { build: { beforeBuildCommand: '' } };
|
|
const inputSnapshot = JSON.parse(JSON.stringify(input));
|
|
addWindowsSigningToOverride(input, VALID_THUMBPRINT);
|
|
expect(input).toEqual(inputSnapshot);
|
|
});
|
|
|
|
it('is idempotent — applying twice with the same thumbprint yields equal output', () => {
|
|
const input: TauriOverrideConfig = {};
|
|
const once = addWindowsSigningToOverride(input, VALID_THUMBPRINT);
|
|
const twice = addWindowsSigningToOverride(once, VALID_THUMBPRINT);
|
|
expect(twice).toEqual(once);
|
|
});
|
|
|
|
it('replaces an old thumbprint when called with a new one (cert rotation)', () => {
|
|
const input: TauriOverrideConfig = {};
|
|
const v1 = addWindowsSigningToOverride(input, VALID_THUMBPRINT);
|
|
const newThumb = '1234567890ABCDEF1234567890ABCDEF12345678';
|
|
const v2 = addWindowsSigningToOverride(v1, newThumb);
|
|
expect(v2.bundle?.windows?.certificateThumbprint).toBe(newThumb);
|
|
});
|
|
|
|
it('rejects an invalid thumbprint up front', () => {
|
|
expect(() =>
|
|
addWindowsSigningToOverride({}, 'too-short'),
|
|
).toThrow(/must be 40 hex characters/i);
|
|
});
|
|
});
|
|
|
|
// ─── addWindowsArtifactSigningToOverride ───────────────────────────────────
|
|
|
|
describe('addWindowsArtifactSigningToOverride', () => {
|
|
const WRAPPER_PATH = String.raw`D:\a\waggle-os\app\scripts\sign-windows-artifact.ps1`;
|
|
const SYSTEM_POWERSHELL_PATH =
|
|
String.raw`C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe`;
|
|
|
|
it('configures an object-form Tauri signCommand with one artifact placeholder', () => {
|
|
const out = addWindowsArtifactSigningToOverride(
|
|
{},
|
|
WRAPPER_PATH,
|
|
);
|
|
|
|
expect(out.bundle?.windows?.signCommand).toEqual({
|
|
cmd: SYSTEM_POWERSHELL_PATH,
|
|
args: [
|
|
'-NoLogo',
|
|
'-NoProfile',
|
|
'-NonInteractive',
|
|
'-ExecutionPolicy',
|
|
'Bypass',
|
|
'-File',
|
|
WRAPPER_PATH,
|
|
'-ArtifactPath',
|
|
'%1',
|
|
],
|
|
});
|
|
});
|
|
|
|
it('removes mutually exclusive certificate-store signing fields', () => {
|
|
const input: TauriOverrideConfig = {
|
|
bundle: {
|
|
windows: {
|
|
certificateThumbprint: 'AB'.repeat(20),
|
|
digestAlgorithm: 'sha256',
|
|
timestampUrl: 'http://timestamp.digicert.com',
|
|
tsp: true,
|
|
nsis: { installMode: 'currentUser' },
|
|
},
|
|
},
|
|
};
|
|
|
|
const out = addWindowsArtifactSigningToOverride(
|
|
input,
|
|
WRAPPER_PATH,
|
|
);
|
|
expect(out.bundle?.windows?.certificateThumbprint).toBeUndefined();
|
|
expect(out.bundle?.windows?.digestAlgorithm).toBeUndefined();
|
|
expect(out.bundle?.windows?.timestampUrl).toBeUndefined();
|
|
expect(out.bundle?.windows?.tsp).toBeUndefined();
|
|
expect(out.bundle?.windows?.nsis).toEqual({ installMode: 'currentUser' });
|
|
});
|
|
|
|
it('is immutable and idempotent', () => {
|
|
const input: TauriOverrideConfig = {
|
|
build: {
|
|
beforeBuildCommand: 'npm run build',
|
|
beforeBundleCommand: 'node mutate-bundle.mjs',
|
|
},
|
|
bundle: {
|
|
active: false,
|
|
targets: ['msi'],
|
|
windows: { nsis: { installMode: 'currentUser' } },
|
|
},
|
|
};
|
|
const snapshot = JSON.parse(JSON.stringify(input));
|
|
const once = addWindowsArtifactSigningToOverride(
|
|
input,
|
|
WRAPPER_PATH,
|
|
);
|
|
const twice = addWindowsArtifactSigningToOverride(
|
|
once,
|
|
WRAPPER_PATH,
|
|
);
|
|
|
|
expect(input).toEqual(snapshot);
|
|
expect(twice).toEqual(once);
|
|
expect(once.build).toEqual({
|
|
beforeBuildCommand: '',
|
|
beforeBundleCommand: '',
|
|
});
|
|
expect(once.bundle?.active).toBe(true);
|
|
expect(once.bundle?.targets).toEqual(['nsis']);
|
|
expect(once.bundle?.windows?.nsis).toEqual({ installMode: 'currentUser' });
|
|
});
|
|
|
|
it('rejects non-absolute, placeholder-bearing, or control-character wrapper paths', () => {
|
|
expect(() =>
|
|
addWindowsArtifactSigningToOverride(
|
|
{},
|
|
'scripts/sign.ps1',
|
|
),
|
|
).toThrow(/absolute Windows path/i);
|
|
expect(() =>
|
|
addWindowsArtifactSigningToOverride(
|
|
{},
|
|
String.raw`D:\a\%1\sign-windows-artifact.ps1`,
|
|
),
|
|
).toThrow(/placeholder/i);
|
|
expect(() =>
|
|
addWindowsArtifactSigningToOverride(
|
|
{},
|
|
'D:\\safe\nmalicious.ps1',
|
|
),
|
|
).toThrow(/control characters/i);
|
|
expect(() =>
|
|
addWindowsArtifactSigningToOverride(
|
|
{},
|
|
String.raw`D:\safe\..\malicious.ps1`,
|
|
),
|
|
).toThrow(/canonical local Windows/i);
|
|
expect(() =>
|
|
addWindowsArtifactSigningToOverride(
|
|
{},
|
|
String.raw`D:\safe\sign.ps1:payload`,
|
|
),
|
|
).toThrow(/canonical local Windows/i);
|
|
});
|
|
|
|
it('contains exactly one artifact placeholder across the complete command', () => {
|
|
const out = addWindowsArtifactSigningToOverride({}, WRAPPER_PATH);
|
|
const command = out.bundle?.windows?.signCommand;
|
|
const placeholderCount = [command?.cmd, ...(command?.args ?? [])]
|
|
.flatMap((part) => part?.match(/%1/g) ?? [])
|
|
.length;
|
|
expect(placeholderCount).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe('apply-signing-config Artifact Signing boundary', () => {
|
|
it('fails closed toward the protected hosted release workflow, never local Build mode', () => {
|
|
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
const result = spawnSync(
|
|
process.execPath,
|
|
[resolve(scriptDir, 'apply-signing-config.mjs')],
|
|
{
|
|
cwd: resolve(scriptDir, '..'),
|
|
env: { ...process.env, WAGGLE_WINDOWS_SIGNING_MODE: 'artifact-signing' },
|
|
encoding: 'utf8',
|
|
},
|
|
);
|
|
const output = `${result.stdout}\n${result.stderr}`;
|
|
expect(result.status).not.toBe(0);
|
|
expect(output).toMatch(/protected GitHub-hosted release workflow/i);
|
|
expect(output).not.toMatch(/-Mode Build/i);
|
|
});
|
|
});
|
|
|
|
// ─── addMacosAdhocToOverride ────────────────────────────────────────────────
|
|
|
|
describe('addMacosAdhocToOverride', () => {
|
|
it('sets bundle.macOS.signingIdentity to "-" (ad-hoc sign sentinel)', () => {
|
|
const out = addMacosAdhocToOverride({});
|
|
expect(out.bundle?.macOS?.signingIdentity).toBe('-');
|
|
});
|
|
|
|
it('preserves existing top-level and bundle fields', () => {
|
|
const input: TauriOverrideConfig = {
|
|
build: { beforeBuildCommand: 'echo' },
|
|
bundle: {
|
|
windows: { certificateThumbprint: 'AB'.repeat(20) },
|
|
},
|
|
};
|
|
const out = addMacosAdhocToOverride(input);
|
|
|
|
expect(out.build).toEqual({ beforeBuildCommand: 'echo' });
|
|
expect(out.bundle?.windows?.certificateThumbprint).toBe('AB'.repeat(20));
|
|
expect(out.bundle?.macOS?.signingIdentity).toBe('-');
|
|
});
|
|
|
|
it('does not mutate the input config', () => {
|
|
const input: TauriOverrideConfig = { bundle: { macOS: {} } };
|
|
const snapshot = JSON.parse(JSON.stringify(input));
|
|
addMacosAdhocToOverride(input);
|
|
expect(input).toEqual(snapshot);
|
|
});
|
|
|
|
it('is idempotent', () => {
|
|
const once = addMacosAdhocToOverride({});
|
|
const twice = addMacosAdhocToOverride(once);
|
|
expect(twice).toEqual(once);
|
|
});
|
|
|
|
it('preserves additional macOS fields (entitlements, providerShortName)', () => {
|
|
const input: TauriOverrideConfig = {
|
|
bundle: {
|
|
macOS: { entitlements: './ent.plist', providerShortName: 'TEAM' },
|
|
},
|
|
};
|
|
const out = addMacosAdhocToOverride(input);
|
|
|
|
expect(out.bundle?.macOS?.entitlements).toBe('./ent.plist');
|
|
expect(out.bundle?.macOS?.providerShortName).toBe('TEAM');
|
|
expect(out.bundle?.macOS?.signingIdentity).toBe('-');
|
|
});
|
|
});
|