From f3f1f14fb1a4521cf8b257b1c9dedb4e45982edf Mon Sep 17 00:00:00 2001 From: NINI Date: Mon, 5 Oct 2026 03:51:27 +0900 Subject: [PATCH] Add negative Android provenance checks and short tap exercise --- docs/android-first-slice.md | 2 ++ tools/android/exercise-first-slice.ps1 | 6 +++--- tools/android/test-provenance.ps1 | 26 ++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 tools/android/test-provenance.ps1 diff --git a/docs/android-first-slice.md b/docs/android-first-slice.md index 6874b5a..8db0097 100644 --- a/docs/android-first-slice.md +++ b/docs/android-first-slice.md @@ -17,6 +17,8 @@ pwsh -NoProfile -File tools/verify.ps1 -Phase AndroidSmoke -Milestone FirstSlice `compare-audio.ps1`은 승인 원본과 실제 PCM을 mono PCM16 24kHz로 맞추고 가장 강한 구간의 위치를 찾은 후 전체 발화의 정규화 상관을 평가합니다. 기준은 상관 ≥0.95, RMS >32, 비무음 샘플 >100입니다. 세 발화의 시작/끝이 차례로 분리되어야 합니다. APK 생성 출처는 원본 `androidbuild-evidence.json`과 `apk-metadata.json`을 유지하고, Smoke의 재검사는 `apk-inspection.json`에만 씁니다. 시작/종료 dirty=false와 소스 SHA·APK hash를 대조하므로 이전 APK를 후속 커밋 산출물로 다시 이름 붙일 수 없습니다. 잘못된 SHA/hash 검사 복사본은 실제 기기 조작 전 실패해야 합니다. +그 거부 동작은 실제 Build 뒤 `pwsh -NoProfile -File tools/android/test-provenance.ps1`로 검사합니다. 원본 근거를 손상시키지 않고 별도 복사본 두 개의 소스 SHA/APK hash만 바꿔 Smoke의 실제 종료 1 및 기기 명령 0개를 확인합니다. 음성은 raw WAV뿐 아니라 수집 당시 소스 SHA/dirty·시각 메타데이터도 최종 Build와 맞아야 합니다. 입력 수정 전 ADB 짧은 `input tap` 두 번은 구 탭을 놓쳤고 같은 좌표 200ms 접촉은 정상 반응했습니다. 수정 전 원시 자료는 `pre-fix/`에 보존하고 수정 후 최종 절차는 짧은 ADB tap으로 다시 확인합니다. 물리 기기의 실제 손가락 동작이나 성능 합격으로 확대하지 않습니다. + APK는 `artifacts/android/FirstSlice/MagnetismHeroes.apk`, 원본 경로를 기록한 Unity 실제 packed-assets 목록은 APK 옆 `.packed-assets.txt`입니다. 근거는 `artifacts/verification/FirstSlice/`에 보존합니다. Build/Smoke의 명령·실제 종료 코드·소스 SHA/dirty·APK SHA-256, aapt 실제 badging/ZIP entries, 기기 ABI/번역기/graphics, ADB 명령별 종료 코드·화면·관찰 결과를 별도로 기록합니다. 설치·프로세스 생존만으로 게임 검사를 통과시키지 않습니다. `tools/android/observe.ps1`은 실제 ADB 조작 후 screencap을 가져오고 각 명령을 `-commands.json`에 기록합니다. Unity uGUI 문자는 Android native hierarchy에 노출되지 않으므로 PNG를 직접 열어 한국어와 화면 의미를 검사합니다. `android-observations.json`에는 아래 번호별 절차·기대와 실제 결과·근거 파일을 작성하며 APK hash·소스 SHA·기기 serial이 맞아야 합니다. 미실행·빈 파일·관찰 누락은 Smoke 실패입니다. 원본 상태는 앱 전용 저장 JSON을 ADB로 읽을 수 있을 때 추가 검증하며 release APK에 진단 서버·단계 우회·테스트 제어 기능을 넣지 않습니다. diff --git a/tools/android/exercise-first-slice.ps1 b/tools/android/exercise-first-slice.ps1 index 04e15a5..88b4a39 100644 --- a/tools/android/exercise-first-slice.ps1 +++ b/tools/android/exercise-first-slice.ps1 @@ -39,7 +39,7 @@ try { Step 'final-02-selection' @('shell','input','tap','1200','616') Start-Audio 'audio-online-guidecollect' 8 Step 'final-03-stage' @('shell','input','tap','1200','560') 2500 - Step 'final-04-precollect-tap' @('shell','input','swipe','930','510','930','510','200') + Step 'final-04-precollect-tap' @('shell','input','tap','930','510') $uncollected=Save-State 'final-04-uncollected' Assert-State 'direct-collection-required' ($uncollected.puzzleStates[0].collectedBallIds.Count -eq 0 -and $uncollected.puzzleStates[0].countedBallIds.Count -eq 0 -and $uncollected.completedStageIds.Count -eq 0) ($uncollected | ConvertTo-Json -Depth 8 -Compress) Start-Audio 'audio-online-guidecount' 8 @@ -53,7 +53,7 @@ try { $after=Save-State 'final-07-incomplete-after' Assert-State 'incomplete-json-restored' (($before | ConvertTo-Json -Depth 8 -Compress) -eq ($after | ConvertTo-Json -Depth 8 -Compress)) '배치/카메라/그룹/수집/빈 계수 전체 JSON 동일' Start-Audio 'audio-online-count01' 8 - Step 'final-08-success-same-board' @('shell','input','swipe','1182','514','1182','514','200') 1500 + Step 'final-08-success-same-board' @('shell','input','tap','1182','514') 1500 $success=Save-State 'final-08-success' Assert-State 'completion-atomic-state' ($success.completedStageIds.Count -eq 1 -and $success.completedStageIds[0] -eq 1 -and $success.puzzleStates.Count -eq 0 -and $success.latestFreePlay.sourceStageId -eq 1) ($success | ConvertTo-Json -Depth 8 -Compress) Assert-State 'success-preserves-board-camera' (($before.puzzleStates[0].balls | ConvertTo-Json -Depth 6 -Compress) -eq ($success.latestFreePlay.balls | ConvertTo-Json -Depth 6 -Compress) -and ($before.puzzleStates[0].camera | ConvertTo-Json -Depth 6 -Compress) -eq ($success.latestFreePlay.camera | ConvertTo-Json -Depth 6 -Compress)) '성공 전후 balls/camera 동일' @@ -88,7 +88,7 @@ try { Step 'final-12-offline-collect' @('shell','input','swipe','930','510','1200','510','800') 4500 $offlineCollected=Save-State 'final-12-offline-collected' Assert-State 'offline-collected-unfinished' ($offlineCollected.puzzleStates.Count -eq 1 -and $offlineCollected.puzzleStates[0].collectedBallIds.Count -eq 1 -and $offlineCollected.puzzleStates[0].countedBallIds.Count -eq 0) '오프라인 실제 수집됨/계수 전 0' - Step 'final-12-offline-count-success' @('shell','input','swipe','1182','510','1182','510','200') 1500 + Step 'final-12-offline-count-success' @('shell','input','tap','1182','510') 1500 $offlineSuccess=Save-State 'final-12-offline-success' Assert-State 'offline-collection-count-actually-success' ($offlineSuccess.completedStageIds.Count -eq 1 -and $offlineSuccess.completedStageIds[0] -eq 1 -and $offlineSuccess.puzzleStates.Count -eq 0 -and $offlineSuccess.latestFreePlay.sourceStageId -eq 1 -and ($offlineCollected.puzzleStates[0].balls | ConvertTo-Json -Depth 6 -Compress) -eq ($offlineSuccess.latestFreePlay.balls | ConvertTo-Json -Depth 6 -Compress)) '완료1/미완료없음/수집 전후 같은 IDs와 배치 자유 전이' Step 'final-13-offline-free-move' @('shell','input','swipe','1182','510','1350','450','800') diff --git a/tools/android/test-provenance.ps1 b/tools/android/test-provenance.ps1 new file mode 100644 index 0000000..f8d7918 --- /dev/null +++ b/tools/android/test-provenance.ps1 @@ -0,0 +1,26 @@ +param([string]$EvidenceDir='artifacts/verification/FirstSlice') +$ErrorActionPreference='Stop' +$EvidenceDir=(Resolve-Path -LiteralPath $EvidenceDir).Path +$apkPath=(Resolve-Path -LiteralPath 'artifacts/android/FirstSlice/MagnetismHeroes.apk').Path +$source=& git rev-parse HEAD +$build=Get-Content -LiteralPath (Join-Path $EvidenceDir 'androidbuild-evidence.json') -Raw | ConvertFrom-Json +if ($build.sourceCommit -ne $source -or $build.sourceDirty -ne $false -or $build.sourceDirtyEnd -ne $false -or $build.apkSha256 -ne (Get-FileHash -LiteralPath $apkPath).Hash) { throw '현재 clean 소스의 실제 Build 근거가 있어야 부정 검사를 실행할 수 있습니다.' } +$results=@() +foreach ($field in @('sourceCommit','apkSha256')) { + $caseDir=Join-Path $EvidenceDir "red/provenance-$field" + New-Item -ItemType Directory -Path $caseDir -Force | Out-Null + $copy=Get-Content -LiteralPath (Join-Path $EvidenceDir 'androidbuild-evidence.json') -Raw | ConvertFrom-Json + $copy.$field='deliberately-invalid-verification-copy' + $copy | ConvertTo-Json -Depth 10 | Set-Content -LiteralPath (Join-Path $caseDir 'androidbuild-evidence.json') -Encoding utf8 + Copy-Item -LiteralPath (Join-Path $EvidenceDir 'apk-metadata.json') -Destination (Join-Path $caseDir 'apk-metadata.json') + $log=Join-Path $caseDir 'negative-smoke.log' + $smoke=Join-Path $PSScriptRoot 'smoke.ps1' + & pwsh -NoProfile -File $smoke -ApkPath $apkPath -Milestone FirstSlice -EvidenceDir $caseDir *> $log + $code=$LASTEXITCODE + $outcome=Get-Content -LiteralPath (Join-Path $caseDir 'android-device-evidence.json') -Raw | ConvertFrom-Json + $passed=$code -eq 1 -and $outcome.status -eq 'failed' -and @($outcome.commands).Count -eq 0 -and $outcome.checks[-1].criterion -eq 'verified-build-provenance' -and $outcome.checks[-1].passed -eq $false + $results += [ordered]@{ field=$field; command="pwsh -NoProfile -File $smoke -ApkPath $apkPath -Milestone FirstSlice -EvidenceDir $caseDir"; actualExitCode=$code; deviceCommandCount=@($outcome.commands).Count; passed=$passed; log=$log } +} +[ordered]@{ sourceCommit=$source; apkSha256=$build.apkSha256; cases=$results } | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $EvidenceDir 'negative-provenance-results.json') -Encoding utf8 +if (@($results | Where-Object passed -ne $true).Count) { throw '출처가 틀린 검사 복사본을 기기 조작 전에 거부하지 못했습니다.' } +Write-Host '소스 SHA/APK hash 손상 복사본 2개를 실제 종료 코드 1로 기기 조작 전 거부했습니다.'