projection: begin the exact-path public epoch
The public tree and its history contain only the listed paths. Earlier projection history remains preserved internally. Source-Sha: 913fde029b935671833254797f0f20f1eb9fabba Policy-Sha: 913fde029b935671833254797f0f20f1eb9fabba Tree-Digest: 180ae530c1058a2a5c89837bdce2d323ae83e669e38590ca72e75b8d92b7262f
This commit is contained in:
commit
67e26be2d9
838 changed files with 157302 additions and 0 deletions
152
installer/windows/Product.wxs
Normal file
152
installer/windows/Product.wxs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
RedFlagSetup.msi — native Windows installer for the RedFlag server (INSTALL-002).
|
||||
|
||||
Built with `wixl` (msitools), NOT the official WiX Toolset .NET CLI. The
|
||||
official `wix build` genuinely does not work when the compiler itself runs
|
||||
on Linux — reproduced empirically: even a single-character Directory/@Name
|
||||
fails WIX0389 "not a relative path" on every WiX version tried (4.0.5,
|
||||
4.0.6, 5.0.0, 5.0.2, 6.0.0, 6.0.1). The tool's own output says why:
|
||||
"warning WIX0000: The WiX Toolset only supports Windows... All behavior
|
||||
after this point is undefined." That's not a disclaimer to route around
|
||||
with a workaround — the validator itself is broken cross-platform. wixl is
|
||||
a from-scratch, Linux-native reimplementation of the (older, WiX v3-era)
|
||||
MSI-building grammar, built for exactly this "generate a real Windows MSI
|
||||
from Linux CI" case, and produces a structurally valid MSI (verified with
|
||||
msiinfo tables / msiinfo suminfo) with no cross-platform path bugs. This
|
||||
file therefore uses the WiX v3 schema/dialect (Product+Package as separate
|
||||
elements, explicit Directory nesting, no StandardDirectory shorthand) —
|
||||
wixl's grammar, not v4's.
|
||||
|
||||
Scope (v1): install the already-built, already-signed server binary as a
|
||||
Windows service, with a forward-only upgrade guard. It does NOT bundle
|
||||
PostgreSQL — docker-compose doesn't either (postgres:16-alpine is a
|
||||
separate container); a native install is expected to point at a reachable
|
||||
Postgres the same way the docker path does via config/.env. The installer's
|
||||
job ends where the docker quick-start's does today: "service is running,
|
||||
go to http://localhost:<port>/setup and finish configuration in the
|
||||
browser" (README.md's existing docker flow, same UX, different transport).
|
||||
|
||||
Config: the Go binary only ever reads OS environment variables
|
||||
(server/internal/config/config.go). Docker gets those from compose's
|
||||
env_file:; a native install has no compose layer, so this installer drops
|
||||
a template env file under ProgramData and the service reads it via
|
||||
config.loadNativeConfigFile() — same manual "generate keys, paste into the
|
||||
env file, restart" flow the docker README already documents, not a new UX.
|
||||
|
||||
NOT YET BUILT into this file: install-time manifest signature verification
|
||||
(fetch the release manifest, verify Ed25519 signature, check the server
|
||||
binary's SHA256 before executing) — the cold-start TOFU problem flagged in
|
||||
docs/tasks/INSTALL-002-native-os-installers.md. This installer currently
|
||||
trusts whatever binary CI hands it, same trust level as unzipping a release
|
||||
tarball by hand. That verification layer is the next slice, not this one.
|
||||
|
||||
VERIFIED: built successfully with wixl 0.106 against a real cross-compiled
|
||||
redflag-server-windows-amd64.exe on 2026-07-01 — msiinfo confirms a valid
|
||||
MSI (Directory/Component/Feature/ServiceInstall/ServiceControl tables all
|
||||
populated correctly). NOT yet verified: actually running the installer on
|
||||
a real Windows host (install, service starts, uninstall, upgrade-over
|
||||
itself). That's the next check, ideally on real hardware or a VM.
|
||||
-->
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||
|
||||
<!--
|
||||
UpgradeCode must never change for the life of this product — it's the
|
||||
identity that makes the MajorUpgrade forward-only guard below mean
|
||||
anything across releases. Generated once, fixed forever after.
|
||||
-->
|
||||
<Product Id="*"
|
||||
Name="RedFlag Server"
|
||||
Language="1033"
|
||||
Version="$(var.RedFlagVersion)"
|
||||
Manufacturer="Fimeg"
|
||||
UpgradeCode="8f3c9e6a-4b1d-4a7f-9c2e-1d6a8b2f5e91">
|
||||
|
||||
<Package InstallerVersion="500" Compressed="yes" InstallScope="perMachine"
|
||||
Description="RedFlag self-hosted update management — server install" />
|
||||
|
||||
<!--
|
||||
Forward-only, no downgrade — ETHOS doctrine elsewhere in this codebase
|
||||
(refresh-token rotation, capability tokens) applied to the installer
|
||||
itself. Windows Installer only compares the first three version
|
||||
fields for upgrade detection — pass $(var.RedFlagVersion) as the
|
||||
3-part form from CI, same 3-vs-4-part reconciliation bump-version.sh
|
||||
already does for desktop/Cargo.toml vs tauri.conf.json.
|
||||
-->
|
||||
<MajorUpgrade DowngradeErrorMessage="A newer version of RedFlag Server is already installed. Forward-only upgrades only." />
|
||||
|
||||
<MediaTemplate EmbedCab="yes" />
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<Directory Id="ProgramFiles64Folder">
|
||||
<Directory Id="INSTALLFOLDER" Name="RedFlag">
|
||||
<Component Id="ServerBinary" Guid="7a2d5e9c-3f81-4c6b-9e0a-2b7d4f8c1a63">
|
||||
<File Id="RedFlagServerExe"
|
||||
Source="dist/redflag-server-windows-amd64.exe"
|
||||
Name="redflag-server.exe"
|
||||
KeyPath="yes" />
|
||||
|
||||
<!--
|
||||
LocalSystem for v1 — matches "get it working" over
|
||||
least-privilege for now. The Linux side went through the same
|
||||
arc (helper started as a plain child before the systemd-run
|
||||
privilege split). Revisit alongside SEC-030 once a Windows
|
||||
privileged-executor model exists to hand mutation off to
|
||||
instead of running the whole server as SYSTEM.
|
||||
-->
|
||||
<ServiceInstall Id="RedFlagServerService"
|
||||
Name="RedFlagServer"
|
||||
DisplayName="RedFlag Server"
|
||||
Description="RedFlag self-hosted update management server"
|
||||
Type="ownProcess"
|
||||
Start="auto"
|
||||
ErrorControl="normal"
|
||||
Account="LocalSystem" />
|
||||
|
||||
<ServiceControl Id="RedFlagServerServiceControl"
|
||||
Name="RedFlagServer"
|
||||
Start="install"
|
||||
Stop="both"
|
||||
Remove="uninstall"
|
||||
Wait="yes" />
|
||||
</Component>
|
||||
</Directory>
|
||||
</Directory>
|
||||
|
||||
<!--
|
||||
ProgramData, not Program Files: this holds the mutable,
|
||||
operator-edited config (DB connection, admin creds, signing key
|
||||
once generated) — it must survive upgrades/reinstalls, which
|
||||
Program Files contents don't reliably. Only the .example template
|
||||
ships here; the real redflag.env is created by the operator (or a
|
||||
future first-run helper), never overwritten by this installer once
|
||||
it exists.
|
||||
-->
|
||||
<Directory Id="CommonAppDataFolder">
|
||||
<Directory Id="REDFLAGDATA" Name="RedFlag">
|
||||
<Component Id="ConfigTemplate" Guid="1c6e8a2d-9b47-4f3e-a1c8-6d3e9b4a2f70">
|
||||
<File Id="RedFlagEnvExample"
|
||||
Source="config/redflag.env.example"
|
||||
Name="redflag.env.example"
|
||||
KeyPath="yes" />
|
||||
</Component>
|
||||
</Directory>
|
||||
</Directory>
|
||||
</Directory>
|
||||
|
||||
<Feature Id="ServerFeature" Level="1">
|
||||
<ComponentRef Id="ServerBinary" />
|
||||
<ComponentRef Id="ConfigTemplate" />
|
||||
</Feature>
|
||||
|
||||
<!--
|
||||
Desktop tray feature slots in here later (Casey, 2026-06-30: "checkbox
|
||||
for adding the desktop alongside it" plus future screen-capture work
|
||||
from the session-broker RAF). Deliberately not stubbed as a disabled
|
||||
checkbox — a checkbox that does nothing when checked is worse than no
|
||||
checkbox. Wire it once the desktop/Tauri artifact has a real merge
|
||||
path into this installer, per Casey's "keep it separate for now."
|
||||
-->
|
||||
|
||||
</Product>
|
||||
</Wix>
|
||||
64
installer/windows/build-desktop.ps1
Normal file
64
installer/windows/build-desktop.ps1
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
# Native Windows CI entry point. Run from an x64 MSVC developer environment
|
||||
# with Rust, Go and Qt 6 on PATH. Produces the complete Desktop payload;
|
||||
# setup must package the entire directory, not just redflag-desktop.exe.
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)][string]$OutputDirectory,
|
||||
[Parameter(Mandatory = $true)][ValidatePattern('^[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?$')][string]$Version
|
||||
)
|
||||
$ErrorActionPreference = 'Stop'
|
||||
Set-StrictMode -Version Latest
|
||||
if ($env:OS -ne 'Windows_NT') { throw 'This build requires a native Windows runner.' }
|
||||
foreach ($tool in @('cargo', 'rustc', 'go', 'cl', 'windeployqt')) {
|
||||
if (-not (Get-Command $tool -ErrorAction SilentlyContinue)) { throw "Missing build tool: $tool" }
|
||||
}
|
||||
$rustInfo = & rustc -vV
|
||||
if ($LASTEXITCODE -ne 0 -or -not ($rustInfo -match 'host: x86_64-pc-windows-msvc')) {
|
||||
throw 'Select the x86_64-pc-windows-msvc Rust toolchain.'
|
||||
}
|
||||
$root = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path
|
||||
$expectedVersion = $Version
|
||||
$output = [IO.Path]::GetFullPath($OutputDirectory)
|
||||
if (Test-Path $output) { throw 'Output directory must be new; refusing to mix payload versions.' }
|
||||
New-Item -ItemType Directory -Path $output | Out-Null
|
||||
Push-Location (Join-Path $root 'desktop')
|
||||
$previousVersion = $env:REDFLAG_RELEASE_VERSION
|
||||
try {
|
||||
$env:REDFLAG_RELEASE_VERSION = $Version
|
||||
& cargo build --release --locked
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Desktop build failed.' }
|
||||
Copy-Item 'target/release/redflag-desktop.exe' $output
|
||||
} finally { $env:REDFLAG_RELEASE_VERSION = $previousVersion; Pop-Location }
|
||||
Push-Location (Join-Path $root 'agent')
|
||||
try {
|
||||
& go build -trimpath -ldflags "-X github.com/Fimeg/RedFlag/agent/internal/version.Version=$Version" -o (Join-Path $output 'redflag-agent.exe') ./cmd/agent/
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Agent build failed.' }
|
||||
} finally { Pop-Location }
|
||||
$desktop = Join-Path $output 'redflag-desktop.exe'
|
||||
& windeployqt --release --qmldir (Join-Path $root 'desktop/qml') --dir $output $desktop
|
||||
if ($LASTEXITCODE -ne 0) { throw 'Qt runtime deployment failed.' }
|
||||
foreach ($relative in @('Qt6Core.dll', 'Qt6Gui.dll', 'Qt6Qml.dll', 'Qt6Quick.dll', 'platforms/qwindows.dll')) {
|
||||
if (-not (Test-Path (Join-Path $output $relative))) { throw "Qt payload is missing $relative" }
|
||||
}
|
||||
Copy-Item (Join-Path $root 'LICENSE') (Join-Path $output 'LICENSE.txt')
|
||||
Copy-Item (Join-Path $root 'THIRD_PARTY_LICENSES.md') $output
|
||||
# Remove the build toolchain from DLL lookup for the version check. A clean
|
||||
# Windows VM must additionally launch QML and exercise the Agent pipe.
|
||||
$buildPath = $env:PATH
|
||||
try {
|
||||
$env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
|
||||
$reportedVersion = & $desktop --version
|
||||
if ($LASTEXITCODE -ne 0 -or $reportedVersion -ne "RedFlag v$expectedVersion") {
|
||||
throw 'Staged Desktop failed its version check without the Qt build PATH.'
|
||||
}
|
||||
} finally { $env:PATH = $buildPath }
|
||||
$files = @(Get-ChildItem $output -Recurse -File | Sort-Object FullName | ForEach-Object {
|
||||
[ordered]@{
|
||||
path = $_.FullName.Substring($output.Length + 1).Replace('\', '/')
|
||||
sha256 = (Get-FileHash $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
size = $_.Length
|
||||
}
|
||||
})
|
||||
[ordered]@{ desktop_version = $reportedVersion; files = $files } |
|
||||
ConvertTo-Json -Depth 4 | Set-Content (Join-Path $output 'payload.json') -Encoding UTF8
|
||||
Write-Output "Desktop payload staged at $output; this is not yet an installer or an installation test."
|
||||
66
installer/windows/build-msi.sh
Executable file
66
installer/windows/build-msi.sh
Executable file
|
|
@ -0,0 +1,66 @@
|
|||
#!/usr/bin/env bash
|
||||
# Canonical CI entry point for the RedFlag Server MSI custody build.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
[ "$#" -eq 6 ] || {
|
||||
echo "usage: $0 <server-exe> <redflag-version> <output-msi> <proof-json> <source-commit> <source-ref>" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
SERVER_EXE="$1"
|
||||
REDFLAG_VERSION="$2"
|
||||
OUTPUT_MSI="$3"
|
||||
PROOF_JSON="$4"
|
||||
SOURCE_COMMIT="$5"
|
||||
SOURCE_REF="$6"
|
||||
ROOT=$(cd "$(dirname "$0")/../.." && pwd)
|
||||
MSI_VERSION=$(printf '%s\n' "$REDFLAG_VERSION" | cut -d. -f1-3)
|
||||
|
||||
for tool in jq msiinfo msiextract wixl; do
|
||||
command -v "$tool" >/dev/null || { echo "$tool not found" >&2; exit 2; }
|
||||
done
|
||||
[ -f "$SERVER_EXE" ] || { echo "no such Server executable: $SERVER_EXE" >&2; exit 2; }
|
||||
|
||||
SERVER_EXE=$(realpath "$SERVER_EXE")
|
||||
OUTPUT_MSI=$(realpath -m "$OUTPUT_MSI")
|
||||
PROOF_JSON=$(realpath -m "$PROOF_JSON")
|
||||
mkdir -p "$ROOT/installer/windows/dist" "$(dirname "$OUTPUT_MSI")" "$(dirname "$PROOF_JSON")"
|
||||
cp "$SERVER_EXE" "$ROOT/installer/windows/dist/redflag-server-windows-amd64.exe"
|
||||
|
||||
(
|
||||
cd "$ROOT/installer/windows"
|
||||
wixl Product.wxs -D RedFlagVersion="$MSI_VERSION" -o "$OUTPUT_MSI"
|
||||
)
|
||||
|
||||
CAB_FILE=$(find "$(dirname "$OUTPUT_MSI")" "$ROOT/installer/windows" \
|
||||
-maxdepth 2 -type f -name '*.cab' -print -quit)
|
||||
if [ -n "$CAB_FILE" ]; then
|
||||
echo "[FAIL] external cabinet produced"
|
||||
find "$(dirname "$OUTPUT_MSI")" "$ROOT/installer/windows" \
|
||||
-maxdepth 2 -type f -name '*.cab' -print
|
||||
exit 1
|
||||
fi
|
||||
|
||||
"$ROOT/installer/windows/verify-msi.sh" \
|
||||
"$OUTPUT_MSI" \
|
||||
"$ROOT/installer/windows/dist/redflag-server-windows-amd64.exe" \
|
||||
"$ROOT/installer/windows/config/redflag.env.example" \
|
||||
--redflag-version "$REDFLAG_VERSION" \
|
||||
--msi-version "$MSI_VERSION" \
|
||||
--source-commit "$SOURCE_COMMIT" \
|
||||
--source-ref "$SOURCE_REF" \
|
||||
--proof-record "$PROOF_JSON"
|
||||
|
||||
FIXTURE="$ROOT/installer/windows/testdata/run3181-hollow.msi"
|
||||
if "$ROOT/installer/windows/verify-msi.sh" "$FIXTURE" \
|
||||
"$ROOT/installer/windows/dist/redflag-server-windows-amd64.exe" \
|
||||
"$ROOT/installer/windows/config/redflag.env.example" > /tmp/hollow-msi.log 2>&1; then
|
||||
echo "[FAIL] hollow MSI regression fixture passed custody verification"
|
||||
cat /tmp/hollow-msi.log
|
||||
exit 1
|
||||
fi
|
||||
grep -E '^\[FAIL\]|^RESULT:' /tmp/hollow-msi.log
|
||||
echo "Hollow MSI regression fixture rejected, as required."
|
||||
echo "Installer custody proof record:"
|
||||
jq . "$PROOF_JSON"
|
||||
43
installer/windows/config/redflag.env.example
Normal file
43
installer/windows/config/redflag.env.example
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# ============================================================
|
||||
# RedFlag Server — native Windows configuration
|
||||
# Copy this file to: redflag.env (same folder), edit it, then
|
||||
# restart the "RedFlag Server" service (services.msc or:
|
||||
# net stop RedFlagServer && net start RedFlagServer
|
||||
# ============================================================
|
||||
# This is the native-install equivalent of config/.env.example
|
||||
# in the main repo — same variables, same semantics. Docker gets
|
||||
# these from docker-compose's env_file:; this binary reads this
|
||||
# file directly (server/internal/config/config.go loadNativeConfigFile).
|
||||
|
||||
# --- PostgreSQL ---
|
||||
# Point this at a Postgres instance you provide — same as the docker
|
||||
# stack's postgres:16-alpine container, just not bundled by this installer.
|
||||
REDFLAG_DB_HOST=localhost
|
||||
REDFLAG_DB_PORT=5432
|
||||
REDFLAG_DB_NAME=redflag
|
||||
REDFLAG_DB_USER=redflag
|
||||
REDFLAG_DB_PASSWORD=CHANGE_ME_pick_a_strong_db_password
|
||||
|
||||
# --- Server ---
|
||||
REDFLAG_SERVER_HOST=0.0.0.0
|
||||
REDFLAG_SERVER_PORT=8080
|
||||
|
||||
# --- Admin Account ---
|
||||
# After first-run setup these are persisted in the database;
|
||||
# these values re-apply on service restart.
|
||||
REDFLAG_ADMIN_USER=admin
|
||||
REDFLAG_ADMIN_EMAIL=admin@example.com
|
||||
REDFLAG_ADMIN_PASSWORD=CHANGE_ME_pick_a_strong_admin_password
|
||||
REDFLAG_JWT_SECRET=CHANGE_ME_random_string_at_least_32_characters_long
|
||||
|
||||
# --- Agent Registration ---
|
||||
REDFLAG_TOKEN_EXPIRY=24h
|
||||
REDFLAG_MAX_TOKENS=100
|
||||
REDFLAG_MAX_SEATS=50
|
||||
|
||||
# --- Ed25519 Signing Key ---
|
||||
# Leave blank on first run. Go to http://localhost:8080/setup and click
|
||||
# "Generate Keys". Copy the private key here, then restart the service.
|
||||
# Without this, agents cannot receive signed commands or upgrade themselves.
|
||||
# BACKUP THE PRIVATE KEY. Losing it means re-enrolling every agent.
|
||||
REDFLAG_SIGNING_PRIVATE_KEY=
|
||||
BIN
installer/windows/testdata/run3181-hollow.msi
vendored
Normal file
BIN
installer/windows/testdata/run3181-hollow.msi
vendored
Normal file
Binary file not shown.
179
installer/windows/verify-msi.sh
Executable file
179
installer/windows/verify-msi.sh
Executable file
|
|
@ -0,0 +1,179 @@
|
|||
#!/usr/bin/env bash
|
||||
# Prove that a RedFlag Server MSI contains the exact bytes staged for it.
|
||||
# MSI compressed size is diagnostic only; it is never an acceptance gate.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
MSI="${1:-}"
|
||||
STAGED_EXE="${2:-}"
|
||||
STAGED_ENV="${3:-}"
|
||||
shift "$(( $# >= 3 ? 3 : $# ))"
|
||||
|
||||
REDFLAG_VERSION=""
|
||||
MSI_VERSION=""
|
||||
PROOF_RECORD=""
|
||||
SOURCE_COMMIT="${GITHUB_SHA:-}"
|
||||
SOURCE_REF="${GITHUB_REF:-}"
|
||||
|
||||
while [ "$#" -gt 0 ]; do
|
||||
[ "$#" -ge 2 ] || { echo "missing value for $1" >&2; exit 2; }
|
||||
case "$1" in
|
||||
--redflag-version) REDFLAG_VERSION="${2:-}"; shift 2 ;;
|
||||
--msi-version) MSI_VERSION="${2:-}"; shift 2 ;;
|
||||
--proof-record) PROOF_RECORD="${2:-}"; shift 2 ;;
|
||||
--source-commit) SOURCE_COMMIT="${2:-}"; shift 2 ;;
|
||||
--source-ref) SOURCE_REF="${2:-}"; shift 2 ;;
|
||||
*) echo "unknown argument: $1" >&2; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[ -n "$MSI" ] && [ -n "$STAGED_EXE" ] && [ -n "$STAGED_ENV" ] || {
|
||||
echo "usage: $0 <msi> <staged-server-exe> <staged-env-example> [options]" >&2
|
||||
exit 2
|
||||
}
|
||||
for f in "$MSI" "$STAGED_EXE" "$STAGED_ENV"; do
|
||||
[ -f "$f" ] || { echo "no such file: $f" >&2; exit 2; }
|
||||
done
|
||||
for tool in msiinfo msiextract sha256sum; do
|
||||
command -v "$tool" >/dev/null || { echo "$tool not found" >&2; exit 2; }
|
||||
done
|
||||
if [ -n "$PROOF_RECORD" ]; then
|
||||
[ -n "$REDFLAG_VERSION" ] && [ -n "$MSI_VERSION" ] || {
|
||||
echo "--proof-record requires --redflag-version and --msi-version" >&2; exit 2; }
|
||||
command -v jq >/dev/null || { echo "jq not found" >&2; exit 2; }
|
||||
fi
|
||||
|
||||
FAIL=0
|
||||
EMBEDDED=false
|
||||
ok() { echo "[ok] $*"; }
|
||||
bad() { echo "[FAIL] $*"; FAIL=1; }
|
||||
|
||||
MSI_SIZE=$(stat -c%s "$MSI")
|
||||
MSI_SHA=$(sha256sum "$MSI" | cut -d' ' -f1)
|
||||
STAGED_SERVER_SIZE=$(stat -c%s "$STAGED_EXE")
|
||||
STAGED_SERVER_SHA=$(sha256sum "$STAGED_EXE" | cut -d' ' -f1)
|
||||
STAGED_CONFIG_SIZE=$(stat -c%s "$STAGED_ENV")
|
||||
STAGED_CONFIG_SHA=$(sha256sum "$STAGED_ENV" | cut -d' ' -f1)
|
||||
|
||||
echo "=== package under test ==="
|
||||
echo " path: $MSI"
|
||||
echo " size: $MSI_SIZE bytes <- diagnostic only, never a gate"
|
||||
echo " sha256: $MSI_SHA"
|
||||
|
||||
echo "=== tables ==="
|
||||
for t in Property Directory Component File Feature FeatureComponents \
|
||||
ServiceInstall ServiceControl Upgrade Media; do
|
||||
if msiinfo export "$MSI" "$t" >/dev/null 2>&1; then ok "table $t present"
|
||||
else bad "table $t missing"; fi
|
||||
done
|
||||
carries() {
|
||||
if msiinfo export "$MSI" "$1" 2>/dev/null | grep -F "$2" >/dev/null; then ok "$1 carries $2"
|
||||
else bad "$1 does not carry $2"; fi
|
||||
}
|
||||
carries ServiceInstall RedFlagServer
|
||||
carries ServiceControl RedFlagServer
|
||||
carries Component ServerBinary
|
||||
carries Component ConfigTemplate
|
||||
carries File redflag-server.exe
|
||||
carries File redflag.env.example
|
||||
|
||||
echo "=== version identity ==="
|
||||
PRODUCT_VERSION=$(msiinfo export "$MSI" Property 2>/dev/null |
|
||||
awk -F'\t' '$1 == "ProductVersion" {print $2; exit}' | tr -d '\r')
|
||||
if [ -n "$MSI_VERSION" ]; then
|
||||
[ "$PRODUCT_VERSION" = "$MSI_VERSION" ] && ok "MSI ProductVersion is $MSI_VERSION" || \
|
||||
bad "MSI ProductVersion '${PRODUCT_VERSION:-missing}' != expected $MSI_VERSION"
|
||||
fi
|
||||
if [ -n "$REDFLAG_VERSION" ]; then
|
||||
if ! command -v go >/dev/null; then
|
||||
bad "go is required to inspect the staged Server build version"
|
||||
else
|
||||
SERVER_VERSION=$(go version -m "$STAGED_EXE" 2>/dev/null |
|
||||
sed -n 's/.*versions\.AgentVersion=\([^[:space:]]*\).*/\1/p' | head -1)
|
||||
[ "$SERVER_VERSION" = "$REDFLAG_VERSION" ] && \
|
||||
ok "staged Server build version is $REDFLAG_VERSION" || \
|
||||
bad "staged Server build version '${SERVER_VERSION:-missing}' != release $REDFLAG_VERSION"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=== cabinet custody ==="
|
||||
msiinfo export "$MSI" Media 2>/dev/null | sed 's/^/ media| /'
|
||||
CAB=$(msiinfo export "$MSI" Media 2>/dev/null |
|
||||
awk -F'\t' 'NR > 3 && $4 != "" {print $4; exit}')
|
||||
if [ -z "$CAB" ]; then
|
||||
bad "Media table names no cabinet at all"
|
||||
elif [ "${CAB#\#}" = "$CAB" ]; then
|
||||
bad "Media names external cabinet '$CAB'"
|
||||
else
|
||||
STREAM="${CAB#\#}"
|
||||
if msiinfo streams "$MSI" 2>/dev/null | grep -Fx "$STREAM" >/dev/null; then
|
||||
EMBEDDED=true
|
||||
ok "Media names embedded cabinet '$CAB' and storage carries '$STREAM'"
|
||||
else
|
||||
bad "embedded cabinet stream '$STREAM' is absent"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "=== extraction (MSI copied alone to an empty directory) ==="
|
||||
ISO=$(mktemp -d)
|
||||
OUT=$(mktemp -d)
|
||||
trap 'rm -rf "$ISO" "$OUT"' EXIT
|
||||
cp "$MSI" "$ISO/package.msi"
|
||||
if (cd "$ISO" && msiextract -C "$OUT" package.msi >/dev/null 2>&1); then
|
||||
ok "msiextract succeeded without a sibling cabinet"
|
||||
else
|
||||
bad "msiextract failed without a sibling cabinet"
|
||||
fi
|
||||
find "$OUT" -type f -printf ' %s bytes %P\n' 2>/dev/null | sort || true
|
||||
|
||||
EXTRACTED_SERVER=""
|
||||
EXTRACTED_CONFIG=""
|
||||
compare() {
|
||||
local name="$1" staged="$2" destination="$3" got gs ss gh sh
|
||||
got=$(find "$OUT" -type f -name "$name" -print -quit 2>/dev/null)
|
||||
if [ -z "$got" ]; then bad "$name is not in the extracted payload"; return; fi
|
||||
printf -v "$destination" '%s' "$got"
|
||||
gs=$(stat -c%s "$got"); ss=$(stat -c%s "$staged")
|
||||
gh=$(sha256sum "$got" | cut -d' ' -f1); sh=$(sha256sum "$staged" | cut -d' ' -f1)
|
||||
[ "$gs" = "$ss" ] && ok "$name size matches staged ($gs bytes)" || bad "$name size $gs != staged $ss"
|
||||
[ "$gh" = "$sh" ] && ok "$name sha256 matches staged ($gh)" || bad "$name sha256 $gh != staged $sh"
|
||||
}
|
||||
echo "=== payload identity ==="
|
||||
compare redflag-server.exe "$STAGED_EXE" EXTRACTED_SERVER
|
||||
compare redflag.env.example "$STAGED_ENV" EXTRACTED_CONFIG
|
||||
|
||||
if [ "$FAIL" -ne 0 ]; then
|
||||
echo "RESULT: payload custody NOT proven"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
EXTRACTED_SERVER_SHA=$(sha256sum "$EXTRACTED_SERVER" | cut -d' ' -f1)
|
||||
EXTRACTED_CONFIG_SHA=$(sha256sum "$EXTRACTED_CONFIG" | cut -d' ' -f1)
|
||||
if [ -n "$PROOF_RECORD" ]; then
|
||||
mkdir -p "$(dirname "$PROOF_RECORD")"
|
||||
WIXL_VERSION=$(wixl --version 2>&1 | head -1 || true)
|
||||
MSIINFO_VERSION=$(msiinfo --version 2>&1 | head -1 || true)
|
||||
MSIEXTRACT_VERSION=$(msiextract --version 2>&1 | head -1 || true)
|
||||
GO_VERSION=$(go version 2>&1 || true)
|
||||
jq -n \
|
||||
--arg source_commit "$SOURCE_COMMIT" --arg source_ref "$SOURCE_REF" \
|
||||
--arg redflag_version "$REDFLAG_VERSION" --arg msi_product_version "$PRODUCT_VERSION" \
|
||||
--arg msi_file "$(basename "$MSI")" --arg msi_sha256 "$MSI_SHA" --argjson msi_size "$MSI_SIZE" \
|
||||
--arg staged_server_sha256 "$STAGED_SERVER_SHA" --argjson staged_server_size "$STAGED_SERVER_SIZE" \
|
||||
--arg extracted_server_sha256 "$EXTRACTED_SERVER_SHA" \
|
||||
--arg staged_config_sha256 "$STAGED_CONFIG_SHA" --argjson staged_config_size "$STAGED_CONFIG_SIZE" \
|
||||
--arg extracted_config_sha256 "$EXTRACTED_CONFIG_SHA" \
|
||||
--arg wixl "$WIXL_VERSION" --arg msiinfo "$MSIINFO_VERSION" \
|
||||
--arg msiextract "$MSIEXTRACT_VERSION" --arg go "$GO_VERSION" \
|
||||
'{record_type:"redflag-installer-custody-proof",signed_release_manifest:false,
|
||||
source:{commit:$source_commit,ref:$source_ref},redflag_version:$redflag_version,
|
||||
msi:{filename:$msi_file,product_version:$msi_product_version,size:$msi_size,sha256:$msi_sha256},
|
||||
staged:{server:{size:$staged_server_size,sha256:$staged_server_sha256},config:{size:$staged_config_size,sha256:$staged_config_sha256}},
|
||||
extracted:{server:{sha256:$extracted_server_sha256},config:{sha256:$extracted_config_sha256}},
|
||||
cabinet_embedded:true,verifier_result:"passed",
|
||||
tools:{wixl:$wixl,msiinfo:$msiinfo,msiextract:$msiextract,go:$go}}' > "$PROOF_RECORD"
|
||||
ok "wrote installer proof record $PROOF_RECORD"
|
||||
fi
|
||||
|
||||
echo "RESULT: payload custody proven"
|
||||
echo "BOUNDARY: this is not a Windows installation or publisher-authentication proof"
|
||||
Loading…
Reference in a new issue