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 gitlabWorkflow = readWorkspaceFile('.gitlab-ci.yml')
const mobileProductionJobStart = gitlabWorkflow.indexOf('mobile-production-release:\n')
const publishReleaseJobStart = gitlabWorkflow.indexOf('\npublish-release:\n')
assert(mobileProductionJobStart >= 0, 'gitlab_mobile_production_job_missing')
assert(publishReleaseJobStart > mobileProductionJobStart, 'gitlab_publish_release_job_missing')
const mobileProductionJob = gitlabWorkflow.slice(mobileProductionJobStart, publishReleaseJobStart)
const publishReleaseJob = gitlabWorkflow.slice(publishReleaseJobStart)
for (const required of [
'git fetch --no-tags origin main:refs/remotes/origin/main',
'test "${CI_COMMIT_REF_PROTECTED:-}" = "true"',
'test -z "${CI_COMMIT_BRANCH:-}"',
'test "$CI_COMMIT_TAG" = "v${VERSION_NAME}"',
'test "$(git rev-parse "refs/tags/${CI_COMMIT_TAG}^{commit}")" = "$CI_COMMIT_SHA"',
'RELEASE_GIT_REF="refs/tags/${CI_COMMIT_TAG}"',
'test "${CI_COMMIT_BRANCH:-}" = "main"',
'test "$CI_COMMIT_SHA" = "$(git rev-parse origin/main)"',
'RELEASE_GIT_REF="refs/heads/main"',
'--git-ref "$RELEASE_GIT_REF"',
'--expected-git-ref "$RELEASE_GIT_REF"',
'$CI_COMMIT_REF_PROTECTED == "true" && $CI_COMMIT_TAG =~ /^v\\d+\\.\\d+\\.\\d+$/',
'$CI_COMMIT_REF_PROTECTED == "true" && $CI_COMMIT_BRANCH == "main"',
]) {
assert(
mobileProductionJob.includes(required),
`gitlab_mobile_candidate_contract_missing_${required.replace(/[^a-z0-9]+/gi, '_')}`,
)
}
assert(
(mobileProductionJob.match(/\n\s+when: manual/g) ?? []).length === 2,
'gitlab_mobile_candidate_manual_rules_invalid',
)
assert(
!mobileProductionJob.includes('--git-ref "refs/tags/$CI_COMMIT_TAG"'),
'gitlab_mobile_evidence_ref_tag_only',
)
assert(
publishReleaseJob.includes('$CI_COMMIT_TAG =~ /^v\\d+\\.\\d+\\.\\d+.*$/'),
'gitlab_public_release_stable_tag_rule_missing',
)
assert(!publishReleaseJob.includes('CI_COMMIT_BRANCH'), 'gitlab_public_release_branch_trigger_enabled')
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`)