This commit is contained in:
+32
-27
@@ -1,7 +1,8 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
# TallyNote native installer. Dry-run by default; pass --apply to mutate the host.
|
||||
# TallyNote native installer. Installs the latest release by default; use
|
||||
# --dry-run to preview without changing the host.
|
||||
PATH=/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/bin
|
||||
export PATH
|
||||
umask 077
|
||||
@@ -20,9 +21,9 @@ SIGNING_KEY=${TALLYNOTE_SIGNING_KEY:-}
|
||||
SIGNATURE_FORMAT=${TALLYNOTE_SIGNATURE_FORMAT:-ed25519}
|
||||
SHA256_FILE=${TALLYNOTE_SHA256_FILE:-}
|
||||
UPDATE_PUBLIC_KEY_FILE=${TALLYNOTE_UPDATE_PUBLIC_KEY_FILE:-}
|
||||
APPLY=0
|
||||
APPLY=1
|
||||
KEEP_RELEASES=${TALLYNOTE_KEEP_RELEASES:-3}
|
||||
REQUIRE_SIGNATURE=${TALLYNOTE_INSTALL_REQUIRE_SIGNATURE:-true}
|
||||
REQUIRE_SIGNATURE=${TALLYNOTE_INSTALL_REQUIRE_SIGNATURE:-false}
|
||||
ALLOW_DOWNGRADE=${TALLYNOTE_ALLOW_DOWNGRADE:-false}
|
||||
ALLOW_UNSIGNED=0
|
||||
MAX_RELEASE_MB=${TALLYNOTE_MAX_RELEASE_MB:-512}
|
||||
@@ -51,17 +52,19 @@ RELEASE_API_URL=${RELEASE_API_URL%/}
|
||||
|
||||
usage() {
|
||||
cat <<'EOF'
|
||||
Usage: install.sh [--apply] [--version VERSION] [--release-base-url HTTPS_URL]
|
||||
Usage: install.sh [--dry-run] [--version VERSION] [--release-base-url HTTPS_URL]
|
||||
[--release-file FILE] [--sha256-url HTTPS_URL|--sha256-file FILE]
|
||||
[--signature-url HTTPS_URL] [--signing-key PUBLIC_KEY_FILE]
|
||||
[--signature-format ed25519|gpg]
|
||||
[--update-public-key-file FILE]
|
||||
[--keep-releases N] [--allow-downgrade] [--allow-unsigned] [--dry-run]
|
||||
[--keep-releases N] [--allow-downgrade] [--allow-unsigned] [--apply]
|
||||
|
||||
The default is --dry-run. Network downloads and filesystem changes happen only
|
||||
with --apply. Production installs require a detached signature (Ed25519 over
|
||||
SHA256SUMS by default; legacy GPG archive signatures are opt-in); --allow-unsigned
|
||||
is for isolated development hosts only.
|
||||
Without arguments, the installer resolves the latest compatible release and
|
||||
installs it. SHA-256 from SHA256SUMS is always required. Detached signature
|
||||
verification is optional by default; enable it with
|
||||
TALLYNOTE_INSTALL_REQUIRE_SIGNATURE=true and provide a public key. Use
|
||||
--dry-run to inspect the selected release without downloading or changing the
|
||||
host. --apply is accepted for backwards compatibility.
|
||||
EOF
|
||||
}
|
||||
die() { printf 'tallynote installer: %s\n' "$*" >&2; exit 1; }
|
||||
@@ -275,9 +278,9 @@ verify_archive() {
|
||||
[[ -n "$expected" ]] || die "checksum file has no entry for $archive_name"
|
||||
[[ "$expected" =~ ^[A-Fa-f0-9]{64}$ ]] || die 'checksum file does not contain a SHA-256 digest'
|
||||
printf '%s %s\n' "$expected" "$archive" | sha256sum -c - >/dev/null || die 'SHA-256 verification failed'
|
||||
if [[ "$REQUIRE_SIGNATURE" == true ]]; then
|
||||
[[ -n "$signature" && -s "$signature" ]] || die '发布包缺少 SHA256SUMS.sig;生产安装必须使用签名'
|
||||
[[ -n "$key" && -f "$key" && ! -L "$key" ]] || die '生产安装必须提供签名公钥(--signing-key FILE)'
|
||||
if [[ "$REQUIRE_SIGNATURE" == true || ( -n "$signature" && -n "$key" ) ]]; then
|
||||
[[ -n "$signature" && -s "$signature" ]] || die '发布包缺少签名文件(SHA256SUMS.sig 或 .asc)'
|
||||
[[ -n "$key" && -f "$key" && ! -L "$key" ]] || die '签名校验需要有效的公钥文件(--signing-key FILE)'
|
||||
[[ "$(stat_uid "$key")" == 0 ]] || die '更新公钥必须由 root 拥有'
|
||||
[[ "$(wc -c < "$key" | tr -d '[:space:]')" -le 16384 ]] || die '更新公钥文件过大'
|
||||
local key_bits
|
||||
@@ -314,7 +317,7 @@ verify_archive() {
|
||||
fi
|
||||
fi
|
||||
elif [[ -n "$signature" || -n "$key" ]]; then
|
||||
log 'warning: signature verification disabled by explicit --allow-unsigned'
|
||||
log 'warning: signature verification skipped; provide both a signature and public key, or enable TALLYNOTE_INSTALL_REQUIRE_SIGNATURE=true'
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -636,7 +639,7 @@ validate_existing_env() {
|
||||
value=$(read_env_value "$file" TALLYNOTE_DATA_DIR)
|
||||
[[ -z "$value" || "${value%/}" == "${DATA_DIR%/}" ]] || die '环境文件中的数据目录与本次安装不一致'
|
||||
value=$(read_env_value "$file" TALLYNOTE_UPDATE_REQUIRE_SIGNATURE)
|
||||
[[ -z "$value" || "$value" == true ]] || die '环境文件禁止关闭发布签名校验'
|
||||
[[ -z "$value" || "$value" == true || "$value" == false ]] || die '环境文件中的签名校验配置必须是 true 或 false'
|
||||
value=$(read_env_value "$file" TALLYNOTE_UPDATE_METADATA_URL)
|
||||
if [[ -n "$value" ]]; then
|
||||
validate_env_value "$value" '环境文件更新源'
|
||||
@@ -710,7 +713,7 @@ main() {
|
||||
if (( APPLY )) || [[ -n "${TALLYNOTE_UNAME_BIN+x}" ]]; then
|
||||
validate_trusted_tool "$UNAME_BIN" 'uname'
|
||||
fi
|
||||
if (( APPLY )) || [[ -n "${TALLYNOTE_OPENSSL_BIN+x}" ]]; then
|
||||
if [[ "$REQUIRE_SIGNATURE" == true || -n "$SIGNATURE_URL" || -n "$SIGNING_KEY" || -n "$UPDATE_PUBLIC_KEY_FILE" || -n "${TALLYNOTE_OPENSSL_BIN+x}" ]]; then
|
||||
validate_trusted_tool "$OPENSSL_BIN" 'openssl'
|
||||
fi
|
||||
detect_platform
|
||||
@@ -730,7 +733,7 @@ main() {
|
||||
if [[ "$VERSION" == "latest" ]]; then
|
||||
if (( ! APPLY )); then
|
||||
[[ -z "$RELEASE_BASE_URL" ]] || require_https "$RELEASE_BASE_URL"
|
||||
log 'version: latest (release lookup happens with --apply)'
|
||||
log 'version: latest (release lookup skipped in dry-run)'
|
||||
log 'dry-run: pass --version VERSION to preview an exact artifact'
|
||||
return 0
|
||||
fi
|
||||
@@ -753,14 +756,15 @@ main() {
|
||||
artifact_url="$RELEASE_BASE_URL/$artifact"
|
||||
log "platform: ${TALLYNOTE_ARCH}/${TALLYNOTE_LIBC}; release: ${VERSION#v}"
|
||||
log "layout: $PREFIX/releases + atomic $PREFIX/current; data: $DATA_DIR"
|
||||
if (( ! APPLY )); then log 'dry-run: pass --apply to download, verify, extract, and configure systemd'; return 0; fi
|
||||
[[ "$REQUIRE_SIGNATURE" == true || "$ALLOW_UNSIGNED" -eq 1 ]] || die '生产安装必须校验发布签名;仅隔离开发环境可使用 --allow-unsigned'
|
||||
[[ "$("$UNAME_BIN" -s)" == Linux ]] || die '安装器只允许在 Linux 上执行 --apply'
|
||||
[[ $EUID -eq 0 ]] || die '--apply must run as root'
|
||||
if (( ! APPLY )); then log 'dry-run: no download, extraction, or systemd changes'; return 0; fi
|
||||
[[ "$("$UNAME_BIN" -s)" == Linux ]] || die '安装器只允许在 Linux 上执行'
|
||||
[[ $EUID -eq 0 ]] || die '安装必须以 root 运行'
|
||||
for command_name in curl sha256sum tar install sed awk find systemctl; do
|
||||
command -v "$command_name" >/dev/null 2>&1 || die "$command_name is required"
|
||||
done
|
||||
command -v "$OPENSSL_BIN" >/dev/null 2>&1 || die 'openssl is required'
|
||||
if [[ "$REQUIRE_SIGNATURE" == true || -n "$SIGNATURE_URL" || -n "$SIGNING_KEY" || -n "$UPDATE_PUBLIC_KEY_FILE" ]]; then
|
||||
command -v "$OPENSSL_BIN" >/dev/null 2>&1 || die 'openssl is required when signature verification is enabled'
|
||||
fi
|
||||
work=$(mktemp -d)
|
||||
INSTALL_WORK_DIR=$work
|
||||
INSTALL_BACKUP_DIR="$work/original"
|
||||
@@ -784,8 +788,9 @@ main() {
|
||||
[[ -z "$SHA256_FILE" ]] || die '本地 SHA256SUMS 文件不存在或是符号链接'
|
||||
download "$SHA256_URL" "$checksum" $((2 * 1024 * 1024))
|
||||
fi
|
||||
SIGNING_KEY=${SIGNING_KEY:-$UPDATE_PUBLIC_KEY_FILE}
|
||||
signature=''
|
||||
if [[ "$REQUIRE_SIGNATURE" == true ]]; then
|
||||
if [[ "$REQUIRE_SIGNATURE" == true || -n "$SIGNATURE_URL" || -n "$SIGNING_KEY" ]]; then
|
||||
if [[ "$SIGNATURE_FORMAT" == gpg ]]; then
|
||||
SIGNATURE_URL=${SIGNATURE_URL:-$RELEASE_BASE_URL/$artifact.asc}
|
||||
signature="$work/$artifact.asc"
|
||||
@@ -794,11 +799,7 @@ main() {
|
||||
signature="$work/SHA256SUMS.sig"
|
||||
fi
|
||||
download "$SIGNATURE_URL" "$signature" $((64 * 1024))
|
||||
elif [[ -n "$SIGNATURE_URL" ]]; then
|
||||
signature="$work/SHA256SUMS.sig"
|
||||
download "$SIGNATURE_URL" "$signature" $((64 * 1024))
|
||||
fi
|
||||
SIGNING_KEY=${SIGNING_KEY:-$UPDATE_PUBLIC_KEY_FILE}
|
||||
verify_archive "$archive" "$checksum" "$signature" "$SIGNING_KEY"
|
||||
[[ "$PREFIX" = /* && "$DATA_DIR" = /* && "$CONFIG_DIR" = /* ]] || die '安装、数据和配置目录必须是绝对路径'
|
||||
[[ ! -L "$DATA_DIR" && ! -L "$PREFIX" && ! -L "$CONFIG_DIR" ]] || die 'installation/data/config paths must not be symlinks'
|
||||
@@ -851,10 +852,14 @@ main() {
|
||||
ensure_env_key TALLYNOTE_UPDATE_STRATEGY systemd
|
||||
ensure_env_key TALLYNOTE_UPDATE_METADATA_URL "$RELEASE_API_URL"
|
||||
ensure_env_key TALLYNOTE_UPDATE_ALLOWED_HOSTS "$RELEASE_ALLOWED_HOSTS"
|
||||
ensure_env_key TALLYNOTE_UPDATE_REQUIRE_SIGNATURE true
|
||||
# The bootstrap verification key is also the key used by the privileged
|
||||
# updater unless the operator already configured a separate one.
|
||||
UPDATE_PUBLIC_KEY_FILE=${UPDATE_PUBLIC_KEY_FILE:-$SIGNING_KEY}
|
||||
if [[ -n "$UPDATE_PUBLIC_KEY_FILE" ]]; then
|
||||
ensure_env_key TALLYNOTE_UPDATE_REQUIRE_SIGNATURE true
|
||||
else
|
||||
ensure_env_key TALLYNOTE_UPDATE_REQUIRE_SIGNATURE false
|
||||
fi
|
||||
if [[ -n "$UPDATE_PUBLIC_KEY_FILE" ]]; then
|
||||
validate_install_path "$UPDATE_PUBLIC_KEY_FILE" '更新公钥路径'
|
||||
[[ -f "$UPDATE_PUBLIC_KEY_FILE" && ! -L "$UPDATE_PUBLIC_KEY_FILE" ]] || die 'update public key file is invalid'
|
||||
|
||||
Reference in New Issue
Block a user