Align Windows release bundle mode validation
This commit is contained in:
1 parent
9d553744a1
commit
8748760594
4 files changed
+126
-4
No files matched your search
@@ -80,6 +80,15 @@ manifest и Git blob IDs в памяти, затем копирует тольк
|
||||
Дополнительно installer проверяет цепочку Git objects `commit → tree → файлы`.
|
||||
На elevated этапе Git не запускается; `.git/config`, hooks и состояние checkout
|
||||
не используются. Не изменяйте ZIP между подготовкой и установкой.
|
||||
Упаковщик ставит всем ZIP entries тип regular file и mode `0100600`; installer
|
||||
проверяет тот же mode (`0x8180`) до чтения содержимого. Для проверки стыка
|
||||
упаковщика и фактической функции mode-валидации на Windows используйте
|
||||
`powershell.exe -NoProfile -File infra/deploy/test_windows_release_bundle_mode.ps1 -PythonPath C:\Python313\python.exe`;
|
||||
`git.exe` должен уже разрешаться обычным `PATH`. Harness создаёт синтетический
|
||||
Git репозиторий и временный ZIP, извлекает только эту функцию из PowerShell AST и
|
||||
проверяет принятие штатного пакета и отказ после изменения mode. Он не запускает
|
||||
installer, не требует elevation, не меняет ACL и не регистрирует задачи; политику
|
||||
выполнения PowerShell менять для него не следует.
|
||||
|
||||
Проверенный bootstrap installer проверяет подпись PSF у
|
||||
`C:\Python313\python.exe` и защищённые ACL, затем размещает файлы клиента в
|
||||
|
||||
@@ -173,6 +173,10 @@ function Read-BoundedZipEntry($Entry, [long]$Maximum) {
|
||||
$buffer.Dispose()
|
||||
}
|
||||
}
|
||||
function Assert-ReceiverBundleEntryMode($Entry) {
|
||||
$entryMode = (([long]$Entry.ExternalAttributes -shr 16) -band 0xFFFF)
|
||||
if ($entryMode -ne 0x8180) { throw "Receiver bundle entry has an unsafe file mode: $($Entry.FullName)" }
|
||||
}
|
||||
function Get-LowerHex([byte[]]$Bytes, [bool]$UseSha1 = $false) {
|
||||
$algorithm = if ($UseSha1) { [Security.Cryptography.SHA1]::Create() } else { [Security.Cryptography.SHA256]::Create() }
|
||||
try { return [BitConverter]::ToString($algorithm.ComputeHash($Bytes)).Replace('-', '').ToLowerInvariant() }
|
||||
@@ -241,8 +245,7 @@ try {
|
||||
if ($entryNames -cnotcontains $expectedEntry) { throw "Receiver bundle is missing an exact required entry: $expectedEntry" }
|
||||
}
|
||||
foreach ($zipEntry in $zip.Entries) {
|
||||
$entryMode = (([long]$zipEntry.ExternalAttributes -shr 16) -band 0xFFFF)
|
||||
if ($entryMode -ne 0x81A0) { throw "Receiver bundle entry has an unsafe file mode: $($zipEntry.FullName)" }
|
||||
Assert-ReceiverBundleEntryMode -Entry $zipEntry
|
||||
}
|
||||
$manifestBytes = Read-BoundedZipEntry -Entry $zip.GetEntry('manifest.json') -Maximum 16384
|
||||
$utf8Strict = [Text.UTF8Encoding]::new($false, $true)
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$PythonPath
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$repository = (Resolve-Path (Join-Path $PSScriptRoot '..\..')).Path
|
||||
$installerPath = Join-Path $PSScriptRoot 'install_windows_release_client.ps1'
|
||||
$fixtureScript = Join-Path $PSScriptRoot 'test_windows_release_launcher.py'
|
||||
$temporaryRoot = Join-Path ([IO.Path]::GetTempPath()) ('akyldash-bundle-mode-' + [guid]::NewGuid().ToString('N'))
|
||||
[void][IO.Directory]::CreateDirectory($temporaryRoot)
|
||||
$bundlePath = Join-Path $temporaryRoot 'fixture.zip'
|
||||
|
||||
try {
|
||||
$fixtureOutput = & $PythonPath -E -s -S $fixtureScript --write-bundle-fixture $bundlePath
|
||||
if ($LASTEXITCODE -ne 0) { throw 'The temporary packager fixture failed.' }
|
||||
$fixture = ($fixtureOutput -join "`n") | ConvertFrom-Json
|
||||
|
||||
$tokens = $null
|
||||
$parseErrors = $null
|
||||
$installerAst = [System.Management.Automation.Language.Parser]::ParseFile(
|
||||
$installerPath, [ref]$tokens, [ref]$parseErrors)
|
||||
if ($parseErrors.Count -gt 0) { throw 'Installer PowerShell syntax is invalid.' }
|
||||
$validatorAst = $installerAst.Find({
|
||||
param($node)
|
||||
$node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and
|
||||
$node.Name -ceq 'Assert-ReceiverBundleEntryMode'
|
||||
}, $true)
|
||||
if ($null -eq $validatorAst) { throw 'Installer bundle-mode validator was not found.' }
|
||||
. ([scriptblock]::Create($validatorAst.Extent.Text))
|
||||
|
||||
Add-Type -AssemblyName System.IO.Compression
|
||||
$stream = [IO.File]::OpenRead($bundlePath)
|
||||
try {
|
||||
$archive = [System.IO.Compression.ZipArchive]::new(
|
||||
$stream, [System.IO.Compression.ZipArchiveMode]::Read, $true)
|
||||
try {
|
||||
foreach ($entry in $archive.Entries) {
|
||||
Assert-ReceiverBundleEntryMode -Entry $entry
|
||||
}
|
||||
} finally { $archive.Dispose() }
|
||||
} finally { $stream.Dispose() }
|
||||
|
||||
$alteredPath = Join-Path $temporaryRoot 'altered-mode.zip'
|
||||
$sourceStream = [IO.File]::OpenRead($bundlePath)
|
||||
$targetStream = [IO.File]::Create($alteredPath)
|
||||
try {
|
||||
$sourceArchive = [System.IO.Compression.ZipArchive]::new(
|
||||
$sourceStream, [System.IO.Compression.ZipArchiveMode]::Read, $true)
|
||||
$targetArchive = [System.IO.Compression.ZipArchive]::new(
|
||||
$targetStream, [System.IO.Compression.ZipArchiveMode]::Create, $true)
|
||||
try {
|
||||
$changed = $false
|
||||
foreach ($entry in $sourceArchive.Entries) {
|
||||
$copy = $targetArchive.CreateEntry($entry.FullName, [System.IO.Compression.CompressionLevel]::NoCompression)
|
||||
$copy.ExternalAttributes = $entry.ExternalAttributes
|
||||
if (-not $changed) {
|
||||
$copy.ExternalAttributes = [int]([long]0x81A0 -shl 16)
|
||||
$changed = $true
|
||||
}
|
||||
$input = $entry.Open()
|
||||
$output = $copy.Open()
|
||||
try { $input.CopyTo($output) }
|
||||
finally { $output.Dispose(); $input.Dispose() }
|
||||
}
|
||||
if (-not $changed) { throw 'Packager fixture had no entries to mutate.' }
|
||||
} finally { $targetArchive.Dispose(); $sourceArchive.Dispose() }
|
||||
} finally { $targetStream.Dispose(); $sourceStream.Dispose() }
|
||||
|
||||
$alteredStream = [IO.File]::OpenRead($alteredPath)
|
||||
try {
|
||||
$alteredArchive = [System.IO.Compression.ZipArchive]::new(
|
||||
$alteredStream, [System.IO.Compression.ZipArchiveMode]::Read, $true)
|
||||
try {
|
||||
$rejected = $false
|
||||
try { Assert-ReceiverBundleEntryMode -Entry $alteredArchive.Entries[0] }
|
||||
catch { $rejected = $true }
|
||||
if (-not $rejected) { throw 'Installer validator accepted an altered ZIP entry mode.' }
|
||||
} finally { $alteredArchive.Dispose() }
|
||||
} finally { $alteredStream.Dispose() }
|
||||
|
||||
Write-Output ("PASS: installer validator accepted the actual packager bundle for commit {0} (SHA-256 {1}) and rejected altered mode." -f $fixture.commit, $fixture.sha256)
|
||||
} finally {
|
||||
Remove-Item -LiteralPath $temporaryRoot -Recurse -Force
|
||||
}
|
||||
@@ -92,7 +92,8 @@ class WindowsReleaseBootstrapTests(unittest.TestCase):
|
||||
script = (ROOT / "infra/deploy/install_windows_release_client.ps1").read_text(encoding="utf-8")
|
||||
self.assertIn("[IO.FileShare]::Read", script)
|
||||
self.assertIn("[System.IO.Compression.ZipArchive]::new($bundleStream", script)
|
||||
self.assertIn("$entryMode -ne 0x81A0", script)
|
||||
self.assertIn("Assert-ReceiverBundleEntryMode -Entry $zipEntry", script)
|
||||
self.assertIn("$entryMode -ne 0x8180", script)
|
||||
self.assertIn("$bundleHash -ne $ExpectedBundleSha256", script)
|
||||
self.assertIn("manifest.commit -cne $ExpectedCommit", script)
|
||||
self.assertIn("Get-GitObjectHex -Kind 'blob' -Bytes $data", script)
|
||||
@@ -185,6 +186,8 @@ class WindowsReleaseBootstrapTests(unittest.TestCase):
|
||||
self.assertEqual(set(member.filename for member in bundle.infolist()),
|
||||
{"manifest.json", *(f"files/{name}" for name in release_bundle.FILES),
|
||||
*release_bundle.TREE_OBJECTS.values(), "objects/commit.bin"})
|
||||
self.assertEqual({(member.external_attr >> 16) & 0xFFFF for member in bundle.infolist()},
|
||||
{0o100600})
|
||||
self.assertEqual(release_bundle._git_object_sha("commit", bundle.read("objects/commit.bin")), commit)
|
||||
for tree in manifest["trees"]:
|
||||
object_name = release_bundle.TREE_OBJECTS[tree["path"]]
|
||||
@@ -358,4 +361,26 @@ class WindowsReleaseBootstrapTests(unittest.TestCase):
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
if len(sys.argv) == 3 and sys.argv[1] == "--write-bundle-fixture":
|
||||
output_path = Path(sys.argv[2])
|
||||
repository = output_path.parent / "fixture-repository"
|
||||
repository.mkdir()
|
||||
subprocess.run(["git", "-C", str(repository), "init", "-b", "main"], check=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
subprocess.run(["git", "-C", str(repository), "config", "user.name", "Akyldash Test"], check=True)
|
||||
subprocess.run(["git", "-C", str(repository), "config", "user.email", "test@example.invalid"], check=True)
|
||||
for name, relative in release_bundle.FILES.items():
|
||||
path = repository / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
path.write_bytes(f"fixture:{name}\n".encode("ascii"))
|
||||
subprocess.run(["git", "-C", str(repository), "add", "--", *release_bundle.FILES.values()], check=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
subprocess.run(["git", "-C", str(repository), "commit", "-m", "bundle fixture"], check=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
commit = subprocess.run(["git", "-C", str(repository), "rev-parse", "HEAD"], check=True,
|
||||
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True).stdout.strip()
|
||||
payload = release_bundle.create_bundle(repository, commit)
|
||||
output_path.write_bytes(payload)
|
||||
print(json.dumps({"commit": commit, "sha256": hashlib.sha256(payload).hexdigest()}))
|
||||
else:
|
||||
unittest.main()
|
||||
Reference in new issue
Block a user