mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-18 01:01:36 +02:00
979 lines
38 KiB
Bash
Executable File
979 lines
38 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Copyright 2026 Mocktail Project Authors
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
set -Eeuo pipefail
|
|
umask 077
|
|
|
|
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)"
|
|
source "${SCRIPT_DIR}/payload_integrity.sh"
|
|
|
|
STORE_ROOT="${MOCKTAIL_PAYLOAD_STORE_ROOT:-${XDG_DATA_HOME:-${HOME}/.local/share}/mocktail}"
|
|
COMPATIBILITY_PATH=""
|
|
EXPECTED_CURRENT=""
|
|
EXPECTED_PAYLOAD_FINGERPRINT=""
|
|
EXPECTED_ROLLBACK_TARGET=""
|
|
CANDIDATE_PROFILE_PATH=""
|
|
CANDIDATE_COMPATIBILITY_PATH=""
|
|
RUNTIME_FINGERPRINT=""
|
|
RUNTIME_BUILD_ID=""
|
|
CANARY_ATTESTATION_PATHS=()
|
|
PROMOTION_APPROVAL_GENERATION=""
|
|
|
|
Log() {
|
|
printf '[payload-store] %s\n' "$*" >&2
|
|
}
|
|
|
|
Die() {
|
|
Log "error: $*"
|
|
exit 1
|
|
}
|
|
|
|
Usage() {
|
|
cat <<'EOF'
|
|
Usage: scripts/payload_store.sh [GLOBAL OPTIONS] COMMAND [ARGUMENTS]
|
|
|
|
Immutable Roblox payload staging and atomic activation.
|
|
|
|
Global options:
|
|
--root DIR Override the XDG payload store root.
|
|
--compatibility FILE Build-ID compatibility profile used by promotion.
|
|
--expected-current ID Require current payload ID to remain unchanged. Use
|
|
"none" when no payload may already be active.
|
|
--expected-payload-fingerprint SHA256
|
|
Require the candidate bytes observed before canary.
|
|
--expected-rollback-target ID
|
|
Require previous-good to match a known baseline.
|
|
--candidate-profile FILE
|
|
Derived exact host-ABI profile for probation promotion.
|
|
--candidate-compatibility FILE
|
|
One-payload compatibility manifest used by probation.
|
|
--runtime-fingerprint SHA256
|
|
Bind approval to the canary runtime executable bytes.
|
|
--runtime-build-id HEX
|
|
Bind approval to the installed runtime ELF Build ID.
|
|
--canary-attestation FILE
|
|
Passed Tier C attestation. Supply exactly twice for
|
|
probation promotion.
|
|
|
|
Commands:
|
|
stage PAYLOAD_DIR Validate and immutably stage a prepared payload.
|
|
import PAYLOAD_DIR Stage, then promote only an exact supported Build ID.
|
|
promote PAYLOAD_ID Atomically activate an exact supported payload.
|
|
promote-probation PAYLOAD_ID
|
|
Approve and activate a double-canary candidate.
|
|
fingerprint PAYLOAD_ID
|
|
Verify and fingerprint immutable runtime bytes.
|
|
verify-current Verify current manifest, payload bytes, and any
|
|
probation approval generation; print its payload ID.
|
|
rollback Restore current.json from previous_good.json.
|
|
status Print current and previous-good manifests.
|
|
|
|
PAYLOAD_DIR must contain libroblox.so, assets/, sober_apk/base.apk,
|
|
sober_apk/split_config.x86_64.apk, and roblox_payload.json. APK/ELF validation
|
|
belongs to update_roblox_payload.sh; this command verifies the resulting hashes
|
|
and metadata before crossing the immutable store boundary.
|
|
EOF
|
|
}
|
|
|
|
RequireCommands() {
|
|
local command_name
|
|
for command_name in flock jq sha256sum sort; do
|
|
command -v "${command_name}" >/dev/null 2>&1 ||
|
|
Die "missing required command: ${command_name}"
|
|
done
|
|
}
|
|
|
|
ParseGlobalOptions() {
|
|
while (( $# > 0 )); do
|
|
case "$1" in
|
|
--root)
|
|
(( $# >= 2 )) || Die "--root requires a directory"
|
|
STORE_ROOT="$2"
|
|
shift 2
|
|
;;
|
|
--compatibility)
|
|
(( $# >= 2 )) || Die "--compatibility requires a file"
|
|
COMPATIBILITY_PATH="$2"
|
|
shift 2
|
|
;;
|
|
--expected-current)
|
|
(( $# >= 2 )) || Die "--expected-current requires a payload ID or none"
|
|
EXPECTED_CURRENT="$2"
|
|
shift 2
|
|
;;
|
|
--expected-payload-fingerprint)
|
|
(( $# >= 2 )) ||
|
|
Die "--expected-payload-fingerprint requires a SHA-256 digest"
|
|
EXPECTED_PAYLOAD_FINGERPRINT="$2"
|
|
shift 2
|
|
;;
|
|
--expected-rollback-target)
|
|
(( $# >= 2 )) ||
|
|
Die "--expected-rollback-target requires a payload ID"
|
|
EXPECTED_ROLLBACK_TARGET="$2"
|
|
shift 2
|
|
;;
|
|
--candidate-profile)
|
|
(( $# >= 2 )) || Die "--candidate-profile requires a file"
|
|
CANDIDATE_PROFILE_PATH="$2"
|
|
shift 2
|
|
;;
|
|
--candidate-compatibility)
|
|
(( $# >= 2 )) || Die "--candidate-compatibility requires a file"
|
|
CANDIDATE_COMPATIBILITY_PATH="$2"
|
|
shift 2
|
|
;;
|
|
--runtime-fingerprint)
|
|
(( $# >= 2 )) || Die "--runtime-fingerprint requires a SHA-256 digest"
|
|
RUNTIME_FINGERPRINT="$2"
|
|
shift 2
|
|
;;
|
|
--runtime-build-id)
|
|
(( $# >= 2 )) || Die "--runtime-build-id requires a hex Build ID"
|
|
RUNTIME_BUILD_ID="${2,,}"
|
|
shift 2
|
|
;;
|
|
--canary-attestation)
|
|
(( $# >= 2 )) || Die "--canary-attestation requires a file"
|
|
CANARY_ATTESTATION_PATHS+=("$2")
|
|
shift 2
|
|
;;
|
|
-h|--help)
|
|
Usage
|
|
exit 0
|
|
;;
|
|
--)
|
|
shift
|
|
break
|
|
;;
|
|
-*)
|
|
Die "unknown global option: $1"
|
|
;;
|
|
*)
|
|
break
|
|
;;
|
|
esac
|
|
done
|
|
REMAINING_ARGUMENTS=("$@")
|
|
}
|
|
|
|
MetadataPath() {
|
|
printf '%s/roblox_payload.json\n' "$1"
|
|
}
|
|
|
|
PayloadFingerprint() {
|
|
jq -Sc '{schema_version, package, version_name, version_code, abi,
|
|
elf_build_id: (.elf_build_id | ascii_downcase), sha256, assets}' \
|
|
"$(MetadataPath "$1")"
|
|
}
|
|
|
|
ReadPayloadId() {
|
|
local -r payload_dir="$1"
|
|
local -r metadata="$(MetadataPath "${payload_dir}")"
|
|
[[ -f "${metadata}" ]] || Die "payload metadata is missing: ${metadata}"
|
|
|
|
local version_code build_id
|
|
version_code="$(jq -er '.version_code | select(type == "number" and floor == . and . >= 0)' "${metadata}")" ||
|
|
Die "payload metadata has an invalid version_code"
|
|
build_id="$(jq -er '.elf_build_id | ascii_downcase | select(test("^[0-9a-f]{40}$"))' "${metadata}")" ||
|
|
Die "payload metadata has an invalid ELF Build ID"
|
|
printf '%s-%s\n' "${version_code}" "${build_id}"
|
|
}
|
|
|
|
ReadManifestPayloadId() {
|
|
local -r manifest="$1"
|
|
[[ -f "${manifest}" && ! -L "${manifest}" ]] ||
|
|
Die "payload manifest is not a regular file: ${manifest}"
|
|
jq -er '
|
|
select(.schema_version == 1) |
|
|
select((.payload_id | type) == "string" and
|
|
(.payload_id | test("^[0-9]+-[0-9a-f]{40}$"))) |
|
|
select(.payload_path == ("payloads/" + .payload_id)) |
|
|
select((.version_code | type) == "number" and
|
|
(.version_code | floor == . and . >= 0)) |
|
|
select((.elf_build_id | type) == "string" and
|
|
(.elf_build_id | test("^[0-9a-f]{40}$"))) |
|
|
select(.payload_id == ((.version_code | tostring) + "-" +
|
|
(.elf_build_id | ascii_downcase))) |
|
|
.payload_id
|
|
' "${manifest}" || Die "payload manifest is invalid: ${manifest}"
|
|
}
|
|
|
|
VerifyPayload() {
|
|
local -r payload_dir="$1"
|
|
[[ -d "${payload_dir}" ]] || Die "payload directory is not readable: ${payload_dir}"
|
|
|
|
local required_path
|
|
for required_path in \
|
|
libroblox.so \
|
|
sober_apk/base.apk \
|
|
sober_apk/split_config.x86_64.apk \
|
|
roblox_payload.json; do
|
|
[[ -f "${payload_dir}/${required_path}" ]] ||
|
|
Die "prepared payload is missing ${required_path}"
|
|
done
|
|
[[ -d "${payload_dir}/assets" ]] || Die "prepared payload is missing assets/"
|
|
if find -P "${payload_dir}" -type l -print -quit | grep -q .; then
|
|
Die "prepared payload must not contain symbolic links"
|
|
fi
|
|
|
|
local -r metadata="$(MetadataPath "${payload_dir}")"
|
|
jq -e '
|
|
.schema_version == 1 and
|
|
.package == "com.roblox.client" and
|
|
.abi == "x86_64" and
|
|
(.version_name | type == "string" and length > 0) and
|
|
(.sha256.libroblox | test("^[0-9a-f]{64}$")) and
|
|
(.sha256.base_apk | test("^[0-9a-f]{64}$")) and
|
|
(.sha256.x86_64_split_apk | test("^[0-9a-f]{64}$"))
|
|
' "${metadata}" >/dev/null || Die "prepared payload metadata is incomplete"
|
|
|
|
local expected actual relative_path
|
|
while IFS='|' read -r relative_path expected; do
|
|
actual="$(sha256sum "${payload_dir}/${relative_path}" | awk '{print $1}')"
|
|
[[ "${actual}" == "${expected}" ]] || Die "hash mismatch for ${relative_path}"
|
|
done < <(jq -r '
|
|
["libroblox.so", .sha256.libroblox],
|
|
["sober_apk/base.apk", .sha256.base_apk],
|
|
["sober_apk/split_config.x86_64.apk", .sha256.x86_64_split_apk] |
|
|
@tsv
|
|
' "${metadata}" | tr '\t' '|')
|
|
|
|
local expected_assets actual_assets
|
|
expected_assets="$(jq -er '.assets.file_count | select(type == "number" and floor == . and . >= 0)' "${metadata}")" ||
|
|
Die "payload metadata has an invalid asset count"
|
|
actual_assets="$(find -P "${payload_dir}/assets" -type f -printf . | wc -c)"
|
|
[[ "${actual_assets}" == "${expected_assets}" ]] || Die "asset count mismatch"
|
|
|
|
local expected_asset_tree actual_asset_tree
|
|
expected_asset_tree="$(jq -r '.assets.sha256_tree // empty' "${metadata}")"
|
|
if [[ -n "${expected_asset_tree}" ]]; then
|
|
[[ "${expected_asset_tree}" =~ ^[0-9a-f]{64}$ ]] ||
|
|
Die "payload metadata has an invalid asset tree hash"
|
|
actual_asset_tree="$(PayloadAssetTreeSha256 "${payload_dir}/assets")"
|
|
[[ "${actual_asset_tree}" == "${expected_asset_tree}" ]] ||
|
|
Die "asset tree hash mismatch"
|
|
fi
|
|
}
|
|
|
|
RequirePayloadIdentity() {
|
|
local -r payload_id="$1"
|
|
local derived_payload_id
|
|
derived_payload_id="$(ReadPayloadId "${STORE_ROOT}/payloads/${payload_id}")"
|
|
[[ "${derived_payload_id}" == "${payload_id}" ]] ||
|
|
Die "payload directory identity mismatch: expected ${payload_id}, found ${derived_payload_id}"
|
|
}
|
|
|
|
PayloadRuntimeFingerprint() {
|
|
local -r payload_id="$1"
|
|
local -r payload_dir="${STORE_ROOT}/payloads/${payload_id}"
|
|
RequirePayloadIdentity "${payload_id}"
|
|
VerifyPayload "${payload_dir}"
|
|
local metadata_hash lib_hash base_hash split_hash asset_tree_hash
|
|
metadata_hash="$(sha256sum "${payload_dir}/roblox_payload.json" | awk '{print $1}')"
|
|
lib_hash="$(sha256sum "${payload_dir}/libroblox.so" | awk '{print $1}')"
|
|
base_hash="$(sha256sum "${payload_dir}/sober_apk/base.apk" | awk '{print $1}')"
|
|
split_hash="$(sha256sum "${payload_dir}/sober_apk/split_config.x86_64.apk" | awk '{print $1}')"
|
|
asset_tree_hash="$(PayloadAssetTreeSha256 "${payload_dir}/assets")"
|
|
printf 'payload_id=%s\nmetadata=%s\nlibroblox=%s\nbase_apk=%s\nx86_64_split=%s\nassets=%s\n' \
|
|
"${payload_id}" "${metadata_hash}" "${lib_hash}" "${base_hash}" \
|
|
"${split_hash}" "${asset_tree_hash}" |
|
|
sha256sum | awk '{print $1}'
|
|
}
|
|
|
|
AcquireStoreLock() {
|
|
mkdir -p "${STORE_ROOT}/payloads"
|
|
exec 9>"${STORE_ROOT}/.payload-store.lock"
|
|
flock 9
|
|
}
|
|
|
|
StagePayload() {
|
|
local payload_dir
|
|
payload_dir="$(cd -- "$1" && pwd -P)"
|
|
VerifyPayload "${payload_dir}"
|
|
local payload_id
|
|
payload_id="$(ReadPayloadId "${payload_dir}")"
|
|
local -r destination="${STORE_ROOT}/payloads/${payload_id}"
|
|
|
|
if [[ -e "${destination}" || -L "${destination}" ]]; then
|
|
if [[ -d "${destination}" && ! -L "${destination}" ]] &&
|
|
(VerifyPayload "${destination}") 2>/dev/null; then
|
|
[[ "$(PayloadFingerprint "${payload_dir}")" == "$(PayloadFingerprint "${destination}")" ]] ||
|
|
Die "immutable payload ID collision: ${payload_id}"
|
|
Log "payload already staged: ${payload_id}"
|
|
printf '%s\n' "${payload_id}"
|
|
return 0
|
|
fi
|
|
local -r quarantine_root="${STORE_ROOT}/quarantine"
|
|
local -r quarantine_path="${quarantine_root}/${payload_id}-$(date -u +%Y%m%dT%H%M%SZ)-$$"
|
|
mkdir -p "${quarantine_root}"
|
|
mv -- "${destination}" "${quarantine_path}"
|
|
Log "quarantined corrupt immutable payload: ${quarantine_path}"
|
|
fi
|
|
|
|
local staging_dir
|
|
staging_dir="$(mktemp -d "${STORE_ROOT}/payloads/.stage-${payload_id}.XXXXXX")"
|
|
trap 'rm -rf -- "${staging_dir}"' RETURN
|
|
cp -a -- "${payload_dir}/." "${staging_dir}/"
|
|
VerifyPayload "${staging_dir}"
|
|
chmod -R a-w "${staging_dir}"
|
|
mv -- "${staging_dir}" "${destination}"
|
|
trap - RETURN
|
|
Log "staged immutable payload: ${payload_id}"
|
|
printf '%s\n' "${payload_id}"
|
|
}
|
|
|
|
IsSupportedProfile() {
|
|
local -r payload_id="$1"
|
|
local -r metadata="${STORE_ROOT}/payloads/${payload_id}/roblox_payload.json"
|
|
[[ -f "${metadata}" ]] || return 1
|
|
[[ -n "${COMPATIBILITY_PATH}" && -r "${COMPATIBILITY_PATH}" ]] || return 1
|
|
|
|
local build_id version_code
|
|
build_id="$(jq -r '.elf_build_id | ascii_downcase' "${metadata}")"
|
|
version_code="$(jq -r '.version_code' "${metadata}")"
|
|
jq -e --arg build_id "${build_id}" --argjson version_code "${version_code}" '
|
|
any(.profiles[]?;
|
|
(.elf_build_id | ascii_downcase) == $build_id and
|
|
.version_code == $version_code and
|
|
.status == "supported" and
|
|
.default_allowed == true and
|
|
.allow_legacy_binary_patches == false)
|
|
' "${COMPATIBILITY_PATH}" >/dev/null
|
|
}
|
|
|
|
ApprovalReceiptPath() {
|
|
printf '%s/approvals/%s-%s.json\n' "${STORE_ROOT}" "$1" "$2"
|
|
}
|
|
|
|
ApprovedProfilePath() {
|
|
printf '%s/host_abi_profiles/%s-%s.json\n' "${STORE_ROOT}" "$1" "$2"
|
|
}
|
|
|
|
ApprovedCompatibilityPath() {
|
|
printf '%s/compatibility_profiles/%s-%s.json\n' "${STORE_ROOT}" "$1" "$2"
|
|
}
|
|
|
|
ApprovedCanaryPath() {
|
|
printf '%s/approvals/%s-%s.canary-%s.json\n' \
|
|
"${STORE_ROOT}" "$1" "$2" "$3"
|
|
}
|
|
|
|
ManifestApprovalGeneration() {
|
|
local -r manifest="$1"
|
|
local -r payload_id="$2"
|
|
local approval_path profile_path compatibility_path generation
|
|
approval_path="$(jq -er '.approval_path | select(type == "string")' \
|
|
"${manifest}" 2>/dev/null || true)"
|
|
profile_path="$(jq -er '.host_abi_profile_path |
|
|
select(type == "string")' "${manifest}" 2>/dev/null || true)"
|
|
compatibility_path="$(jq -er '.compatibility_manifest_path |
|
|
select(type == "string")' "${manifest}" 2>/dev/null || true)"
|
|
if [[ "${approval_path}" =~ ^approvals/${payload_id}-([0-9a-f]{40})\.json$ ]]; then
|
|
generation="${BASH_REMATCH[1]}"
|
|
else
|
|
return 1
|
|
fi
|
|
[[ "${profile_path}" == \
|
|
"host_abi_profiles/${payload_id}-${generation}.json" &&
|
|
"${compatibility_path}" == \
|
|
"compatibility_profiles/${payload_id}-${generation}.json" ]] ||
|
|
return 1
|
|
printf '%s\n' "${generation}"
|
|
}
|
|
|
|
IsExactCandidateProfile() {
|
|
local -r payload_id="$1"
|
|
local -r profile_path="$2"
|
|
local -r payload_dir="${STORE_ROOT}/payloads/${payload_id}"
|
|
local -r metadata="${payload_dir}/roblox_payload.json"
|
|
[[ -f "${profile_path}" && ! -L "${profile_path}" ]] || return 1
|
|
|
|
local build_id version_code libroblox_sha256
|
|
build_id="$(jq -er '.elf_build_id | ascii_downcase |
|
|
select(test("^[0-9a-f]{40}$"))' "${metadata}" 2>/dev/null)" || return 1
|
|
version_code="$(jq -er '.version_code |
|
|
select(type == "number" and floor == . and . >= 0)' \
|
|
"${metadata}" 2>/dev/null)" || return 1
|
|
libroblox_sha256="$(jq -er '.sha256.libroblox |
|
|
select(test("^[0-9a-f]{64}$"))' "${metadata}" 2>/dev/null)" || return 1
|
|
|
|
jq -e --arg payload_id "${payload_id}" \
|
|
--arg payload_path "payloads/${payload_id}" \
|
|
--arg build_id "${build_id}" \
|
|
--arg payload_sha256 "${libroblox_sha256}" '
|
|
.schema_version == 1 and
|
|
.payload_id == $payload_id and
|
|
.payload_path == $payload_path and
|
|
(.elf_build_id | ascii_downcase) == $build_id and
|
|
.payload_sha256 == $payload_sha256 and
|
|
(.reference | type) == "object" and
|
|
(.profile | type) == "object" and
|
|
(.profile.bridge_entries | type) == "array" and
|
|
(.profile.data_seeds | type) == "object" and
|
|
(.profile.native_allocator | type) == "object" and
|
|
(.profile.constructor_run_ranges | type) == "array" and
|
|
(.profile.native_mimalloc_constructor_run_ranges | type) == "array" and
|
|
(.profile.native_pre_jni_bootstrap | type) == "object" and
|
|
.profile.default_allocator_strategy == "native_mimalloc"
|
|
' "${profile_path}" >/dev/null
|
|
}
|
|
|
|
IsExactCandidateCompatibility() {
|
|
local -r payload_id="$1"
|
|
local -r manifest_path="$2"
|
|
local -r metadata="${STORE_ROOT}/payloads/${payload_id}/roblox_payload.json"
|
|
[[ -f "${manifest_path}" && ! -L "${manifest_path}" ]] || return 1
|
|
local build_id version_code
|
|
build_id="$(jq -er '.elf_build_id | ascii_downcase' "${metadata}" 2>/dev/null)" || return 1
|
|
version_code="$(jq -er '.version_code' "${metadata}" 2>/dev/null)" || return 1
|
|
jq -e --arg build_id "${build_id}" --argjson version_code "${version_code}" '
|
|
.schema_version == 1 and
|
|
(.profiles | type) == "array" and
|
|
(.profiles | length) == 1 and
|
|
.profiles[0].version_code == $version_code and
|
|
(.profiles[0].elf_build_id | ascii_downcase) == $build_id and
|
|
.profiles[0].status == "experimental" and
|
|
.profiles[0].default_allowed == true and
|
|
.profiles[0].allow_legacy_binary_patches == false and
|
|
.profiles[0].allow_host_abi_bridges == true and
|
|
.profiles[0].allow_host_constructor_replay == true
|
|
' "${manifest_path}" >/dev/null
|
|
}
|
|
|
|
IsApprovedProfile() {
|
|
local -r payload_id="$1"
|
|
local -r generation="$2"
|
|
[[ "${generation}" =~ ^[0-9a-f]{40}$ ]] || return 1
|
|
local -r receipt="$(ApprovalReceiptPath "${payload_id}" "${generation}")"
|
|
local -r profile="$(ApprovedProfilePath "${payload_id}" "${generation}")"
|
|
local -r compatibility="$(ApprovedCompatibilityPath \
|
|
"${payload_id}" "${generation}")"
|
|
local -r canary_one="$(ApprovedCanaryPath \
|
|
"${payload_id}" "${generation}" 1)"
|
|
local -r canary_two="$(ApprovedCanaryPath \
|
|
"${payload_id}" "${generation}" 2)"
|
|
local regular_path
|
|
for regular_path in "${receipt}" "${profile}" "${compatibility}" \
|
|
"${canary_one}" "${canary_two}"; do
|
|
[[ -f "${regular_path}" && ! -L "${regular_path}" ]] || return 1
|
|
done
|
|
IsExactCandidateProfile "${payload_id}" "${profile}" || return 1
|
|
IsExactCandidateCompatibility "${payload_id}" "${compatibility}" || return 1
|
|
|
|
local payload_fingerprint libroblox_sha256 profile_sha256
|
|
local compatibility_sha256 canary_one_sha256 canary_two_sha256
|
|
payload_fingerprint="$(PayloadRuntimeFingerprint "${payload_id}" 2>/dev/null)" || return 1
|
|
libroblox_sha256="$(sha256sum \
|
|
"${STORE_ROOT}/payloads/${payload_id}/libroblox.so" | awk '{print $1}')"
|
|
profile_sha256="$(sha256sum "${profile}" | awk '{print $1}')"
|
|
compatibility_sha256="$(sha256sum "${compatibility}" | awk '{print $1}')"
|
|
canary_one_sha256="$(sha256sum "${canary_one}" | awk '{print $1}')"
|
|
canary_two_sha256="$(sha256sum "${canary_two}" | awk '{print $1}')"
|
|
local canonical_library
|
|
canonical_library="$(cd -- "${STORE_ROOT}/payloads/${payload_id}" && pwd -P)/libroblox.so"
|
|
local receipt_runtime_sha256 receipt_runtime_build_id expected_generation
|
|
receipt_runtime_sha256="$(jq -er '.canary_runtime_sha256 |
|
|
select(test("^[0-9a-f]{64}$"))' "${receipt}")" || return 1
|
|
receipt_runtime_build_id="$(jq -er '.runtime_build_id |
|
|
select(test("^[0-9a-f]{40}$"))' "${receipt}")" || return 1
|
|
expected_generation="$(printf '%s\n' \
|
|
"runtime_build_id=${receipt_runtime_build_id}" \
|
|
"payload=${payload_fingerprint}" \
|
|
"profile=${profile_sha256}" \
|
|
"compatibility=${compatibility_sha256}" \
|
|
"canary_1=${canary_one_sha256}" \
|
|
"canary_2=${canary_two_sha256}" |
|
|
sha256sum | awk '{print substr($1, 1, 40)}')"
|
|
[[ "${generation}" == "${expected_generation}" ]] || return 1
|
|
|
|
jq -e --arg payload_id "${payload_id}" \
|
|
--arg generation "${generation}" \
|
|
--arg payload_path "${canonical_library}" \
|
|
--arg build_id "${payload_id#*-}" \
|
|
--arg payload_sha256 "${libroblox_sha256}" \
|
|
--arg payload_fingerprint "${payload_fingerprint}" \
|
|
--arg profile_sha256 "${profile_sha256}" \
|
|
--arg compatibility_sha256 "${compatibility_sha256}" \
|
|
--arg canary_one_sha256 "${canary_one_sha256}" \
|
|
--arg canary_two_sha256 "${canary_two_sha256}" \
|
|
--arg runtime_sha256 "${receipt_runtime_sha256}" \
|
|
--arg runtime_build_id "${receipt_runtime_build_id}" '
|
|
.schema_version == 1 and .status == "approved" and
|
|
.generation == $generation and
|
|
.payload_id == $payload_id and .payload_path == $payload_path and
|
|
(.elf_build_id | ascii_downcase) == $build_id and
|
|
.payload_sha256 == $payload_sha256 and
|
|
.payload_fingerprint == $payload_fingerprint and
|
|
.profile_sha256 == $profile_sha256 and
|
|
.compatibility_manifest_sha256 == $compatibility_sha256 and
|
|
.canary_runtime_sha256 == $runtime_sha256 and
|
|
.runtime_sha256 == $runtime_sha256 and
|
|
.runtime_build_id == $runtime_build_id and
|
|
.canary_tier == "C" and .successful_runs == 2 and
|
|
.canary_attestation_sha256 ==
|
|
[$canary_one_sha256, $canary_two_sha256]
|
|
' "${receipt}" >/dev/null || return 1
|
|
VerifyCanaryAttestation "${canary_one}" 1 "${payload_id}" \
|
|
"${payload_fingerprint}" "${profile_sha256}" \
|
|
"${compatibility_sha256}" "${receipt_runtime_sha256}" \
|
|
"${receipt_runtime_build_id}" || return 1
|
|
VerifyCanaryAttestation "${canary_two}" 2 "${payload_id}" \
|
|
"${payload_fingerprint}" "${profile_sha256}" \
|
|
"${compatibility_sha256}" "${receipt_runtime_sha256}" \
|
|
"${receipt_runtime_build_id}" || return 1
|
|
[[ "$(jq -er '.run_id' "${canary_one}")" != \
|
|
"$(jq -er '.run_id' "${canary_two}")" ]]
|
|
}
|
|
|
|
ValidateManifestPayload() {
|
|
local -r manifest="$1"
|
|
local payload_id approval_generation=""
|
|
payload_id="$(ReadManifestPayloadId "${manifest}")"
|
|
if IsSupportedProfile "${payload_id}"; then
|
|
RequireSupportedProfile "${payload_id}"
|
|
else
|
|
approval_generation="$(ManifestApprovalGeneration \
|
|
"${manifest}" "${payload_id}")" ||
|
|
Die "approved payload manifest has invalid approval references"
|
|
RequireSupportedProfile "${payload_id}" "${approval_generation}"
|
|
fi
|
|
printf '%s|%s\n' "${payload_id}" "${approval_generation}"
|
|
}
|
|
|
|
RequireSupportedProfile() {
|
|
local -r payload_id="$1"
|
|
local -r approval_generation="${2:-}"
|
|
[[ -d "${STORE_ROOT}/payloads/${payload_id}" ]] || Die "payload is not staged: ${payload_id}"
|
|
RequirePayloadIdentity "${payload_id}"
|
|
[[ -n "${COMPATIBILITY_PATH}" && -r "${COMPATIBILITY_PATH}" ]] ||
|
|
Die "promotion requires --compatibility FILE"
|
|
if ! IsSupportedProfile "${payload_id}" &&
|
|
! IsApprovedProfile "${payload_id}" "${approval_generation}"; then
|
|
Die "payload ${payload_id} is neither exact-supported nor probation-approved"
|
|
fi
|
|
VerifyPayload "${STORE_ROOT}/payloads/${payload_id}"
|
|
}
|
|
|
|
CurrentPayloadId() {
|
|
local -r current="${STORE_ROOT}/current.json"
|
|
if [[ ! -e "${current}" && ! -L "${current}" ]]; then
|
|
printf 'none\n'
|
|
return 0
|
|
fi
|
|
local manifest_identity payload_id
|
|
manifest_identity="$(ValidateManifestPayload "${current}")"
|
|
payload_id="${manifest_identity%%|*}"
|
|
printf '%s\n' "${payload_id}"
|
|
}
|
|
|
|
RequireExpectedCurrent() {
|
|
[[ -n "${EXPECTED_CURRENT}" ]] || return 0
|
|
[[ "${EXPECTED_CURRENT}" == none ||
|
|
"${EXPECTED_CURRENT}" =~ ^[0-9]+-[0-9a-f]{40}$ ]] ||
|
|
Die "--expected-current has an invalid payload ID"
|
|
local actual
|
|
actual="$(CurrentPayloadId)"
|
|
[[ "${actual}" == "${EXPECTED_CURRENT}" ]] ||
|
|
Die "current payload changed during update: expected ${EXPECTED_CURRENT}, found ${actual}"
|
|
}
|
|
|
|
WriteManifest() {
|
|
local -r payload_id="$1"
|
|
local -r destination="$2"
|
|
local -r approval_generation="${3:-}"
|
|
local -r metadata="${STORE_ROOT}/payloads/${payload_id}/roblox_payload.json"
|
|
local temporary
|
|
temporary="$(mktemp "${STORE_ROOT}/.manifest.XXXXXX")"
|
|
local approved=false
|
|
if ! IsSupportedProfile "${payload_id}" &&
|
|
IsApprovedProfile "${payload_id}" "${approval_generation}"; then
|
|
approved=true
|
|
fi
|
|
if ! jq -n \
|
|
--arg payload_id "${payload_id}" \
|
|
--arg payload_path "payloads/${payload_id}" \
|
|
--arg version_name "$(jq -r '.version_name' "${metadata}")" \
|
|
--argjson version_code "$(jq -r '.version_code' "${metadata}")" \
|
|
--arg build_id "$(jq -r '.elf_build_id | ascii_downcase' "${metadata}")" \
|
|
--arg activated_at "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
|
|
--argjson approved "${approved}" \
|
|
--arg approval_path \
|
|
"approvals/${payload_id}-${approval_generation}.json" \
|
|
--arg profile_path \
|
|
"host_abi_profiles/${payload_id}-${approval_generation}.json" \
|
|
--arg compatibility_path \
|
|
"compatibility_profiles/${payload_id}-${approval_generation}.json" \
|
|
'{schema_version: 1, payload_id: $payload_id, payload_path: $payload_path,
|
|
version_name: $version_name, version_code: $version_code,
|
|
elf_build_id: $build_id, activated_at: $activated_at} +
|
|
(if $approved then {
|
|
approval_path: $approval_path,
|
|
host_abi_profile_path: $profile_path,
|
|
compatibility_manifest_path: $compatibility_path
|
|
} else {} end)' > "${temporary}"; then
|
|
rm -f -- "${temporary}"
|
|
return 1
|
|
fi
|
|
if ! chmod 0644 "${temporary}"; then
|
|
rm -f -- "${temporary}"
|
|
return 1
|
|
fi
|
|
if ! mv -f -- "${temporary}" "${destination}"; then
|
|
rm -f -- "${temporary}"
|
|
return 1
|
|
fi
|
|
}
|
|
|
|
CopyApprovalArtifact() {
|
|
local -r source_path="$1"
|
|
local -r destination_path="$2"
|
|
local -r expected_sha256="$3"
|
|
[[ -f "${source_path}" && ! -L "${source_path}" ]] ||
|
|
Die "approval input is not a regular file: ${source_path}"
|
|
[[ "${expected_sha256}" =~ ^[0-9a-f]{64}$ ]] ||
|
|
Die "approval artifact has an invalid expected hash"
|
|
mkdir -p -- "${destination_path%/*}"
|
|
if [[ -e "${destination_path}" || -L "${destination_path}" ]]; then
|
|
[[ -f "${destination_path}" && ! -L "${destination_path}" &&
|
|
"$(sha256sum "${destination_path}" | awk '{print $1}')" == \
|
|
"${expected_sha256}" ]] ||
|
|
Die "immutable approval generation collision: ${destination_path}"
|
|
return 0
|
|
fi
|
|
local temporary_path
|
|
temporary_path="$(mktemp "${destination_path%/*}/.approval-artifact.XXXXXX")"
|
|
if ! cp -- "${source_path}" "${temporary_path}" ||
|
|
[[ "$(sha256sum "${temporary_path}" | awk '{print $1}')" != \
|
|
"${expected_sha256}" ]] ||
|
|
! chmod 0444 -- "${temporary_path}" ||
|
|
! mv -- "${temporary_path}" "${destination_path}"; then
|
|
rm -f -- "${temporary_path}"
|
|
Die "failed to persist an approval artifact"
|
|
fi
|
|
}
|
|
|
|
VerifyCanaryAttestation() {
|
|
local -r attestation_path="$1"
|
|
local -r expected_run="$2"
|
|
local -r payload_id="$3"
|
|
local -r payload_fingerprint="$4"
|
|
local -r profile_sha256="$5"
|
|
local -r compatibility_sha256="$6"
|
|
local -r runtime_sha256="$7"
|
|
local -r runtime_build_id="$8"
|
|
[[ -f "${attestation_path}" && ! -L "${attestation_path}" ]] || return 1
|
|
jq -e --argjson run "${expected_run}" \
|
|
--arg payload_id "${payload_id}" \
|
|
--arg payload_fingerprint "${payload_fingerprint}" \
|
|
--arg profile_sha256 "${profile_sha256}" \
|
|
--arg compatibility_sha256 "${compatibility_sha256}" \
|
|
--arg runtime_sha256 "${runtime_sha256}" \
|
|
--arg runtime_build_id "${runtime_build_id}" '
|
|
.schema_version == 1 and .status == "passed" and
|
|
.canary_tier == "C" and .run == $run and
|
|
.payload_id == $payload_id and
|
|
.payload_fingerprint == $payload_fingerprint and
|
|
.profile_sha256 == $profile_sha256 and
|
|
.compatibility_manifest_sha256 == $compatibility_sha256 and
|
|
.runtime_sha256 == $runtime_sha256 and
|
|
.runtime_build_id == $runtime_build_id and
|
|
(.run_id | type == "string" and
|
|
test("^[0-9]+-[0-9a-f]{40}-[12]-[0-9]+-[0-9]+$")) and
|
|
(.readiness_log_sha256 | test("^[0-9a-f]{64}$"))
|
|
' "${attestation_path}" >/dev/null
|
|
}
|
|
|
|
PromoteProbationPayload() {
|
|
local -r payload_id="$1"
|
|
[[ "${payload_id}" =~ ^[0-9]+-[0-9a-f]{40}$ ]] ||
|
|
Die "invalid payload ID: ${payload_id}"
|
|
[[ -d "${STORE_ROOT}/payloads/${payload_id}" ]] ||
|
|
Die "payload is not staged: ${payload_id}"
|
|
RequirePayloadIdentity "${payload_id}"
|
|
VerifyPayload "${STORE_ROOT}/payloads/${payload_id}"
|
|
IsSupportedProfile "${payload_id}" &&
|
|
Die "an exact-supported payload must use regular promotion"
|
|
[[ -n "${EXPECTED_PAYLOAD_FINGERPRINT}" &&
|
|
"${EXPECTED_PAYLOAD_FINGERPRINT}" =~ ^[0-9a-f]{64}$ ]] ||
|
|
Die "probation promotion requires --expected-payload-fingerprint"
|
|
[[ "${RUNTIME_FINGERPRINT}" =~ ^[0-9a-f]{64}$ ]] ||
|
|
Die "probation promotion requires a valid --runtime-fingerprint"
|
|
[[ "${RUNTIME_BUILD_ID}" =~ ^[0-9a-f]{40}$ ]] ||
|
|
Die "probation promotion requires a valid --runtime-build-id"
|
|
(( ${#CANARY_ATTESTATION_PATHS[@]} == 2 )) ||
|
|
Die "probation promotion requires exactly two canary attestations"
|
|
IsExactCandidateProfile "${payload_id}" "${CANDIDATE_PROFILE_PATH}" ||
|
|
Die "derived candidate profile is not bound to ${payload_id}"
|
|
IsExactCandidateCompatibility "${payload_id}" \
|
|
"${CANDIDATE_COMPATIBILITY_PATH}" ||
|
|
Die "candidate compatibility manifest is not exact probation metadata"
|
|
|
|
local actual_fingerprint profile_sha256 compatibility_sha256
|
|
actual_fingerprint="$(PayloadRuntimeFingerprint "${payload_id}")"
|
|
[[ "${actual_fingerprint}" == "${EXPECTED_PAYLOAD_FINGERPRINT}" ]] ||
|
|
Die "payload ${payload_id} changed after canary"
|
|
profile_sha256="$(sha256sum "${CANDIDATE_PROFILE_PATH}" | awk '{print $1}')"
|
|
compatibility_sha256="$(sha256sum "${CANDIDATE_COMPATIBILITY_PATH}" | awk '{print $1}')"
|
|
VerifyCanaryAttestation "${CANARY_ATTESTATION_PATHS[0]}" 1 \
|
|
"${payload_id}" "${actual_fingerprint}" "${profile_sha256}" \
|
|
"${compatibility_sha256}" "${RUNTIME_FINGERPRINT}" \
|
|
"${RUNTIME_BUILD_ID}" ||
|
|
Die "first canary attestation is invalid"
|
|
VerifyCanaryAttestation "${CANARY_ATTESTATION_PATHS[1]}" 2 \
|
|
"${payload_id}" "${actual_fingerprint}" "${profile_sha256}" \
|
|
"${compatibility_sha256}" "${RUNTIME_FINGERPRINT}" \
|
|
"${RUNTIME_BUILD_ID}" ||
|
|
Die "second canary attestation is invalid"
|
|
[[ "$(jq -er '.run_id' "${CANARY_ATTESTATION_PATHS[0]}")" != \
|
|
"$(jq -er '.run_id' "${CANARY_ATTESTATION_PATHS[1]}")" ]] ||
|
|
Die "canary attestations must have distinct run IDs"
|
|
RequireExpectedCurrent
|
|
|
|
local canary_one_sha256 canary_two_sha256
|
|
canary_one_sha256="$(sha256sum "${CANARY_ATTESTATION_PATHS[0]}" | awk '{print $1}')"
|
|
canary_two_sha256="$(sha256sum "${CANARY_ATTESTATION_PATHS[1]}" | awk '{print $1}')"
|
|
local approval_generation
|
|
approval_generation="$(printf '%s\n' \
|
|
"runtime_build_id=${RUNTIME_BUILD_ID}" \
|
|
"payload=${actual_fingerprint}" \
|
|
"profile=${profile_sha256}" \
|
|
"compatibility=${compatibility_sha256}" \
|
|
"canary_1=${canary_one_sha256}" \
|
|
"canary_2=${canary_two_sha256}" |
|
|
sha256sum | awk '{print substr($1, 1, 40)}')"
|
|
CopyApprovalArtifact "${CANDIDATE_PROFILE_PATH}" \
|
|
"$(ApprovedProfilePath "${payload_id}" "${approval_generation}")" \
|
|
"${profile_sha256}"
|
|
CopyApprovalArtifact "${CANDIDATE_COMPATIBILITY_PATH}" \
|
|
"$(ApprovedCompatibilityPath \
|
|
"${payload_id}" "${approval_generation}")" "${compatibility_sha256}"
|
|
CopyApprovalArtifact "${CANARY_ATTESTATION_PATHS[0]}" \
|
|
"$(ApprovedCanaryPath "${payload_id}" "${approval_generation}" 1)" \
|
|
"${canary_one_sha256}"
|
|
CopyApprovalArtifact "${CANARY_ATTESTATION_PATHS[1]}" \
|
|
"$(ApprovedCanaryPath "${payload_id}" "${approval_generation}" 2)" \
|
|
"${canary_two_sha256}"
|
|
|
|
local canonical_library libroblox_sha256 receipt_temporary receipt_path
|
|
canonical_library="$(cd -- "${STORE_ROOT}/payloads/${payload_id}" && pwd -P)/libroblox.so"
|
|
libroblox_sha256="$(sha256sum "${canonical_library}" | awk '{print $1}')"
|
|
receipt_path="$(ApprovalReceiptPath \
|
|
"${payload_id}" "${approval_generation}")"
|
|
receipt_temporary="$(mktemp "${STORE_ROOT}/approvals/.receipt.XXXXXX")"
|
|
jq -n \
|
|
--arg payload_id "${payload_id}" \
|
|
--arg generation "${approval_generation}" \
|
|
--arg payload_path "${canonical_library}" \
|
|
--arg build_id "${payload_id#*-}" \
|
|
--arg payload_sha256 "${libroblox_sha256}" \
|
|
--arg payload_fingerprint "${actual_fingerprint}" \
|
|
--arg profile_sha256 "${profile_sha256}" \
|
|
--arg compatibility_sha256 "${compatibility_sha256}" \
|
|
--arg runtime_sha256 "${RUNTIME_FINGERPRINT}" \
|
|
--arg runtime_build_id "${RUNTIME_BUILD_ID}" \
|
|
--arg canary_one_sha256 "${canary_one_sha256}" \
|
|
--arg canary_two_sha256 "${canary_two_sha256}" \
|
|
--arg approved_at "$(date -u +'%Y-%m-%dT%H:%M:%SZ')" '
|
|
{schema_version: 1, status: "approved", payload_id: $payload_id,
|
|
generation: $generation,
|
|
payload_path: $payload_path, elf_build_id: $build_id,
|
|
payload_sha256: $payload_sha256,
|
|
payload_fingerprint: $payload_fingerprint,
|
|
profile_sha256: $profile_sha256,
|
|
compatibility_manifest_sha256: $compatibility_sha256,
|
|
canary_runtime_sha256: $runtime_sha256,
|
|
runtime_sha256: $runtime_sha256,
|
|
runtime_build_id: $runtime_build_id, canary_tier: "C",
|
|
successful_runs: 2,
|
|
canary_attestation_sha256:
|
|
[$canary_one_sha256, $canary_two_sha256],
|
|
approved_at: $approved_at}' > "${receipt_temporary}" || {
|
|
rm -f -- "${receipt_temporary}"
|
|
Die "failed to create probation approval receipt"
|
|
}
|
|
chmod 0444 -- "${receipt_temporary}"
|
|
local -r receipt_sha256="$(sha256sum \
|
|
"${receipt_temporary}" | awk '{print $1}')"
|
|
CopyApprovalArtifact "${receipt_temporary}" "${receipt_path}" \
|
|
"${receipt_sha256}"
|
|
rm -f -- "${receipt_temporary}"
|
|
IsApprovedProfile "${payload_id}" "${approval_generation}" ||
|
|
Die "persisted probation approval failed verification"
|
|
PROMOTION_APPROVAL_GENERATION="${approval_generation}"
|
|
PromotePayload "${payload_id}"
|
|
}
|
|
|
|
PromotePayload() {
|
|
local -r payload_id="$1"
|
|
[[ "${payload_id}" =~ ^[0-9]+-[0-9a-f]{40}$ ]] || Die "invalid payload ID: ${payload_id}"
|
|
RequireSupportedProfile "${payload_id}" \
|
|
"${PROMOTION_APPROVAL_GENERATION}"
|
|
if [[ -n "${EXPECTED_PAYLOAD_FINGERPRINT}" ]]; then
|
|
[[ "${EXPECTED_PAYLOAD_FINGERPRINT}" =~ ^[0-9a-f]{64}$ ]] ||
|
|
Die "--expected-payload-fingerprint has an invalid SHA-256 digest"
|
|
local actual_fingerprint
|
|
actual_fingerprint="$(PayloadRuntimeFingerprint "${payload_id}")"
|
|
[[ "${actual_fingerprint}" == "${EXPECTED_PAYLOAD_FINGERPRINT}" ]] ||
|
|
Die "payload ${payload_id} changed after canary"
|
|
fi
|
|
RequireExpectedCurrent
|
|
|
|
local current_payload_id
|
|
current_payload_id="$(CurrentPayloadId)"
|
|
if [[ "${current_payload_id}" == "${payload_id}" ]]; then
|
|
WriteManifest "${payload_id}" "${STORE_ROOT}/current.json" \
|
|
"${PROMOTION_APPROVAL_GENERATION}" ||
|
|
Die "failed to refresh the current payload manifest"
|
|
Log "payload is already current: ${payload_id}"
|
|
return 0
|
|
fi
|
|
local previous_backup=""
|
|
local previous_existed=false
|
|
if [[ "${current_payload_id}" != none ]]; then
|
|
if [[ -e "${STORE_ROOT}/previous_good.json" ||
|
|
-L "${STORE_ROOT}/previous_good.json" ]]; then
|
|
ValidateManifestPayload "${STORE_ROOT}/previous_good.json" >/dev/null
|
|
previous_existed=true
|
|
previous_backup="$(mktemp "${STORE_ROOT}/.previous-backup.XXXXXX")"
|
|
cp -- "${STORE_ROOT}/previous_good.json" "${previous_backup}"
|
|
fi
|
|
local previous_temporary
|
|
previous_temporary="$(mktemp "${STORE_ROOT}/.previous-good.XXXXXX")"
|
|
cp -- "${STORE_ROOT}/current.json" "${previous_temporary}"
|
|
if ! mv -f -- "${previous_temporary}" "${STORE_ROOT}/previous_good.json"; then
|
|
rm -f -- "${previous_temporary}"
|
|
[[ -z "${previous_backup}" ]] || rm -f -- "${previous_backup}"
|
|
return 1
|
|
fi
|
|
fi
|
|
if ! WriteManifest "${payload_id}" "${STORE_ROOT}/current.json" \
|
|
"${PROMOTION_APPROVAL_GENERATION}"; then
|
|
if [[ "${previous_existed}" == true ]]; then
|
|
mv -f -- "${previous_backup}" "${STORE_ROOT}/previous_good.json"
|
|
elif [[ "${current_payload_id}" != none ]]; then
|
|
rm -f -- "${STORE_ROOT}/previous_good.json"
|
|
fi
|
|
return 1
|
|
fi
|
|
if [[ -n "${previous_backup}" ]] &&
|
|
! rm -f -- "${previous_backup}"; then
|
|
Log "warning: failed to remove temporary previous-good backup" || true
|
|
fi
|
|
if IsSupportedProfile "${payload_id}"; then
|
|
Log "promoted supported payload: ${payload_id}" || true
|
|
else
|
|
Log "promoted probation-approved payload: ${payload_id}" || true
|
|
fi
|
|
return 0
|
|
}
|
|
|
|
ImportPayload() {
|
|
local payload_id
|
|
payload_id="$(StagePayload "$1")"
|
|
if IsSupportedProfile "${payload_id}"; then
|
|
PromotePayload "${payload_id}"
|
|
else
|
|
Log "staged ${payload_id} without promotion: Build ID is not default-supported"
|
|
fi
|
|
printf '%s\n' "${payload_id}"
|
|
}
|
|
|
|
RollbackPayload() {
|
|
local -r previous="${STORE_ROOT}/previous_good.json"
|
|
[[ -f "${previous}" ]] || Die "no previous-good payload is available"
|
|
local manifest_identity payload_id approval_generation
|
|
manifest_identity="$(ValidateManifestPayload "${previous}")"
|
|
payload_id="${manifest_identity%%|*}"
|
|
approval_generation="${manifest_identity#*|}"
|
|
if [[ -n "${EXPECTED_ROLLBACK_TARGET}" ]]; then
|
|
[[ "${EXPECTED_ROLLBACK_TARGET}" =~ ^[0-9]+-[0-9a-f]{40}$ ]] ||
|
|
Die "--expected-rollback-target has an invalid payload ID"
|
|
[[ "${payload_id}" == "${EXPECTED_ROLLBACK_TARGET}" ]] ||
|
|
Die "previous-good payload ${payload_id} does not match rollback target ${EXPECTED_ROLLBACK_TARGET}"
|
|
fi
|
|
RequireExpectedCurrent
|
|
|
|
if [[ -z "${approval_generation}" ]]; then
|
|
WriteManifest "${payload_id}" "${STORE_ROOT}/current.json" ||
|
|
Die "failed to atomically restore previous-good payload"
|
|
Log "rolled back to previous-good payload: ${payload_id}" || true
|
|
return 0
|
|
fi
|
|
|
|
local rollback_temporary
|
|
rollback_temporary="$(mktemp "${STORE_ROOT}/.rollback.XXXXXX")"
|
|
if ! cp -- "${previous}" "${rollback_temporary}" ||
|
|
! chmod 0644 -- "${rollback_temporary}" ||
|
|
! ValidateManifestPayload "${rollback_temporary}" >/dev/null ||
|
|
! mv -f -- "${rollback_temporary}" "${STORE_ROOT}/current.json"; then
|
|
rm -f -- "${rollback_temporary}"
|
|
Die "failed to atomically restore previous-good payload"
|
|
fi
|
|
Log "rolled back to previous-good payload: ${payload_id}" || true
|
|
return 0
|
|
}
|
|
|
|
PrintStatus() {
|
|
jq -n \
|
|
--slurpfile current <(if [[ -f "${STORE_ROOT}/current.json" ]]; then cat "${STORE_ROOT}/current.json"; else printf 'null\n'; fi) \
|
|
--slurpfile previous <(if [[ -f "${STORE_ROOT}/previous_good.json" ]]; then cat "${STORE_ROOT}/previous_good.json"; else printf 'null\n'; fi) \
|
|
'{current: $current[0], previous_good: $previous[0]}'
|
|
}
|
|
|
|
Main() {
|
|
RequireCommands
|
|
ParseGlobalOptions "$@"
|
|
set -- "${REMAINING_ARGUMENTS[@]}"
|
|
(( $# > 0 )) || { Usage >&2; exit 2; }
|
|
local -r command="$1"
|
|
shift
|
|
AcquireStoreLock
|
|
case "${command}" in
|
|
stage)
|
|
(( $# == 1 )) || Die "stage requires PAYLOAD_DIR"
|
|
StagePayload "$1"
|
|
;;
|
|
import)
|
|
(( $# == 1 )) || Die "import requires PAYLOAD_DIR"
|
|
ImportPayload "$1"
|
|
;;
|
|
promote)
|
|
(( $# == 1 )) || Die "promote requires PAYLOAD_ID"
|
|
PromotePayload "$1"
|
|
;;
|
|
promote-probation)
|
|
(( $# == 1 )) || Die "promote-probation requires PAYLOAD_ID"
|
|
PromoteProbationPayload "$1"
|
|
;;
|
|
fingerprint)
|
|
(( $# == 1 )) || Die "fingerprint requires PAYLOAD_ID"
|
|
[[ "$1" =~ ^[0-9]+-[0-9a-f]{40}$ ]] || Die "invalid payload ID: $1"
|
|
PayloadRuntimeFingerprint "$1"
|
|
;;
|
|
verify-current)
|
|
(( $# == 0 )) || Die "verify-current accepts no arguments"
|
|
CurrentPayloadId
|
|
;;
|
|
rollback)
|
|
(( $# == 0 )) || Die "rollback accepts no arguments"
|
|
RollbackPayload
|
|
;;
|
|
status)
|
|
(( $# == 0 )) || Die "status accepts no arguments"
|
|
PrintStatus
|
|
;;
|
|
*)
|
|
Die "unknown command: ${command}"
|
|
;;
|
|
esac
|
|
}
|
|
|
|
Main "$@"
|