import { generateKeyPairSync } from 'node:crypto' import { spawnSync } from 'node:child_process' import { createRequire } from 'node:module' import { existsSync, linkSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, } from 'node:fs' import { tmpdir } from 'node:os' import { dirname, join, relative, resolve, sep } from 'node:path' import { fileURLToPath } from 'node:url' import { buildReleasePayload, canonicalJson, COMPROMISED_SIGNER_SHA256, createImmutableVerificationSnapshot, hashRegularFileStable, prepareVerifiedReleasePublication, RELEASE_AAB_NAME, RELEASE_APK_NAME, RELEASE_PACKAGE_NAME, signReleaseEvidence, SIGNED_EVIDENCE_NAME, writeJsonCreateOnly, } from './mobile-release-evidence-lib.mjs' const workspaceRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..') const require = createRequire(import.meta.url) const { assertNoMobileArtifacts } = require('../deploy-site-to-nas.js') const productionAdMobId = 'ca-app-pub-1234567890123456~1234567890' const productionSigner = '4fac6924821c50daabed764932a53c486f8c6c5f34b9f18db920aa4099152b54' const versionName = '9.8.7' const versionCode = 9_008_007 const releaseProvenance = { repository: 'chanpaca/D3ROVoice', commitSha: 'a'.repeat(40), treeSha: 'b'.repeat(40), gitRef: 'refs/tags/v9.8.7', workflowIdentity: 'github:.github/workflows/release.yml:package-android', runId: '123456789', runAttempt: 1, runnerIdentity: 'github-hosted:ubuntu-latest', verifierSha256: 'c'.repeat(64), bundletoolSha256: 'd'.repeat(64), } function fail(code) { throw new Error(`mobile_release_boundary_self_test:${code}`) } function assert(condition, code) { if (!condition) fail(code) } function readWorkspaceFile(relativePath) { return readFileSync(resolve(workspaceRoot, relativePath), 'utf8') } function runVerifierSelfTest(relativePath) { const result = spawnSync(process.execPath, [resolve(workspaceRoot, relativePath), '--self-test'], { cwd: workspaceRoot, encoding: 'utf8', windowsHide: true, }) assert(result.status === 0, `verifier_self_test_failed_${relativePath}_${result.stderr || result.stdout}`) } function runCliExpectedFailure(relativePath, options, omitted, expectedCode) { const argumentsList = Object.entries(options) .filter(([name]) => name !== omitted) .flatMap(([name, value]) => [`--${name}`, String(value)]) const result = spawnSync(process.execPath, [resolve(workspaceRoot, relativePath), ...argumentsList], { cwd: workspaceRoot, encoding: 'utf8', windowsHide: true, }) const output = `${result.stderr ?? ''}${result.stdout ?? ''}` assert(result.status !== 0, `cli_missing_argument_accepted_${relativePath}_${omitted}`) assert( output.includes(expectedCode), `cli_missing_argument_wrong_failure_${relativePath}_${omitted}_${output}`, ) } function listScripts(directory) { const result = [] for (const entry of readdirSync(directory, { withFileTypes: true })) { if (entry.name === 'node_modules') continue const target = join(directory, entry.name) if (entry.isDirectory()) result.push(...listScripts(target)) else if (/\.(?:cjs|js|mjs)$/.test(entry.name)) result.push(target) } return result } function verifySourceContracts() { const legacySync = readWorkspaceFile('scripts/ci/sync-and-publish-forgejo-release.mjs') for (const forbidden of [ 'app-debug.apk', 'd3ro-voice-v1.0.0.apk', 'Android Release APK', ]) { assert(!legacySync.includes(forbidden), `legacy_sync_contains_${forbidden.replace(/[^a-z0-9]+/gi, '_')}`) } const retiredCapture = readWorkspaceFile('scripts/capture-pricing.js') for (const forbidden of [ 'assembleDebug', 'app-debug.apk', 'apksigner', 'copyFileSync', 'execSync', 'fetch(', 'forgejoAuthorization', 'writeFileSync', ]) { assert(!retiredCapture.includes(forbidden), `retired_capture_contains_${forbidden.replace(/[^a-z0-9]+/gi, '_')}`) } assert(retiredCapture.includes('LEGACY_MOBILE_RELEASE_PIPELINE_DISABLED'), 'retired_capture_marker_missing') const releaseWorkflow = readWorkspaceFile('.github/workflows/release.yml') for (const required of [ 'create-mobile-release-evidence.mjs', 'prepare-mobile-release-publication.mjs', 'ANDROID_RELEASE_EVIDENCE_PRIVATE_KEY_B64', 'ANDROID_UPLOAD_CERT_SHA256', 'release/android-release-identity.json', 'release/mobile-release-evidence-public.pem', '--expected-admob-app-id', '--expected-upload-cert-sha256', '--repository "$GITHUB_REPOSITORY"', '--commit-sha "$GITHUB_SHA"', '--tree-sha "$(git rev-parse', '--git-ref "$GITHUB_REF"', '--workflow-identity "$GITHUB_WORKFLOW_REF"', '--run-id "$GITHUB_RUN_ID"', '--run-attempt "$GITHUB_RUN_ATTEMPT"', '--runner-identity "$RUNNER_NAME:$RUNNER_OS:$RUNNER_ARCH"', 'test "$GITHUB_SHA" = "$(git rev-parse HEAD)"', 'git status --porcelain --untracked-files=all', '--expected-play-app-signing-cert-sha256', '--forbidden-upload-cert-sha256', '--expected-version-name', '--expected-version-code', '--bundletool', '--snapshot-dir', 'a099cfa1543f55593bc2ed16a70a7c67fe54b1747bb7301f37fdfd6d91028e29', 'environment: mobile-production-release', 'fetch-depth: 0', 'git merge-base --is-ancestor', 'tag_name: ${{ steps.release-identity.outputs.tag }}', 'overwrite_files: false', 'name: android-play-console-handoff', 'REPOSITORY_VISIBILITY: ${{ github.event.repository.visibility }}', 'test "$REPOSITORY_VISIBILITY" = "private"', 'release-publication/app-release.aab', 'release-publication/android-release-evidence.json', 'release-publication/android-publication-manifest.json', 'release-snapshot/release-artifact-verification.json', 'retention-days: 7', ]) { assert(releaseWorkflow.includes(required), `release_workflow_missing_${required.replace(/[^a-z0-9]+/gi, '_')}`) } for (const forbidden of [ 'ANDROID_RELEASE_CERT_SHA256', '--expected-cert-sha256', 'ANDROID_RELEASE_EVIDENCE_PUBLIC_KEY_B64', 'name: android-release-assets', 'path: android-source/', 'release-dist/android', ]) { assert(!releaseWorkflow.includes(forbidden), `release_workflow_forbidden_${forbidden.replace(/[^a-z0-9]+/gi, '_')}`) } const handoffStep = releaseWorkflow.match( /- name: Upload Restricted Play Console AAB Handoff[\s\S]*?(?=\n\s{6}- name:|\n\s{2}#)/, )?.[0] ?? '' assert(handoffStep.includes('app-release.aab'), 'restricted_handoff_aab_missing') assert(!handoffStep.includes('app-release.apk'), 'restricted_handoff_contains_upload_key_apk') const publicReleaseStep = releaseWorkflow.slice(releaseWorkflow.indexOf('- name: Create GitHub Release')) assert(!publicReleaseStep.includes('.apk'), 'public_release_contains_android_apk') assert(!publicReleaseStep.includes('.aab'), 'public_release_contains_android_aab') assert(!publicReleaseStep.includes('android-play-console-handoff'), 'public_release_contains_android_handoff') assert(releaseWorkflow.includes('test "$GITHUB_SHA" = "$(git rev-parse origin/main)"'), 'dispatch_main_sha_guard_missing') assert(releaseWorkflow.includes('test "$RELEASE_TAG" = "v$VERSION_NAME"'), 'release_tag_identity_guard_missing') const packageJson = JSON.parse(readWorkspaceFile('package.json')) assert( packageJson.scripts?.['release:mobile:boundary'] === 'node scripts/ci/verify-mobile-release-boundary.mjs', 'package_boundary_script_missing', ) assert( packageJson.scripts?.['release:mobile:boundary:test'] === 'node scripts/ci/verify-mobile-release-boundary.mjs --self-test', 'package_boundary_self_test_script_missing', ) const evidenceCreator = readWorkspaceFile('scripts/ci/create-mobile-release-evidence.mjs') for (const required of [ 'verify-android-artifact.mjs', "'--mode', 'release'", "'--expected-admob-app-id'", "'--expected-upload-cert-sha256'", "'--expected-version-name'", "'--expected-version-code'", "'--bundletool'", 'createImmutableVerificationSnapshot', 'signReleaseEvidence', "'repository'", "'commit-sha'", "'tree-sha'", "'git-ref'", "'workflow-identity'", "'run-id'", "'run-attempt'", "'runner-identity'", 'hashRegularFileStable(snapshot.verifierPath)', 'hashRegularFileStable(snapshot.bundletoolPath)', 'verifier_changed_during_verification', 'bundletool_changed_during_verification', ]) { assert(evidenceCreator.includes(required), `evidence_creator_missing_${required.replace(/[^a-z0-9]+/gi, '_')}`) } assert(!evidenceCreator.includes("'expected-cert-sha256'"), 'evidence_creator_legacy_certificate_argument') const publicationPreparer = readWorkspaceFile('scripts/ci/prepare-mobile-release-publication.mjs') for (const required of [ "'expected-upload-cert-sha256'", "'expected-repository'", "'expected-commit-sha'", "'expected-tree-sha'", "'expected-git-ref'", "'expected-workflow-identity'", "'expected-run-id'", "'expected-run-attempt'", "'expected-runner-identity'", "'expected-verifier-sha256'", "'expected-bundletool-sha256'", ]) { assert( publicationPreparer.includes(required), `publication_preparer_missing_${required.replace(/[^a-z0-9]+/gi, '_')}`, ) } assert( !publicationPreparer.includes("'expected-cert-sha256'"), 'publication_preparer_legacy_certificate_argument', ) const excluded = new Set([ resolve(workspaceRoot, 'scripts/ci/verify-mobile-release-boundary.mjs'), ]) for (const scriptPath of listScripts(resolve(workspaceRoot, 'scripts'))) { if (excluded.has(scriptPath)) continue const source = readFileSync(scriptPath, 'utf8') const consumesDebugApk = /outputs[\\/]apk[\\/]debug[\\/]app-debug\.apk/i.test(source) const publishesArtifact = /(copyFile|upload|forgejo|release asset|fetch\s*\()/i.test(source) assert(!(consumesDebugApk && publishesArtifact), `debug_apk_publish_script_${relative(workspaceRoot, scriptPath)}`) } const evidenceLibrary = readWorkspaceFile('scripts/ci/mobile-release-evidence-lib.mjs') for (const required of [ 'O_NOFOLLOW', 'fstatSync', 'COPYFILE_EXCL', 'source_changed_during_snapshot', 'hardlink_rejected', 'reparse_rejected', 'destination_directory_replaced', COMPROMISED_SIGNER_SHA256, 'compromised_signer_rejected', 'RELEASE_EVIDENCE_SCHEMA_VERSION = 2', 'validateReleaseProvenance', 'publication_provenance_', ]) { assert(evidenceLibrary.includes(required), `toctou_contract_missing_${required}`) } const artifactVerifier = readWorkspaceFile('scripts/ci/verify-android-artifact.mjs') for (const required of [ 'expected-upload-cert-sha256', "'dump', 'manifest'", "'jarsigner'", "'keytool'", 'aab_package_name_mismatch', 'aab_version_name_mismatch', 'aab_admob_application_id_mismatch', 'aab_signer_sha256_mismatch', COMPROMISED_SIGNER_SHA256, 'release_signer_sha256', '_compromised', '--self-test', 'assertArchivePathStable', 'artifact_hardlink', ]) { assert(artifactVerifier.includes(required), `artifact_verifier_contract_missing_${required.replace(/[^a-z0-9]+/gi, '_')}`) } assert(!artifactVerifier.includes("['expected-cert-sha256']"), 'artifact_verifier_legacy_certificate_argument') const appLinksVerifier = readWorkspaceFile('scripts/ci/verify-android-app-links.mjs') for (const required of [ 'expected-play-app-signing-cert-sha256', 'forbidden-upload-cert-sha256', COMPROMISED_SIGNER_SHA256, 'expectedPlayAppSigningCertificateVerified', 'uploadCertificateExcluded', '--self-test', ]) { assert(appLinksVerifier.includes(required), `app_links_verifier_contract_missing_${required.replace(/[^a-z0-9]+/gi, '_')}`) } const siteConfig = readWorkspaceFile('site/vite.config.ts') assert(siteConfig.includes('publicDir: false'), 'site_public_directory_not_isolated') assert(!siteConfig.includes("'releases/"), 'site_release_binary_allowlisted') for (const safePublicPath of [ 'site/public/.well-known/assetlinks.json', 'site/public/accept-invite.css', 'site/public/accept-invite.html', 'site/public/accept-invite.js', 'site/public/accept-invite/index.html', 'site/public/download.html', 'site/public/favicon.svg', ]) { const safePublicSource = readWorkspaceFile(safePublicPath) assert(!/(?:d3ro-voice[^"']*\.apk|git\.chanpaca\.net\/attachments\/(?:0b015367-dd8b-488c-8cc0-4db413b51792|d2e1b123-5678-496a-bf74-bc188938c999))/i.test(safePublicSource), `safe_public_mobile_reference_${safePublicPath}`) } const siteDeployWorkflow = readWorkspaceFile('.github/workflows/deploy-site.yml') assert(siteDeployWorkflow.includes('verify-mobile-release-boundary.mjs --self-test'), 'pages_deploy_boundary_gate_missing') const deploySite = readWorkspaceFile('scripts/deploy-site-to-nas.js') assert(deploySite.includes('assertNoMobileArtifacts'), 'nas_mobile_artifact_guard_missing') assert(!deploySite.includes('sync-and-publish-forgejo-release'), 'nas_legacy_release_sync_enabled') const apiProject = readWorkspaceFile('apps/api-server/D3ROVoice.Api.csproj') assert(apiProject.includes(''), 'api_static_release_exclusion_missing') for (const staleAsset of ['index-D7M5UQvT.js', 'index-JlYFxlAJ.js']) { assert(apiProject.includes(``), `api_stale_marketing_asset_publishable_${staleAsset}`) } const apiProgram = readWorkspaceFile('apps/api-server/Program.cs') assert(apiProgram.includes('mobileReleasePath') && apiProgram.indexOf('mobileReleasePath') < apiProgram.indexOf('app.UseStaticFiles()'), 'api_runtime_mobile_release_guard_missing') assert(apiProgram.includes('legacyMarketingAsset'), 'api_runtime_legacy_marketing_guard_missing') const osHook = readWorkspaceFile('site/src/hooks/useClientOS.ts') const androidConfig = osHook.match(/android:\s*\{[\s\S]*?\n\s*\},/)?.[0] ?? '' assert(androidConfig.includes("downloadUrl: '#download'"), 'android_download_not_unavailable') assert(!androidConfig.includes('attachments/'), 'android_attachment_link_enabled') const downloadUi = readWorkspaceFile('site/src/sections/Download.tsx') assert(downloadUi.includes('aria-disabled="true"'), 'android_download_ui_not_disabled') assert(!/attachments\/[0-9a-f-]+[\s\S]{0,120}\.apk/i.test(downloadUi), 'android_direct_attachment_enabled') } function expectFailure(label, operation, expectedCode) { let thrown = null try { operation() } catch (error) { thrown = error } assert(thrown instanceof Error, `${label}_did_not_fail`) assert(thrown.message.includes(expectedCode), `${label}_wrong_failure_${thrown.message}`) } function createFixture(root, privateKeyPem) { const sourceRoot = join(root, 'source') const apkDirectory = join(sourceRoot, 'apk', 'release') const aabDirectory = join(sourceRoot, 'bundle', 'release') mkdirSync(apkDirectory, { recursive: true }) mkdirSync(aabDirectory, { recursive: true }) const apkPath = join(apkDirectory, RELEASE_APK_NAME) const aabPath = join(aabDirectory, RELEASE_AAB_NAME) writeFileSync(apkPath, Buffer.from('fixture production apk\n'.repeat(80))) writeFileSync(aabPath, Buffer.from('fixture production aab\n'.repeat(90))) const apk = hashRegularFileStable(apkPath) const aab = hashRegularFileStable(aabPath) const verification = { artifact: RELEASE_APK_NAME, mode: 'release', packageName: RELEASE_PACKAGE_NAME, buildTools: 'fixture', apkSha256: apk.sha256, signerSha256: productionSigner, debuggable: false, adMobAppId: productionAdMobId, versionName, versionCode, bundleBytes: 123_456, modelBytes: 77_691_713, modelSha256: 'be07e048e1e599ad46341c8d2a135645097a538221678b7acdd1b1919c6e1b21', abis: ['arm64-v8a'], aab: { artifact: RELEASE_AAB_NAME, sha256: aab.sha256, packageName: RELEASE_PACKAGE_NAME, signerSha256: productionSigner, debuggable: false, adMobAppId: productionAdMobId, versionName, versionCode, bundleBytes: 123_456, modelBytes: 77_691_713, modelSha256: 'be07e048e1e599ad46341c8d2a135645097a538221678b7acdd1b1919c6e1b21', abis: ['arm64-v8a'], }, } const expected = { versionName, versionCode, adMobAppId: productionAdMobId, signerSha256: productionSigner, } const provenance = structuredClone(releaseProvenance) expected.provenance = structuredClone(provenance) const payload = buildReleasePayload({ verification, apkPath, aabPath, expected, provenance }) const evidence = signReleaseEvidence(payload, privateKeyPem) const evidencePath = join(sourceRoot, SIGNED_EVIDENCE_NAME) writeJsonCreateOnly(evidencePath, evidence) return { sourceRoot, apkPath, aabPath, evidencePath, evidence, payload, expected, provenance, verification, } } function verifyNegativeAndMaterializationTests() { const temporaryRoot = mkdtempSync(join(tmpdir(), 'd3ro-mobile-release-boundary-')) const trustedTempRoot = realpathSync(tmpdir()) const resolvedTemporaryRoot = realpathSync(temporaryRoot) assert( resolvedTemporaryRoot.startsWith(`${trustedTempRoot}${sep}`), 'temporary_root_outside_system_temp', ) try { const creatorOptions = { aab: 'fixture', apk: 'fixture', bundletool: 'fixture', 'commit-sha': releaseProvenance.commitSha, 'expected-admob-app-id': productionAdMobId, 'expected-upload-cert-sha256': productionSigner, 'expected-version-code': versionCode, 'expected-version-name': versionName, 'git-ref': releaseProvenance.gitRef, 'private-key': 'fixture', repository: releaseProvenance.repository, 'run-attempt': releaseProvenance.runAttempt, 'run-id': releaseProvenance.runId, 'runner-identity': releaseProvenance.runnerIdentity, 'snapshot-dir': 'fixture', 'tree-sha': releaseProvenance.treeSha, 'workflow-identity': releaseProvenance.workflowIdentity, } for (const name of [ 'repository', 'commit-sha', 'tree-sha', 'git-ref', 'workflow-identity', 'run-id', 'run-attempt', 'runner-identity', ]) { runCliExpectedFailure( 'scripts/ci/create-mobile-release-evidence.mjs', creatorOptions, name, `create_evidence_argument_missing_${name}`, ) } const publicationOptions = { aab: 'fixture', apk: 'fixture', 'destination-dir': 'fixture', evidence: 'fixture', 'expected-admob-app-id': productionAdMobId, 'expected-bundletool-sha256': releaseProvenance.bundletoolSha256, 'expected-commit-sha': releaseProvenance.commitSha, 'expected-git-ref': releaseProvenance.gitRef, 'expected-repository': releaseProvenance.repository, 'expected-run-attempt': releaseProvenance.runAttempt, 'expected-run-id': releaseProvenance.runId, 'expected-runner-identity': releaseProvenance.runnerIdentity, 'expected-tree-sha': releaseProvenance.treeSha, 'expected-upload-cert-sha256': productionSigner, 'expected-verifier-sha256': releaseProvenance.verifierSha256, 'expected-version-code': versionCode, 'expected-version-name': versionName, 'expected-workflow-identity': releaseProvenance.workflowIdentity, 'public-key': 'fixture', 'source-root': 'fixture', } for (const name of [ 'expected-repository', 'expected-commit-sha', 'expected-tree-sha', 'expected-git-ref', 'expected-workflow-identity', 'expected-run-id', 'expected-run-attempt', 'expected-runner-identity', 'expected-verifier-sha256', 'expected-bundletool-sha256', ]) { runCliExpectedFailure( 'scripts/ci/prepare-mobile-release-publication.mjs', publicationOptions, name, `prepare_publication_argument_missing_${name}`, ) } runVerifierSelfTest('scripts/ci/verify-android-artifact.mjs') runVerifierSelfTest('scripts/ci/verify-android-app-links.mjs') const keyPair = generateKeyPairSync('ed25519') const otherKeyPair = generateKeyPairSync('ed25519') const privateKeyPem = keyPair.privateKey.export({ type: 'pkcs8', format: 'pem' }) const publicKeyPem = keyPair.publicKey.export({ type: 'spki', format: 'pem' }) const publicKeyPath = join(temporaryRoot, 'trusted-public.pem') const otherPublicKeyPath = join(temporaryRoot, 'other-public.pem') writeFileSync(publicKeyPath, publicKeyPem, { flag: 'wx', mode: 0o600 }) writeFileSync( otherPublicKeyPath, otherKeyPair.publicKey.export({ type: 'spki', format: 'pem' }), { flag: 'wx', mode: 0o600 }, ) const fixture = createFixture(temporaryRoot, privateKeyPem) const rawSnapshotSource = join(temporaryRoot, 'raw-snapshot-source') mkdirSync(rawSnapshotSource) const rawApk = join(rawSnapshotSource, RELEASE_APK_NAME) const rawAab = join(rawSnapshotSource, RELEASE_AAB_NAME) const rawVerifier = join(rawSnapshotSource, 'verify-android-artifact.mjs') const rawBundletool = join(rawSnapshotSource, 'bundletool-fixture.jar') writeFileSync(rawApk, 'raw apk snapshot fixture') writeFileSync(rawAab, 'raw aab snapshot fixture') writeFileSync(rawVerifier, 'raw verifier snapshot fixture') writeFileSync(rawBundletool, 'raw bundletool snapshot fixture') const immutableSnapshot = createImmutableVerificationSnapshot({ apkPath: rawApk, aabPath: rawAab, destinationDirectory: join(temporaryRoot, 'immutable-verification-snapshot'), verifierPath: rawVerifier, bundletoolPath: rawBundletool, }) assert(hashRegularFileStable(immutableSnapshot.apkPath).sha256 === hashRegularFileStable(rawApk).sha256, 'immutable_apk_snapshot_mismatch') assert(hashRegularFileStable(immutableSnapshot.aabPath).sha256 === hashRegularFileStable(rawAab).sha256, 'immutable_aab_snapshot_mismatch') assert(hashRegularFileStable(immutableSnapshot.verifierPath).sha256 === hashRegularFileStable(rawVerifier).sha256, 'immutable_verifier_snapshot_mismatch') assert(hashRegularFileStable(immutableSnapshot.bundletoolPath).sha256 === hashRegularFileStable(rawBundletool).sha256, 'immutable_bundletool_snapshot_mismatch') const destination = join(temporaryRoot, 'sealed-release') const prepared = prepareVerifiedReleasePublication({ ...fixture, publicKeyPath, destinationDirectory: destination, }) assert(prepared.manifest.packageName === RELEASE_PACKAGE_NAME, 'happy_package_mismatch') assert(existsSync(join(destination, RELEASE_APK_NAME)), 'happy_apk_missing') assert(existsSync(join(destination, RELEASE_AAB_NAME)), 'happy_aab_missing') expectFailure('overwrite', () => prepareVerifiedReleasePublication({ ...fixture, publicKeyPath, destinationDirectory: destination, }), 'destination_must_not_exist') expectFailure('wrong_key', () => prepareVerifiedReleasePublication({ ...fixture, publicKeyPath: otherPublicKeyPath, destinationDirectory: join(temporaryRoot, 'wrong-key-output'), }), 'signature_key_mismatch') expectFailure('wrong_version', () => prepareVerifiedReleasePublication({ ...fixture, publicKeyPath, destinationDirectory: join(temporaryRoot, 'wrong-version-output'), expected: { ...fixture.expected, versionName: '9.8.8' }, }), 'publication_version_name_mismatch') expectFailure('wrong_certificate', () => prepareVerifiedReleasePublication({ ...fixture, publicKeyPath, destinationDirectory: join(temporaryRoot, 'wrong-certificate-output'), expected: { ...fixture.expected, signerSha256: '1'.repeat(64) }, }), 'publication_signer_mismatch') expectFailure('compromised_expected_publication_certificate', () => prepareVerifiedReleasePublication({ ...fixture, publicKeyPath, destinationDirectory: join(temporaryRoot, 'compromised-certificate-output'), expected: { ...fixture.expected, signerSha256: COMPROMISED_SIGNER_SHA256 }, }), 'publication_expected_compromised_signer') expectFailure('wrong_admob', () => prepareVerifiedReleasePublication({ ...fixture, publicKeyPath, destinationDirectory: join(temporaryRoot, 'wrong-admob-output'), expected: { ...fixture.expected, adMobAppId: 'ca-app-pub-1234567890123456~1234567891' }, }), 'publication_admob_app_id_mismatch') for (const [field, mismatch, code] of [ ['repository', 'attacker/D3ROVoice', 'repository'], ['commitSha', 'e'.repeat(40), 'commit_sha'], ['treeSha', 'f'.repeat(40), 'tree_sha'], ['gitRef', 'refs/tags/v9.8.8', 'git_ref'], ['workflowIdentity', 'github:.github/workflows/release.yml:attacker', 'workflow_identity'], ['runId', '987654321', 'run_id'], ['runAttempt', 2, 'run_attempt'], ['runnerIdentity', 'self-hosted:attacker', 'runner_identity'], ['verifierSha256', '1'.repeat(64), 'verifier_sha256'], ['bundletoolSha256', '2'.repeat(64), 'bundletool_sha256'], ]) { expectFailure(`wrong_provenance_${code}`, () => prepareVerifiedReleasePublication({ ...fixture, publicKeyPath, destinationDirectory: join(temporaryRoot, `wrong-provenance-${code}-output`), expected: { ...fixture.expected, provenance: { ...fixture.expected.provenance, [field]: mismatch }, }, }), `publication_provenance_${code}_mismatch`) } const missingProvenanceField = structuredClone(fixture.payload) delete missingProvenanceField.provenance.treeSha expectFailure('missing_provenance_field', () => signReleaseEvidence( missingProvenanceField, privateKeyPem, ), 'provenance_keys_invalid') expectFailure('extra_provenance_field', () => signReleaseEvidence({ ...fixture.payload, provenance: { ...fixture.payload.provenance, untrusted: 'extra' }, }, privateKeyPem), 'provenance_keys_invalid') expectFailure('noncanonical_run_attempt', () => signReleaseEvidence({ ...fixture.payload, provenance: { ...fixture.payload.provenance, runAttempt: '01' }, }, privateKeyPem), 'provenance_run_attempt_invalid') const missingExpectedProvenanceField = structuredClone(fixture.expected.provenance) delete missingExpectedProvenanceField.runnerIdentity expectFailure('missing_expected_provenance_field', () => prepareVerifiedReleasePublication({ ...fixture, publicKeyPath, destinationDirectory: join(temporaryRoot, 'missing-expected-provenance-output'), expected: { ...fixture.expected, provenance: missingExpectedProvenanceField }, }), 'expected_provenance_keys_invalid') const legacySchemaFixture = createFixture(join(temporaryRoot, 'legacy-schema-case'), privateKeyPem) const legacySchemaEvidence = { ...legacySchemaFixture.evidence, schemaVersion: 1 } writeFileSync( legacySchemaFixture.evidencePath, `${JSON.stringify(legacySchemaEvidence)}\n`, { flag: 'w', mode: 0o600 }, ) expectFailure('legacy_evidence_schema', () => prepareVerifiedReleasePublication({ ...legacySchemaFixture, publicKeyPath, destinationDirectory: join(temporaryRoot, 'legacy-schema-output'), }), 'evidence_schema_version') const tamperedFixture = createFixture(join(temporaryRoot, 'tampered-case'), privateKeyPem) const tamperedEvidence = structuredClone(tamperedFixture.evidence) tamperedEvidence.payload.versionCode += 1 writeFileSync( tamperedFixture.evidencePath, `${JSON.stringify(tamperedEvidence)}\n`, { flag: 'w', mode: 0o600 }, ) expectFailure('tampered_signature', () => prepareVerifiedReleasePublication({ ...tamperedFixture, publicKeyPath, destinationDirectory: join(temporaryRoot, 'tampered-output'), }), 'signature_invalid') const provenanceTamperedFixture = createFixture( join(temporaryRoot, 'provenance-tampered-case'), privateKeyPem, ) const provenanceTamperedEvidence = structuredClone(provenanceTamperedFixture.evidence) provenanceTamperedEvidence.payload.provenance.commitSha = 'e'.repeat(40) writeFileSync( provenanceTamperedFixture.evidencePath, `${JSON.stringify(provenanceTamperedEvidence)}\n`, { flag: 'w', mode: 0o600 }, ) expectFailure('tampered_provenance_signature', () => prepareVerifiedReleasePublication({ ...provenanceTamperedFixture, publicKeyPath, destinationDirectory: join(temporaryRoot, 'provenance-tampered-output'), }), 'signature_invalid') expectFailure('nonrelease_mode', () => signReleaseEvidence({ ...fixture.payload, mode: 'e2e', }, privateKeyPem), 'nonrelease_mode_rejected') expectFailure('debuggable', () => signReleaseEvidence({ ...fixture.payload, debuggable: true, }, privateKeyPem), 'debuggable_release_rejected') expectFailure('test_admob', () => signReleaseEvidence({ ...fixture.payload, adMobAppId: 'ca-app-pub-3940256099942544~3347511713', }, privateKeyPem), 'test_admob_rejected') expectFailure('debug_signer', () => signReleaseEvidence({ ...fixture.payload, signerSha256: 'fac61745dc0903786fb9ede62a962b399f7348f0bb6f899b8332667591033b9c', }, privateKeyPem), 'debug_signer_rejected') expectFailure('compromised_signer', () => signReleaseEvidence({ ...fixture.payload, signerSha256: COMPROMISED_SIGNER_SHA256, }, privateKeyPem), 'compromised_signer_rejected') expectFailure('package_name', () => signReleaseEvidence({ ...fixture.payload, packageName: 'com.attacker.voice', }, privateKeyPem), 'package_name_mismatch') expectFailure('path_escape_name', () => signReleaseEvidence({ ...fixture.payload, apk: { ...fixture.payload.apk, fileName: '../app-release.apk' }, }, privateKeyPem), 'apk_file_name_invalid') for (const [label, patch, code] of [ ['aab_package', { packageName: 'com.attacker.voice' }, 'verification_aab_package_mismatch'], ['aab_version_name', { versionName: '9.8.8' }, 'aab_apk_version_name_mismatch'], ['aab_version_code', { versionCode: versionCode + 1 }, 'aab_apk_version_code_mismatch'], ['aab_admob', { adMobAppId: 'ca-app-pub-1234567890123456~1234567891' }, 'aab_apk_admob_app_id_mismatch'], ['aab_signer', { signerSha256: '1'.repeat(64) }, 'aab_apk_signer_mismatch'], ['aab_compromised_signer', { signerSha256: COMPROMISED_SIGNER_SHA256 }, 'verification_aab_compromised_signer'], ['aab_debuggable', { debuggable: true }, 'verification_aab_debuggable'], ]) { expectFailure(label, () => buildReleasePayload({ verification: { ...fixture.verification, aab: { ...fixture.verification.aab, ...patch } }, apkPath: fixture.apkPath, aabPath: fixture.aabPath, expected: fixture.expected, provenance: fixture.provenance, }), code) } expectFailure('verification_compromised_signer', () => buildReleasePayload({ verification: { ...fixture.verification, signerSha256: COMPROMISED_SIGNER_SHA256 }, apkPath: fixture.apkPath, aabPath: fixture.aabPath, expected: fixture.expected, provenance: fixture.provenance, }), 'verification_compromised_signer') expectFailure('expected_compromised_signer', () => buildReleasePayload({ verification: fixture.verification, apkPath: fixture.apkPath, aabPath: fixture.aabPath, expected: { ...fixture.expected, signerSha256: COMPROMISED_SIGNER_SHA256 }, provenance: fixture.provenance, }), 'expected_compromised_signer') const hardlinkPath = join(temporaryRoot, 'hardlinked.apk') linkSync(fixture.apkPath, hardlinkPath) expectFailure('hardlink_source', () => hashRegularFileStable(hardlinkPath), 'hardlink_rejected') rmSync(hardlinkPath) const unsafeSite = join(temporaryRoot, 'unsafe-site') mkdirSync(unsafeSite) writeFileSync(join(unsafeSite, 'legacy.js'), 'location.href="https://git.chanpaca.net/attachments/0b015367-dd8b-488c-8cc0-4db413b51792"') expectFailure('static_link_bypass', () => assertNoMobileArtifacts(unsafeSite), 'Legacy mobile download link blocked') rmSync(join(unsafeSite, 'legacy.js')) writeFileSync(join(unsafeSite, 'unsealed.apk'), 'not a release') expectFailure('static_apk_bypass', () => assertNoMobileArtifacts(unsafeSite), 'Unsealed mobile artifact blocked') const outsideRoot = join(temporaryRoot, 'outside') mkdirSync(outsideRoot) const outsideApk = join(outsideRoot, RELEASE_APK_NAME) writeFileSync(outsideApk, 'outside') expectFailure('outside_source_root', () => prepareVerifiedReleasePublication({ ...fixture, apkPath: outsideApk, publicKeyPath, destinationDirectory: join(temporaryRoot, 'outside-output'), }), `${RELEASE_APK_NAME}_outside_source_root`) const swappedOriginal = `${fixture.apkPath}.original` renameSync(fixture.apkPath, swappedOriginal) writeFileSync(fixture.apkPath, Buffer.alloc(fixture.payload.apk.bytes, 0x58)) expectFailure('artifact_path_swap', () => prepareVerifiedReleasePublication({ ...fixture, publicKeyPath, destinationDirectory: join(temporaryRoot, 'hash-output'), }), `${RELEASE_APK_NAME}_hash_mismatch`) assert(!existsSync(join(temporaryRoot, 'hash-output')), 'failed_snapshot_destination_not_cleaned') expectFailure('verification_nonrelease', () => buildReleasePayload({ verification: { ...fixture.verification, mode: 'e2e' }, apkPath: fixture.apkPath, aabPath: fixture.aabPath, expected: fixture.expected, provenance: fixture.provenance, }), 'verification_nonrelease_mode') const createOnlyPath = join(temporaryRoot, 'create-only.json') writeJsonCreateOnly(createOnlyPath, { first: true }) expectFailure('create_only_evidence', () => writeJsonCreateOnly(createOnlyPath, { second: true }), 'EEXIST') } finally { const finalRoot = realpathSync(temporaryRoot) if (!finalRoot.startsWith(`${trustedTempRoot}${sep}`)) fail('cleanup_target_outside_temp') rmSync(finalRoot, { recursive: true, force: false }) } } const cliArguments = process.argv.slice(2) const selfTest = cliArguments.includes('--self-test') if (cliArguments.some((argument) => argument !== '--self-test')) fail('unexpected_argument') verifySourceContracts() if (selfTest) verifyNegativeAndMaterializationTests() process.stdout.write(`${canonicalJson({ ok: true, sourceContracts: true, negativeSelfTests: selfTest, })}\n`)