release: simplify unsigned installation
TallyNote release / linux-x64 (push) Successful in 6m24s

This commit is contained in:
Qiufeng
2026-09-01 00:10:35 +08:00
parent 12495fb6a4
commit 4434acf697
14 changed files with 130 additions and 93 deletions
+32 -27
View File
@@ -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'