Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,19 @@ jobs:
run: |
chmod +x gradlew
./gradlew :app:testFossDebugUnitTest :app:assembleFossDebug :app:assembleFossRelease --no-daemon --stacktrace
- name: Test updater with real Linux apksigner and debug APK
shell: pwsh
run: |
$apk = (Resolve-Path 'app/build/outputs/apk/foss/debug/mumla-foss-debug.apk').Path
$badging = & "$env:ANDROID_HOME/build-tools/36.0.0/aapt" dump badging $apk
$package = [regex]::Match(($badging -join "`n"), "package: name='([^']+)' versionCode='([0-9]+)' versionName='([^']+)'" )
if (-not $package.Success) { throw 'Could not parse debug APK identity.' }
$env:MINIMUM_TEST_SIGNED_APK = $apk
$env:MINIMUM_TEST_EXPECTED_APPLICATION_ID = $package.Groups[1].Value
$env:MINIMUM_TEST_EXPECTED_VERSION_CODE = $package.Groups[2].Value
$env:MINIMUM_TEST_EXPECTED_VERSION_NAME = $package.Groups[3].Value
./tools/verify-cellular-policy.ps1
./tests/update-minimum-device.Tests.ps1
- name: Upload debug APK
uses: actions/upload-artifact@v4
with:
Expand Down
62 changes: 49 additions & 13 deletions scripts/update-minimum-device.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -358,17 +358,41 @@ function Find-ApkSignerInSdkRoots {

function Parse-ApkSignerOutput {
param([string]$Text)
# PowerShell 7 wraps some extensionless native-command output as ErrorRecord text on Linux,
# which can prefix the original line. Strip terminal control sequences and locate the exact
# apksigner label without requiring it to begin the rendered PowerShell line.
# Process capture returns raw streams; accept only anchored apksigner structural
# lines after removing terminal controls. PEM certificates are authoritative.
$normalized = [regex]::Replace($Text, '\x1B\[[0-?]*[ -/]*[@-~]', '')
$digests = @([regex]::Matches($normalized,
'(?i)Signer #\d+ certificate SHA-256 digest:\s*([0-9a-f]{64})(?![0-9a-f])') |
$textDigests = @([regex]::Matches($normalized,
'(?im)^Signer #\d+ certificate SHA-256 digest:\s*([0-9a-f]{64})\s*$') |
ForEach-Object { $_.Groups[1].Value.ToUpperInvariant() })
if ($digests.Count -eq 0) {
Throw-UpdateError "APK_SIGNATURE_INVALID" "apksigner did not report a verified signing certificate."
$pemBlocks = @([regex]::Matches($normalized,
'(?s)-----BEGIN CERTIFICATE-----\s*(.*?)\s*-----END CERTIFICATE-----'))
$pemDigests = @($pemBlocks | ForEach-Object {
try {
$der = [Convert]::FromBase64String(([regex]::Replace($_.Groups[1].Value, '\s', '')))
$certificate = New-Object Security.Cryptography.X509Certificates.X509Certificate2 -ArgumentList @(,$der)
$sha256 = [Security.Cryptography.SHA256]::Create()
try { ([BitConverter]::ToString($sha256.ComputeHash($certificate.RawData))).Replace('-', '') }
finally { $sha256.Dispose(); $certificate.Dispose() }
} catch {
Throw-UpdateError "APK_SIGNATURE_INVALID" "apksigner returned an invalid signer certificate."
}
})
$countLines = @([regex]::Matches($normalized, '(?im)^Number of signers:\s*([0-9]+)\s*$'))
if ($countLines.Count -ne 1 -or $pemDigests.Count -eq 0 -or
[int]$countLines[0].Groups[1].Value -ne $pemDigests.Count -or
@($pemDigests | Select-Object -Unique).Count -ne $pemDigests.Count) {
Throw-UpdateError "APK_SIGNATURE_INVALID" "apksigner did not report an exact verified signer certificate set."
}
if ($textDigests.Count -gt 0) {
$textSet = @($textDigests | Sort-Object)
$pemSet = @($pemDigests | Sort-Object)
if ($textDigests.Count -ne $pemDigests.Count -or
@($textDigests | Select-Object -Unique).Count -ne $textDigests.Count -or
(Compare-Object $textSet $pemSet)) {
Throw-UpdateError "APK_SIGNATURE_INVALID" "apksigner textual and certificate signer sets disagree."
}
}
return $digests
return $pemDigests
}

function Stop-ApkSignerProcess {
Expand Down Expand Up @@ -415,19 +439,31 @@ function Invoke-ApkSignerProcess {
$start.RedirectStandardOutput = $true
$start.RedirectStandardError = $true
if ($ApkSigner.EndsWith(".bat", [StringComparison]::OrdinalIgnoreCase)) {
if (-not $env:ComSpec) {
Throw-UpdateError "APKSIGNER_MISSING" "The Windows command processor required for apksigner.bat is unavailable."
}
# cmd expands percent variables even inside quotes. Refuse percent rather
# than allow either path to be rewritten before the reviewed tool runs.
if ($ApkPath.Contains('%') -or $ApkSigner.Contains('%')) {
Throw-UpdateError "APK_SIGNATURE_INVALID" "Unsafe character in the Windows APK or apksigner path."
}
if (-not $env:ComSpec) {
Throw-UpdateError "APKSIGNER_MISSING" "The Windows command processor required for apksigner.bat is unavailable."
}
$start.FileName = $env:ComSpec
$start.Arguments = "/d /s /v:off /c `"`"$ApkSigner`" verify --verbose --print-certs `"$ApkPath`"`""
$start.Arguments = "/d /s /v:off /c `"`"$ApkSigner`" verify --verbose --print-certs --print-certs-pem `"$ApkPath`"`""
} else {
$start.FileName = $ApkSigner
$start.Arguments = "verify --verbose --print-certs `"$ApkPath`""
if ($start.PSObject.Properties["ArgumentList"]) {
# .NET Core exposes a true argv collection. Use it for the Unix
# extensionless launcher so paths are never reparsed as one string.
$start.ArgumentList.Add("verify")
$start.ArgumentList.Add("--verbose")
$start.ArgumentList.Add("--print-certs")
$start.ArgumentList.Add("--print-certs-pem")
$start.ArgumentList.Add($ApkPath)
} else {
# Windows PowerShell 5.1 has no ArgumentList; extensionless launchers
# are unusual there, but retain safe quote-delimited compatibility.
$start.Arguments = "verify --verbose --print-certs --print-certs-pem `"$ApkPath`""
}
}
$process = New-Object System.Diagnostics.Process
$process.StartInfo = $start
Expand Down
21 changes: 16 additions & 5 deletions tests/update-minimum-device.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -110,10 +110,7 @@ Test-Case "returning target switches to its correlated ADB port" {

Test-Case "apksigner output parser requires verified signer digest" {
$digest = "168F42ED412DA80ADAF27BED0984DBEE191168E9DF04F08AFA240A3F9DE45972"
Assert-Equal $digest (Parse-ApkSignerOutput "Signer #1 certificate SHA-256 digest: $digest") "apksigner digest"
$escape = [char]27
$linuxWrapped = "NativeCommandError: ${escape}[36mSigner #1 certificate SHA-256 digest: $($digest.ToLowerInvariant())${escape}[0m"
Assert-Equal $digest (Parse-ApkSignerOutput $linuxWrapped) "PowerShell Linux wrapped digest"
Assert-ThrowsCode { Parse-ApkSignerOutput "Number of signers: 1`nSigner #1 certificate SHA-256 digest: $digest" } "APK_SIGNATURE_INVALID" "text digest without certificate refused"
Assert-ThrowsCode { Parse-ApkSignerOutput "DOES NOT VERIFY" } "APK_SIGNATURE_INVALID" "missing signer digest"
}

Expand Down Expand Up @@ -249,9 +246,23 @@ if (Test-Path -LiteralPath $realApkPath -PathType Leaf) {
Assert-Equal ([long]3070301) $identity.VersionCode "real APK version code"
Assert-True ($identity.VersionName -match '-debug$') "real APK debug version"
}
$signers = @(Get-ApkSignerDigests -ApkPath $realApkPath)
$probe = Invoke-ApkSignerProcess -ApkSigner (Resolve-ApkSigner) -ApkPath $realApkPath
Assert-Equal 0 $probe.ExitCode "real apksigner exit"
$probeText = (($probe.Stdout, $probe.Stderr) -join "`n")
$signers = @(Parse-ApkSignerOutput $probeText)
Assert-True ($signers.Count -ge 1) "real APK signer count"
Assert-True (@($signers | Where-Object { $_ -notmatch '^[0-9A-F]{64}$' }).Count -eq 0) "real APK signer format"
$pem = [regex]::Match($probeText, '(?s)-----BEGIN CERTIFICATE-----\s*.*?\s*-----END CERTIFICATE-----')
Assert-True $pem.Success "real signer PEM present"
Assert-ThrowsCode { Parse-ApkSignerOutput ($probeText + "`n" + $pem.Value) } "APK_SIGNATURE_INVALID" "duplicate PEM refused"
$invalidPemText = $probeText.Remove($pem.Index, $pem.Length).Insert($pem.Index,
"-----BEGIN CERTIFICATE-----`nNOT-BASE64`n-----END CERTIFICATE-----")
Assert-ThrowsCode { Parse-ApkSignerOutput $invalidPemText } "APK_SIGNATURE_INVALID" "invalid PEM refused"
$wrongCount = [regex]::Replace($probeText, '(?im)^Number of signers:\s*[0-9]+\s*$', 'Number of signers: 99', 1)
Assert-ThrowsCode { Parse-ApkSignerOutput $wrongCount } "APK_SIGNATURE_INVALID" "signer count mismatch refused"
Assert-ThrowsCode { Parse-ApkSignerOutput ($probeText + "`nNumber of signers: $($signers.Count)") } "APK_SIGNATURE_INVALID" "duplicate signer count refused"
$fakeDigest = if ($signers[0] -ceq ('A' * 64)) { 'B' * 64 } else { 'A' * 64 }
Assert-ThrowsCode { Parse-ApkSignerOutput ($probeText + "`nSigner #99 certificate SHA-256 digest: $fakeDigest") } "APK_SIGNATURE_INVALID" "fake textual digest refused"
}
}

Expand Down
Loading